Текст
                    Beginning
Microsoft*
Visual СГ 2008
Karli Watson
Christian Nagel
Jacob Hammer Pedersen
Jon D. Reid
Morgan Skinner
Eric White
WILEY
Wiley Publishing, Inc.


Microsoft9 Visual СГ 2008 Базовый курс Карли Уотсон Кристиан Нейгел Якоб Хаммер Педерсен Джон Д. Рид Морган Скиннер Эрик Уайт 1 "Диалектика" Москва • Санкт-Петербург • Киев 2009
ББК 32.973.26-018.2.75 У65 УДК 681.3.07 Компьютерное издательство "Диалектика" Зав. редакцией С.Н. Тригуб Перевод с английского Я.П. Валковой, ДЛ. Иваненко, Ю.И. Корниенко, НА. Мухина Под редакцией Ю.Н. Артеменко По общим вопросам обращайтесь в издательство "Диалектика" по адресу: info@dialektika.com, http://www.dialektika.com Уотсон, Карл и, Нейгел, Кристиан, Педерсен, Якоб Хаммер, Рид, Джон Д., Скиннер, Морган, Уайт, Эрик. У65 Visual С# 2008: базовый курс. : Пер. с англ. - М. : ООО "И.Д. Вильяме", 2009. - 1216 с.: ил. — Парал. тит. англ. ISBN 978-5-8459-1532-0 (рус.) ББК 32.973.264I8.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Wrox Press. Copyright © 2009 by Dialektika Computer Publishing. Original English language edition Copyright © 2008 by Wiley Publishing, Inc., Indianapolis, Indiana All rights reserved including the right of reproduction in whole or in part in any form. This translation is published by arrangement with Wiley Publishing, Inc. No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, scanning or otherwise, without either the prior written permission of the Publisher. Wiley, the Wiley logo, Wrox, the Wrox logo, Wrox Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley &Sons, Inc. and/or its affiliates, in the United States and other countries, and may not be used without written permission. Microsoft and Visual C# are registered trademarks of Microsoft Corporation in the United States and/or other countries. All other trademarks are the property of their respective owners. Wiley Publishing, Inc., is not associated with any product or vendor mentioned in this book. Научно-популярное издание Карли Уотсон, Кристиан Нейгел, Якоб Хаммер Педерсен, Джон Д. Рид, Морган Скиннер, Эрик Уайт Visual C# 2008: базовый курс Верстка Т.Н. Артеменко Художественный редактор В.Г. Павлютин Подписано в печать 17.02.2009. Формат 70x100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 98,04. Уч.-изд. л. 76,23. Тираж 1000 экз. Заказ № 14379. Отпечатано по технологии CtP в ОАО "Печатный двор" им. А. М. Горького 197110, Санкт-Петербург, Чкаловский пр., 15. ООО аИ. Д. Вильяме", 127055, г. Москва, ул. Лесная, д. 43, стр. 1 ISBN 978-5-8459-1532-0 (рус.) © Компьютерное издательство "Диалектика", 2009, перевод, оформление, макетирование ISBN 978-0-470-191354 (англ.) © by Wiley Publishing, Inc., Indianapolis, Indiana, 2008
Оглавление Часть I. Язык С# 29 Глава 1. Начальные сведения о языке С# 30 Глава 2. Написание программы на языке С# 40 Глава 3. Переменные и выражения 56 Глава 4. Управление потоком выполнения 82 Глава 5. Дополнительные сведения о переменных 114 Глава 6. Функции 144 Глава 7. Отладка и обработка ошибок 172 Глава 8. Введение в объектно-ориентированное программирование 203 Глава 9. Определение классов 226 Глава 10. Определение членов класса 255 Глава 11. Коллекции, сравнения и преобразования 289 Глава 12. Обобщения 340 Глава 13. Дополнительные приемы объектно-ориентированного программирования 376 Глава 14. Расширения в языке С# 3.0 402 Часть II. Программирование для Windows 433 Глава 15. Основы программирования для Windows 434 Глава 16. Расширенные функциональные возможности Windows Forms 490 Глава 17. Использование обычных диалоговых окон 528 Глава 18. Развертывание Windows-приложений 572 Часть III. Программирование для Web 613 Глава 19. Основы программирования для Web 614 Глава 20. Расширенное программирование для Web 656 Глава 21. Web-службы 691 Глава 22. Программирование с использованием Ajax 718 Глава 23. Развертывание Web-приложений 736 Часть IV. Доступ к данным 751 Глава 24. Данные файловой системы 752 Глава 25. XML 790 Глава 26. Введение в LINQ 818 Глава 27. LINQ to SQL 858 Глава 28. ADO.NET и LINQ поверх DataSet 890 Глава 29. LINQ to XML 942 Часть V. Дополнительные технологии 963 Глава 30. Атрибуты 964 Глава 31. XML-документация 991 Глава 32. Передача данных по сети 1014 Глава 33. Введение в GDI+ 1042 Глава 34. Windows Presentation Foundation 1074 Глава 35. Windows Communication Foundation 1145 Глава 36. Windows Workflow Foundation 1176 Предметный указатель 1206
Содержание Об авторах 21 Введение 22 На кого рассчитана эта книга 23 Как организована эта книга 23 Язык С# (главы 1-14) 24 Программирование для Windows (главы 15-18) 25 Программирование для Web (главы 19-23) 25 Доступ к данным (главы 24-29) 25 Дополнительные технологии (главы 30-36) 26 Что необходимо для использования этой книги 26 Соглашения 27 Исходный код 27 От издательства 28 Часть I. Язык С# 29 Глава 1. Начальные сведения о языке С# 30 Что собой представляет .NET Framework 30 Что входит в состав .NET Framework 31 Написание приложений с помощью .NET Framework 32 Что собой представляет язык С# 35 Приложения, которые можно писать на С# 36 Язык С# в этой книге 37 Visual Studio 2008 37 Продукты Visual Studio 2008 Express 38 Решения 39 Резюме 39 Глава 2. Написание программы на языке С# 40 Среды разработки 41 Visual Studio 2008 41 Visual C# 2008 Express Edition 44 Консольные приложения 45 Окно Solution Explorer 48 Окно Properties 49 Окно Error List 49 Приложения Windows Forms 51 Резюме 55 Глава 3. Переменные и выражения 56 Базовый синтаксис С# 57 Структура базового консольного приложения на С# 59 Переменные 61 Простые типы 61 Именование переменных 66
Содержание Литеральные значения 68 Объявление переменных и присваиванием им значений 70 Выражения 70 Математические операции 71 Операции присваивания 76 Приоритеты операций 76 Пространства имен 77 Резюме 80 Упражнения 81 Глава 4. Управление потоком выполнения 82 Булевская логика 82 Поразрядные операции 85 Булевские операции присваивания 89 Дополнение таблицы приоритетов операций 91 Оператор goto 92 Ветвление 93 Тернарная операция 93 Оператор if 94 Оператор switch 98 Организация циклов 101 Циклы do 102 Циклы while 104 Циклы for 106 Прерывание циклов 111 Бесконечные циклы 112 Резюме 112 Упражнения 113 Глава 5. Дополнительные сведения о переменных 114 Преобразование типов 115 Неявное преобразование 115 Явное преобразование 116 Явные преобразования с помощью команд Convert 120 Сложные типы переменных 123 Перечисления 123 Структуры 128 Массивы 131 Манипулирование строками 137 Резюме 142 Упражнения 143 Глава 6. Функции 144 Определение и использование функций 145 Возвращаемые значения 147 Параметры 149 Область видимости переменных 156 Область видимости переменных в других структурах 159 Параметры и возвращаемые значения вместо глобальных данных 161
8 Содержание Функция Main () 162 Использование функций в структурах 165 Перегрузка функций 165 Делегаты 167 Резюме 170 Упражнения 171 Глава 7. Отладка и обработка ошибок 172 Отладка в VS и VCE 173 Выполнение отладки в непрерывном (обычном) режиме 174 Выполнение отладки в режиме останова 183 Обработка ошибок 194 Ключевые слова try.. .catch.. .finally 194 Просмотр и конфигурирование исключений 199 Примечания по обработке исключений 200 Резюме 201 Упражнения 202 Глава 8. Введение в объектно-ориентированное программирование 203 Что собой представляет объектно-ориентированное программирование 204 Что собой представляет объект 205 Объектом является все, что угодно 208 Жизненный цикл объекта 209 Статические члены классов и члены экземпляров классов 210 Приемы объектно-ориентированного программирования 211 Интерфейсы 212 Наследование 213 Полиморфизм 216 Отношения между объектами 218 Перегрузка операций 220 События 220 Ссылочные типы и типы-значения 221 Объектно-ориентированное программирование для Windows-приложений 221 Резюме 224 Упражнения 225 Глава 9. Определение классов 226 Определение классов в С# 226 Определения интерфейсов 229 Класс System. Obj ect 232 Конструкторы и деструкторы 234 Последовательность выполнения конструкторов 235 Средства объектно-ориентированного программирования, доступные в VS и VCE 239 Окно Class View 239 Окно Object Browser 241 Добавление классов 242 Диаграммы классов 243 Проекты библиотек классов 245
Содержание 9 Интерфейсы или абстрактные классы 248 Типы-структуры 251 Поверхностное копирование или глубокое копирование 253 Резюме 253 Упражнения 254 Глава 10. Определение членов класса 255 Определение членов 255 Определение полей 256 Определение методов 257 Определение свойств 258 Добавление членов из диаграммы классов 263 Рефакторизация членов 266 Автоматические свойства 267 Дополнительные вопросы, касающиеся членов классов 267 Сокрытие методов базового класса 268 Вызов переопределенных и скрытых методов базового класса 269 Вложенные определения типов 270 Реализация интерфейсов 271 Реализация интерфейсов в классах 272 Частичные определения классов 275 Частичные определения методов 276 Пример приложения 278 Продумывание приложения 278 Написание библиотеки классов 279 Добавление класса Card 282 Клиентское приложение для библиотеки классов 286 Резюме 287 Упражнения 288 Глава 11. Коллекции, сравнения и преобразования 289 Коллекции 289 Использование коллекций 290 Определение коллекций 297 Индексаторы 298 Добавление коллекции Cards в CardLib 300 Ключевые коллекции и интерфейс IDictionary 303 Итераторы 305 Глубокое копирование 310 Добавление возможности глубокого копирования в CardLib 312 Сравнения 314 Сравнение типов 314 Сравнение значений 319 Преобразования 335 Перегрузка операций преобразования 335 Операция as 336 Резюме 338 Упражнения 338
10 Содержание Глава 12. Обобщения 340 Что собой представляют обобщения 340 Использование обобщений 342 Нулевые типы 342 Пространство имен System.Collections .Generics 349 Определение обобщений 359 Определение обобщенных классов 360 Определение обобщенных интерфейсов 371 Определение обобщенных методов 372 Определение обобщенных делегатов 374 Резюме 374 Упражнения 374 Глава 13. Дополнительные приемы объектно-ориентированного программирования 376 Операция : : и спецификатор глобального пространства имен 376 Специальные исключения 378 Базовые классы исключений 378 Добавление специальных исключений в CardLib 378 События 380 Что собой представляет событие 380 Обработка событий 382 Определение событий 384 Расширение и использование CardLib 392 Клиентское приложение для игры в карты, использующее CardLib 392 Резюме 400 Упражнения 401 Глава 14. Расширения в языке С# 3.0 402 Инициализаторы 403 Инициализаторы объектов 403 Инициализаторы коллекций 405 Выведение типов 408 Анонимные типы 410 Методы расширений 414 Лямбда-выражения 420 Краткое повторение анонимных методов 420 Использование лямбда-выражений для анонимных методов 421 Параметры лямбда-выражений 425 Тело операторов лямбда-выражений 425 Лямбда-выражения как делегаты и как деревья выражений 426 Лямбда-выражения и коллекции 427 Резюме 430 Упражнения 431
Содержание 11 Часть II. Программирование для Windows 433 Глава 15. Основы программирования для Windows 434 Элементы управления 435 Свойства 435 Привязка, стыковка и определение формы элементов управления 437 События 438 Элемент управления Button 440 Свойства элемента управления Button 441 События элемента управления Button 441 Элементы управления Label и LinkLabel 444 Элемент управления TextBox 445 Свойства элемента управления TextBox 445 События элемента управления TextBox 446 Элементы управления RadioButton и CheckBox 454 Свойства элемента управления RadioButton 455 События элемента управления RadioButton 455 Свойства элемента управления CheckBox 456 События элемента управления CheckBox 456 Элемент управления GroupBox 457 Элемент управления RichTextBox 461 Свойства элемента управления Ri chTextBox 461 События элемента управления Ri chTextBox 462 Элементы управления ListBox и CheckedListBox 468 Свойства элемента управления ListBox 468 Методы элемента управления ListBox 470 События элемента управления ListBox 470 Добавление обработчиков событий 472 Элемент управления ListView 473 Свойства элемента управления Li s tView 473 Методы элемента управления List View 476 События элемента управления ListView 476 ListViewItem 476 ColumnHeader 477 Элемент управления ImageList 477 Элемент управления TabControl 484 Свойства элемента управления TabControl 485 Работа с элементом управления TabControl 485 Резюме 488 Упражнения 489 Глава 16. Расширенные функциональные возможности Windows Forms 490 Меню и панели инструментов 491 Два как один 491 Использование элемента управления MenuStrip 491 Создание меню вручную 492 Дополнительные свойства элемента управления ToolStripMenuItem 495 Добавление функциональных возможностей меню 495
12 Содержание Панели инструментов 497 Свойства элемента управления Tool Strip 498 Элементы элемента управления Too IS trip 499 Элемент управления Status St rip 504 Свойства элемента управления StatusStripStatusLabel 504 Приложения SDI и MDI 507 Построение MDI-приложений 509 Создание элементов управления 517 Элемент управления Label Тех tbox 519 Отладка пользовательских элементов управления 523 Расширение элемента управления Label Тех tbox 524 Резюме 527 Упражнения 527 Глава 17. Использование обычных диалоговых окон 528 Обычные диалоговые окна 529 Применение диалоговых окон 531 Файловые диалоговые окна 532 OpenFileDialog 532 SaveFileDialog 544 Печать 549 Архитектура печати 549 Печать нескольких страниц 555 Диалоговое окно PageSetupDialog 557 Класс PrintDialog 560 Предварительный просмотр 563 Класс PrintPreviewDialog 564 Класс PrintPreviewControl 564 Диалоговые окна FontDialog и ColorDialog 565 FontDialog 566 ColorDialog 568 Диалоговое окно FolderBrowserDialog 569 Резюме 570 Упражнения 571 Глава 18. Развертывание Windows-приложений 572 Обзор процесса развертывания 572 Развертывание ClickOnce 574 Обновления 584 Типы проектов установки и развертывания Visual Studio 585 Архитектура программы установки Microsoft Windows 586 Термины программы установки Windows 587 Преимущества программы установки Windows 589 Создание установочного пакета для приложения SimpleEditor 589 Планирование установки 589 Создание проекта 590 Свойства проекта 591 Редакторы установки 594 Редактор File System Editor 595
Содержание 13 Редактор File Types Editor 598 Редактор Launch Condition Editor 600 Редактор User Interface Editor 601 Компоновка проекта 605 Инсталляция 605 Окно Welcome 605 Окно Read Me 606 Окно License Agreement 606 Необязательные файлы 607 Окно Select Installation Folder 607 Окно Disk Cost 608 Окно Confirm Installation 608 Окно Progress 609 Окно Installation Complete 609 Выполнение приложения 610 Удаление приложения 610 Резюме 610 Упражнения 611 Часть III. Программирование для Web 6is Глава 19. Основы программирования для Web 614 Обзор 615 Исполняющая среда ASP.NET 615 Создание простой страницы 616 Серверные элементы управления 623 Обработчики событий 624 Проверка данных, вводимых пользователем 628 Управление состоянием 632 Управление состоянием на стороне клиента 633 Управление состоянием на стороне сервера 635 Аутентификация и авторизация 638 Конфигурация аутентификации 638 Использование элементов управления безопасностью 643 Чтение и запись в базу данных SQL Server 646 Резюме 654 Упражнения 655 Глава 20. Расширенное программирование для Web 656 Мастер-страницы 657 Система навигации по сайту 663 Пользовательские элементы управления 664 Профили 667 Группы профилей 669 Профили и компоненты 670 Профили и специальные типы данных 670 Профили и анонимные пользователи 670 Web-части 672 Диспетчер Web-частей 673
14 Содержание Зона Web-частей 673 Зона Editor 676 Зона Catalog 679 Зона Connections 680 JavaScript 684 Элемент Script 684 Объявление переменных 684 Определение функций 685 Операторы 685 Объекты 685 Резюме 689 Упражнения 690 Глава 21. Web-службы 691 Предшественники Web-служб 692 Вызов удаленных процедур (RPC) 692 SOAP 693 Применение Web-служб 694 Сценарий использования приложения бюро путешествий 694 Сценарий использования приложения распространения книг 695 Типы клиентских приложений 695 Архитектура приложения 695 Архитектура Web-служб 696 Методы, которые можно вызывать 696 Вызов метода 697 SOAP и брандмауэры 699 Базовый профиль WS-I 699 Web-службы и каркас .NET Framework 699 Создание Web-службы 700 Клиент 702 Создание простой Web-службы ASP.NET 702 Добавления Web-метода 704 Тестирование Web-службы 705 Реализация Windows-клиента 706 Асинхронный вызов службы 710 Реализация клиента ASP.NET 712 Передача данных 713 Резюме 716 Упражнения 717 Глава 22. Программирование с использованием Ajax 718 Что собой представляет технология Ajax 718 Элемент управления Update Panel 721 Элемент управления Timer 726 Элемент управления UpdateProgress 726 Web-службы 728 Расширяющие элементы управления 733 Резюме 735 Упражнения 735
Содержание 15 Глава 23. Развертывание Web-приложений 736 Компонент IIS 736 Конфигурирование IIS 738 Копирование Web-сайта 740 Публикация Web-сайта 743 Инсталлятор Windows 744 Создание установочной программы 744 Инсталляция Web-приложения 746 Резюме 749 Упражнения 749 Часть IV. Доступ к данным 751 Глава 24. Данные файловой системы 752 Потоки 753 Классы ввода и вывода 753 Классы File и Directory 755 Класс Filelnfо 756 Класс Directorylnfо 758 Путевые имена и относительные пути 758 Объект FileStream 759 Позиция в файле 760 Чтение данных 761 Объект StreamWriter 765 Объект StreamReader 767 Файлы с разделителями 770 Чтение и запись сжатых файлов 774 Сериализованные объекты 777 Мониторинг структуры файла 782 Резюме 788 Упражнения 789 Глава 25. XML 790 Документы XML 791 Элементы XML 791 Атрибуты 792 Объявление XML 793 Структура документа XML 793 Пространства имен XML 794 Правильно оформленный и действительный XML 795 Проверка действительности документов XML 795 Схемы 796 Использование XML в приложении 799 Объектная модель документа XML 799 Класс XmlElement 800 Изменение значений узлов 804 Выборка узлов 809 XPath 809 Резюме 816 Упражнения 817
1 б Содержание Глава 26. Введение в LINQ 818 Вариации LINQ 819 Первый запрос LINQ 820 Объявление переменной результата с использованием ключевого слова var 821 Спецификация источника данных: конструкция from 822 Спецификация условия: конструкция where 822 Выбор элементов: конструкция select 823 Последний штрих: использование цикла for each 823 Отложенное выполнение запроса 823 Использование синтаксиса методов LINQ и лямбда-выражений 823 Расширяющие методы LINQ 824 Синтаксис запросов в сравнении с синтаксисом методов 824 Лямбда-выражения 825 Упорядочивание результатов запроса 827 Конструкция о г derby 828 Упорядочивание с использованием синтаксиса методов 829 Упорядочивание больших наборов данных 830 Агрегатные операции 832 Запросы сложных объектов 836 Проекция: создание новых объектов в запросах 839 Проекция: синтаксис методов 841 Запрос Select Distinct 842 Any и All 843 Многоуровневое упорядочивание 844 Синтаксис методов многоуровневого упорядочивания: ThenBy 846 Групповые запросы 847 Take и Skip 849 First и FirstOrDefault 851 Операции с множествами 852 Соединения 855 Ресурсы для дополнительного изучения 856 Резюме 857 Упражнения 857 Глава 27. LINQ to SQL 858 Объектно-реляционное отображение (ORM) 859 Инсталляция SQL Server и данных примеров Northwind 860 Инсталляция SQL Server Express 2005 860 Инсталляция базы данных примеров Northwind 861 Первый запрос LINQ to SQL 861 Навигация по отношениям LINQ to SQL 869 Дальнейшее углубление в LINQ to SQL 872 Группировка, упорядочивание и другие расширенные запросы в LINQ to SQL 875 Отображение сгенерированного SQL 877 Привязка данных с LINQ to SQL 881 Обновление связанных данных с помощью LINQ to SQL 886 Резюме 888 Упражнения 888
Содержание 17 Глава 28. ADO.NET и LINQ поверх DataSet 890 Что такое ADO.NET? 891 Почему это называется ADO.NET? 891 Цели проектирования ADO.NET 892 Обзор классов и объектов ADO.NET 894 Объекты поставщика 894 Объекты-потребители 895 Использование пространства имен System. Data 896 Чтение данных с помощью DataReader 897 Чтение данных с помощью DataSet 904 Наполнение DataSet данными 904 Доступ к таблицам, строкам и столбцам в DataSet 905 Обновление базы данных 908 Добавление строк в базу данных 911 Поиск строк 914 Удаление строк 917 Доступ к нескольким таблицам в DataSet 919 Отношения в ADO.NET 919 Навигация по отношениям 919 XML и ADO.NET 926 Поддержка XML в объектах DataSet из ADO.NET 926 Поддержка SQL в ADO.NET 929 Команды SQL в адаптерах данных 929 Использование SELECT с конструкцией WHERE 930 Просмотр SQL-команд SELECT, UPDATE, INSERT и DELETE 930 Непосредственное выполнение команд SQL 932 Вызов хранимой процедуры SQL 935 Использование LINQ поверх DataSet с ADO.NET 937 Когда использовать LINQ поверх DataSet 937 Резюме 940 Упражнения 941 Глава 29. LINQ to XML 942 Функциональные конструкторы LINQ to XML 943 Конструирование текста элемента XML со строками 946 Сохранение и загрузка документа XML 946 Загрузка XML из строки 949 Содержимое сохраненного документа XML 949 Работа с фрагментами XML 950 Генерация XML из LINQ to SQL 952 Отображение XML-документа Northwind Customer Orders 955 Как запрашивать документ XML 956 Использование членов запроса 957 Резюме 962 Упражнения 962
18 Содержание Часть V. Дополнительные технологии 963 Глава 30. Атрибуты 964 Что такое атрибут 965 Рефлексия 968 Встроенные атрибуты 971 System.Diagnostics.ConditionalAttribute 972 System.ObsoleteAttribute 974 System.SerializableAttribute 976 System.Reflection.AssemblyDelaySignAttribute 979 Пользовательские атрибуты 982 BugFixAttribute 983 Резюме 990 Глава 31. XML-документация 991 Добавление XML-документации 992 Комментарии XML-документации 994 Добавление XML-документации с использованием диаграммы классов 1001 Генерирование файлов XML-документации 1004 Пример приложения, оснащенного XML-документацией 1007 Применение XML-документации 1008 Программная обработка XML-документации 1009 Стилевое оформление XML-документации с помощью XSLT 1011 Средства документирования 1012 Резюме 1013 Упражнения 1013 Глава 32. Передача данных по сети 1014 Обзор сетевой передачи 1014 Преобразование имен 1017 Унифицированный идентификатор ресурса 1019 Протоколы TCP и UDP 1020 Прикладные протоколы 1020 Варианты программирования сетевой передачи 1022 WebClient 1023 Классы WebRequest и WebResponse 1025 Классы TcpListener и TcpClient 1033 Резюме 1041 Упражнения 1041 Глава 33. Введение в GDI+ 1042 Обзор графического рисования 1043 Класс Graphics 1043 Удаление объектов 1044 Система координат 1045 Области 1051 Цвета 1052 Рисование линий с использованием класса Реп 1053
Содержание 19 Рисование форм с использованием класса Brush 1056 Прорисовка текста с использованием класса Font 1058 Прорисовка с использованием изображений 1062 Рисование с использованием текстурной кисти 1064 Двойная буферизация 1068 Расширенные возможности GDI+ 1070 Отсечение 1070 Пространство имен System. Drawing. Drawing2D 1072 Пространство имен System. Drawing. Imaging 1072 Резюме 1072 Упражнения 1073 Глава 34. Windows Presentation Foundation 1074 Что собой представляет WPF 1075 Возможности, которые WPF предлагает для дизайнеров 1076 Возможности, которые WPF предлагает для разработчиков приложений на С# 1079 Структура базового WPF-приложения 1079 Основные понятия WPF 1090 Синтаксис XAML 1091 Настольные приложения и Web-приложения 1094 Объект приложения 1095 Основные сведения об элементах управления 1096 Компоновка элементов управления 1105 Стилизация элементов управления 1115 Триггеры 1121 Анимация 1122 Статические и динамические ресурсы 1125 Программирование с использованием WPF 1131 Пользовательские элементы управления в WPF 1132 Реализация свойств зависимостей 1132 Резюме 1142 Упражнения 1143 Глава 35. Windows Communication Foundation 1145 Что собой представляет WCF 1146 Концепции WCF 1147 Коммуникационные протоколы 1147 Адреса, конечные точки и привязки 1148 Контракты 1150 Шаблоны передачи сообщений 1151 Механизмы поведения 1151 Хостинг 1152 Программирование с использованием WCF 1152 Определение контрактов для служб WCF 1161 Службы WCF с собственным хостом 1168 Резюме 1174 Упражнения 1175
20 Содержание Глава 36. Windows Workflow Foundation 1176 Действия 1180 Действие DelayActivity 1180 Действие SuspendActivity 1181 Действие WhileActivity 1183 Действие SequenceActivity 1185 Специальные действия 1189 Исполняющая среда рабочего потока 1194 Привязка данных 1201 Резюме 1205 Предметный указатель 1206
Об авторах Карли Уотсон (Karli Watson) — внештатный специалист по информационным технологиям, автор и разработчик. Кроме того, он работает техническим консультантом в компаниях 3form Ltd. (www.3form.net) и Boost.net (www.boost.net) и первым помощником специалиста по техническим вопросам в Content Master (www. contentmaster. com). По большей части Карли увлекается .NET (в особенности языком С#) и написал на эту тему немало книг. Он умеет передавать идеи так, чтобы они были понятны любому, кто жаждет знаний, и проводит массу времени за изучением новых технологий в поисках новых вещей, которым он еще может научить людей. В те редкие периоды, когда Карли не занят ничем из вышеперечисленного, он мечтает бороздить заснеженные склоны гор на сноуборде или напечатать свой роман. В любом случае, его всегда можно узнать по яркой разноцветной одежде. Кристиан Нейгел (Christian Nagel) — архитектор и разработчик программного обеспечения, занимающийся обучением и консультациями по проектированию, а также сотрудник компании Thinktecture (www.thinktecture.com), проводящий учебные курсы и семинары по .NET-технологиям Microsoft. Его достижения в сообществе разработчиков позволили получить пост регионального директора Microsoft и звание наиболее ценного специалиста по ASP.NET. Еще он известен как автор нескольких замечательных книг, среди которых Professional C#, Pro .NETNetwork Programming^ Enterprise Services with the .NET Frameworks, и регулярно выступает с докладами на различных соответствующих его отрасли конференциях. Разработкой и проектированием архитектуры программного обеспечения Кристиан занимается более 15 лет. Он начинал свою компьютерную карьеру с PDP 11 и VAX/VMS и занимался различными языками и платформами. Начиная с 2000 г., он стал работать с .NET и С# и строить распределенные решения. С ним можно связаться на его сайте по адресу www. christiannagel. com. Якоб Хаммер Педерсен (Jacob Hammer Pedersen) — разработчик систем в датской компании Fujitsu Service. Он занимается программированием ПК с начала 90-х годов прошлого века с применением самых различных языков, в том числе Pascal, Visual Basic, C/C++ и С#. Якоб является соавтором целого ряда книг по .NET и работает с широким спектром технологий Microsoft, начиная от SQL Server и заканчивая расширениями Office. Живет и работает в Дании, в городе Орхус. Джон Рид (Jon D. Reid) — начальник отдела системного проектирования в компании Indigo Biosystems, Inc. (www.indigobio.com) и независимый поставщик ПО для биологических наук, где он разрабатывает приложения на С# для сред Microsoft. Он является соавтором многих книг по .NET, в том числе Beginning Visual C# 2005, Beginning С# Databases: From Novice to Professional, Pro Visual Studio .NET, AD0.NETProgrammer's Reference и Professional SQL Server 2000 XML. Морган Скиннер (Morgan Skinner) начал программировать еще школьником в 1980 г. и с тех пор увлекся компьютерными технологиями. Сейчас он работает на компанию Microsoft в качестве консультанта по разработке приложений и помогает клиентам с созданием архитектур, дизайном, кодированием и тестированием. Он занялся .NET после выхода PDC с 2000 г. и написал по этому продукту несколько статей MSDN и книг. В свое свободное время он развлекается борьбой с сорняками на своем участке. С Морганом можно связаться на его сайте по адресу www.morganskinner.com. Эрик Уайт (Eric White) — независимый консультант по программному обеспечению с более чем двадцатилетним опытом в сфере построения информационных систем и систем бухгалтерского учета. В период, когда он не занят программированием на С#, его наверняка можно найти с ледорубом в руках, карабкающимся вверх по какой-нибудь горе.
Введение Язык С# является относительно новым языком, о котором миру впервые стало известно тогда, когда Microsoft в июле 2000 г. объявила о выходе первой версии .NET Framework. С тех пор он сильно вырос в плане популярности и стал чуть ли не самым предпочитаемым языком среди разработчиков Windows- и Web-приложений, которые используют .NET Framework. Отчасти привлекательность языка С# связана с его понятным синтаксисом, который происходит от синтаксиса C/C++, но упрощает некоторые вещи, которые ранее не находили одобрения среди многих программистов. Несмотря на это упрощение, язык С# обладает той же мощью, что и C++, и потому теперь нет никакой причины не переходить на его использование. Этот язык не сложен, что делает его замечательным кандидатом для изучения элементарных приемов программирования. Простота в изучении в сочетании с возможностями .NET Framework превращают язык С# в прекрасный вариант для начала программистской карьеры. В состав последней версии языка С#— С#3.0, — которая поставляется с .NET Framework 3.5, вошли как существующие успешные, так и новые даже более привлекательные функциональные возможности. Некоторые из них своими корнями уходят в C++ (во всяком случае, внешне), но некоторые все же являются совершенно новыми. В последних выпусках средств разработки для продуктов линейки Visual Studio и Visual Studio Express тоже предлагается много усовершенствований и улучшений для упрощения жизни разработчикам и значительного повышения их продуктивности. В настоящей книге преследуется цель ознакомить со всеми аспектами программирования на С#, начиная с самого языка, написания на нем Windows- и Web-приложений и использования источников данных, и заканчивая некоторыми усовершенствованными приемами вроде программирования графических объектов. Также в ней можно будет узнать о возможностях Visual C# Express 2008, Visual Web Developer Express 2008 и Visual Studio 2008 и всех способах, которыми эти продукты могут помогать при разработке приложений. Книга написана в дружественной и последовательной манере с приложением максимума усилий для облегчения процесса изучения все более и более сложных приемов. Ни в одном месте технические термины не появляются вдруг ниоткуда; каждое понятие вводится и объясняется по мере необходимости. Употребление технического жаргона сведено к минимуму, но там, где без него никак не обойтись, в контексте обязательно приводятся соответствующие определения и пояснения. Авторы книги являются экспертами в своей области и одинаково страстно увлекаются как С#, так и .NET Framework. Вряд ли удастся найти группу людей, более способных своими совместными знаниями обучить языку С#, начиная с простейших принципов и заканчивая усовершенствованными приемами. Помимо базовых сведений в книге также приводится масса полезных рекомендаций, советов, упражнений и полноценных примеров кода (доступных для загрузки на сайте издательства), которые смогут пригодиться во время освоения материала. Мы делимся этими знаниями безо всякой утайки и надеемся, что вы сможете воспользоваться ими и стать наилучшим в своем роде программистом. Удачи и всего самого наилучшего!
Введение 23 На кого рассчитана эта книга Данная книга предназначена для любого, кто желает научиться программировать на языке С# с использованием .NET Framework. В первых главах рассказывается о самом языке, исходя из предположения об отсутствии какого-либо опыта программирования. Тем, кому приходилось ранее программировать на других языках, большая часть материала в этих главах будет знакома. Многие аспекты синтаксиса С# совпадают с рядом других языков, а многие структуры (вроде структур для организации циклов и ветвления) выглядят вообще так же, как и во всех языках программирования. Однако те, кто не является опытным программистом, смогут извлечь из этих глав пользу и узнать, как все эти приемы выглядят конкретно в С#. Тем, кто является новичком в программировании, следует начинать с самого начала. Тем, кто не знаком с .NET Framework, но умеет программировать, можно прочитать главу 1 и затем пропустить несколько следующих глав вплоть до того, когда начнется рассмотрение способов применения изученных понятий С# на практике. Тем, кто умеет программировать, но никогда ранее не сталкивался с языком объектно- ориентированного программирования, можно начинать изучение с главы 8. В качестве альтернативного варианта те, кто знаком с языком С#, могут предпочесть сконцентрироваться на главах, посвященных недавним разработкам в .NET Framework и С#, т.е. главах о коллекциях, обобщениях и дополнениях, появившихся в версии С# 3.0 (главы с 11 по 14), или вообще пропустить первый раздел книги полностью и начать с главы 15. При написании глав в этой книге преследовались две цели: сделать так, чтобы их можно было читать последовательно для получения полного курса обучения по С#, и так, чтобы при необходимости в них можно было заглядывать выборочно для получения справочного материала. Помимо основного материла в состав каждой главы входит ряд специально отобранных упражнений, предназначенных для закрепления пройденного материала. Упражнения включают как простые вопросы, допускающие несколько возможных ответов или ответов типа "да/нет", так более сложные вопросы, требующие внесения изменений в предлагаемые приложения и создания своих собственных. Ответы на все эти упражнения доступны для загрузки на Web-сайте издательства. Как организована эта книга Эта книга состоит из шести частей, которые кратко описаны ниже. □ Введение. В этой части рассказывается о том, чему посвящена данная книга и на кого она рассчитана. □ Язык С#. В этой части рассматриваются все аспекты языка С#, начиная от базовых понятий и заканчивая объектно-ориентированными приемами. □ Программирование для Windows. В этой части рассказывается о том, как писать и развертывать Windows-приложения на языке С#. □ Программирование для Web. В этой части описываются способы разработки и развертывания Web-приложений и Web-служб. □ Доступ к данным. В этой части рассказывается о том, как в приложениях работать с данными, включая данные, которые хранятся в файлах на жестком диске, данные в формате XML и данные, хранящиеся в базах.
24 Введение □ Дополнительные технологии. В этой части рассматриваются некоторые дополнительные способы применения С# и .NET Framework, включая атрибуты, документацию XML, сетевые коммуникации и программирование графических объектов с помощью GDI+. Также здесь рассказывается о WPF, WCF и WF — технологиях, которые впервые появились в .NET 3.0 и были улучшены в .NET 3.5. В следующих разделах приводится краткое описание содержимого глав каждой из пяти частей книги. ЯзыкС# (главы 1-14) Глава 1 знакомит с языком С# и показывает, каким образом он вписывается в ландшафт .NET. Здесь читатель узнает об основных приемах программирования, которыми можно пользоваться в данной среде, и том, чем полезны Visual C# Express (VCE) и Visual Studio (VS). Глава 2 позволяет приступить к написанию приложений на С#. Здесь читатель познакомится с синтаксисом языка С# и сможет попробовать использовать его на практике путем создания предлагаемых в качестве примера приложений командной строки и приложений Windows. Эти примеры демонстрируют то, как быстро и легко создавать приложения и вводить их в эксплуатацию. Еще здесь читатель сможет познакомиться с тем, как выглядят среды разработки VCE и VS, а также основными окнами и средствами, с которыми придется иметь дело в остальной части книги. Далее читателю предлагается узнать больше об основах языка С#. В частности, в главе 3 можно узнать о том, что собой представляют переменные и как ими манипулировать, в главе 4 — как улучшать структуру приложений за счет управления выполнением программы (за счет добавления циклов и ветвлений), в главе 5 — о некоторых других, более сложных типах переменных, наподобие массивов, а в главе 6 — как инкапсулировать код в функции для упрощения выполнения повторяющихся операций и повышения удобочитаемости кода. К началу главы 7 читатель уже будет обладать всеми необходимым основными навыками работы с языком С# и сможет сконцентрироваться на способах отладки своих приложений, что подразумевает рассмотрение того, как выводить трассировочную информацию по мере выполнения приложений, и того, как использовать VS для перехвата ошибок и принятия решений по их устранению с помощью мощной отладочной среды. В главе 8 начинается рассмотрение объектно-ориентированного программирования (ООП), где первым делом рассказывается о том, что подразумевает этот термин, и что такое объект. На первый взгляд ООП может казаться очень сложной технологией. Поэтому вся восьмая глава посвящена развенчанию этого мифа и объяснению преимуществ ООП, в связи с чем в ходе главы почти не приводится никакого кода на С#, только в самом конце. В главе 9 читателю предлагается применить теорию на практике и начать использовать приемы ООП в своих приложениях на С#. Именно в этом и кроется истинная мощь языка С#. Для начала будет показано, как определяются классы и интерфейсы, а затем, в главе 10 — как определяются члены классов (поля, свойства и методы). В конце этой главы начнется построение приложения карточной игры, которое будет разрабатываться далее на протяжении нескольких глав для иллюстрации прочих приемов ООП. Поняв, как приемы ООП работают в С#, читатель сможет перейти к главе 11 и рассмотреть типичные сценарии их применения, вроде манипулирования коллекциями объектов и сравнения и преобразования объектов. Глава 12 посвящена новому и очень полезному механизму С#, который появился еще в версии .NET 2.0, а имен-
Введение 25 но — обобщениям, которые позволяют создавать очень гибкие классы. В главе 13 обсуждение языка С# и ООП продолжается рассмотрением дополнительных приемов и примечательных событий, которые становятся очень важными при программировании, например, Windows-приложений. И, наконец, в главе 14 рассказывается о тех функциональных возможностях языка С#, которые появились в версии С# 3.0. Программирование для Windows (главы 15-18) Глава 15 начинается с объяснения того, что вообще подразумевается под программированием Windows-приложений и как его можно выполнять в VCE и VS. Как и раньше, сначала приводятся основы, на которых строится все дальнейшее обсуждение. Затем в главе 16 показывается, как в своих приложениях можно использовать различные поставляемые в .NET Framework элементы управления. Здесь читатель сможет быстро понять, как .NET позволяет собирать Windows-приложения графическим образом и компоновать сложные приложения с минимальными усилиями и временем. В главе 17 рассматриваются некоторые наиболее часто применяемые функциональные возможности, с помощью которых можно с легкостью добавлять специфические функции, вроде управления файлами, печати и т.д. В главе 18 рассказывается о развертывании приложений, а также о создании программ установки для предоставления пользователям возможности быстро инсталлировать приложения и приступать к работе с ними. Программирование для Web (главы 19-23) Эта часть построена по тому же принципу, что и часть по программированию для Windows. В главе 19 описаны элементы управления, необходимые для создания простейших Web-приложений, и рассказывается о том, как их использовать вместе и решать разнообразные задачи с помощью ASP.NET. В главе 20 продолжается рассмотрение этой темы, и описываются некоторые более сложные приемы, усовершенствованные элементы управления и способы управления состоянием в контексте Web, a также требования для соблюдения Web-стандартов. Глава 21 представляет собой своего рода экскурс в замечательный мир Web-служб, которые позволяют программно получать доступ к данным и возможностям через Internet. В частности, Web-службы позволяют предоставлять Web- и Windows-приложениям доступ к сложным данным и функциональным возможностям не зависящим от платформы образом. В этой главе рассказывается о том, как можно использовать и создавать Web-службы, а также о тех дополнительных средствах, которые для этого предоставляются в .NET, вроде средств безопасности. В главе 22 рассказывается о программировании с применением технологии Ajax, которая позволяет добавлять в Web-приложения динамические функциональные возможности для клиентов. В .NET Framework 3.5 возможности Ajax предоставляются в составе компонента ASP.NET AJAX, и в этой главе объясняется, как ими пользоваться. В главе 23 описаны способы развертывания Web-приложений и служб, в частности, новые средства VS и VWD, которые позволяют публиковать приложения на Web- сайтах с помощью нескольких щелчков кнопкой мыши. Доступ к данным (главы 24-29) В главе 24 показано, как можно делать так, чтобы приложения сохраняли и извлекали данные с диска, причем как в виде текстовых файлов, так и в виде более сложных представлений. Еще здесь можно узнать о сжатии данных, работе с унаследованными данными вроде файлов со значениями, разделенными запятой (CSV), а также
26 Введение о слежении за изменениями в файловой системе и выполнении на их основе тех или иных действий. В главе 25 читатель познакомится с языком, который очень быстро становится фактическим стандартом для обмена данными, а именно — языком XML. В предыдущих главах он уже несколько раз упоминался, но в этой главе приводится целый перечень основных правил его использования и демонстрируются его преимущества. В остальных главах этой части рассказывается о LINQ — языке запросов, который встроен во все последние версии .NET Framework. В частности, в главе 26 приводится общее описание LINQ, в главе 27 показано, как применять LINQ для получения доступа к данным, хранящимся в базе данных, в главе 28 — как использовать LINQ вместе с более старой технологией доступа к данным ADO.NET и, наконец, в главе 29 — о том, как использовать LINQ с XML-данными. Дополнительные технологии (главы 30-36) В последней части этой книги рассматриваются различные дополнительные вопросы, касающиеся С# и .NET. В частности, в главе 30 рассказывается об атрибутах, которые являются мощным способом как для включения дополнительной информации о типах в сборки, так и для добавления функциональных возможностей, которые в противном случае было бы трудно реализовать. В главе 31 речь идет о XML-документации и том, как документировать приложения на уровне исходного кода. Здесь читатель увидит, как добавлять такую информацию и как ее использовать и извлекать, а также научится генерировать расширенную документацию в стиле MSDN из своего кода. Затем в главе 32 читатель познакомится с вопросами сетевой связи и узнает, как приложения могут взаимодействовать друг с другом и с другими службами через различные типы сетей. В главе 33 рассматриваются способы программирования графических объектов с помощью GDI+. Здесь читатель научится манипулировать графическими объектами и стилизовать приложения и получит возможность создавать самые разнообразные приложения на С#. Напоследок читатель сможет познакомиться с некоторыми новыми и весьма привлекательными технологиями, которые появились вместе с последним выпуском .NET Framework. В частности, в главе 34 рассказывается о технологии WPF (Windows Presentation Foundation), которая обещает привести к внесению серьезных изменений в разработку как Windows-, так и Web-приложений, в главе 35 — о технологии WCF (Windows Communication Foundation), которая улучшает и расширяет технологию Web-служб до технологии коммуникаций на уровне предприятия, а в главе 36 — о технологии WF (Windows Workflow Foundation), которая позволяет реализовать в приложениях механизм рабочих потоков, т.е. определять операции, которые должны выполняться в определенном порядке в зависимости от внешних взаимодействий, что является очень полезным для многих типов приложений. Что необходимо для использования этой книги Код и описания С# и .NET Framework, приводимые в настоящей книге, подходят для версии .NET 3.5. Для понимания этих аспектов в книге не требуется ничего, кроме самого среды, но для проработки многих предлагаемых здесь примеров требуется еще и наличие средства разработки. В этой книге в качестве основного средства разработки используется Visual C# Express 2008, хотя в некоторых главах также приме-
Введение 27 няется Visual Web Developer Express 2008. Кроме того, некоторые функциональные возможности доступны только в полной версии Visual Studio 2008, что в тех местах, где это так, соответствующим образом отмечено. Соглашения Для предоставления возможности извлечения максимальной пользы из излагаемого материала и понимания, о чем идет речь, везде в этой книге используются определенные соглашения. ИИИИИ Практическое занятие Здесь приводятся упражнения, которые требуется проработать, следуя указанным в тексте книги инструкциям. 1. Они обычно состоят из ряда шагов. 2. Каждый шаг сопровождается номером. 3. Эти шаги нужно выполнять с помощью своей копии базы данных. Описание полученных результатов После каждого раздела "Практическое занятие" следует раздел, в котором приводится подробное описание введенного ранее кода. В таких врезках содержится важная информация, которую следует запомнить и которая имеет непосредственное отношение к материалу, внутри которого она находится. \у i Примечания, советы, подсказки, трюки и прочие сведения, предлагаемые в качестве дополнения к основному тексту, идут отдельно и выделяются курсивом, как показано здесь. Ниже описаны стили, используемые в тексте. □ Новые термины и важные слова при первом упоминании выделяются курсивом. □ Клавиатурные комбинации приводятся в виде <Ctrl+A>. □ Имена файлов, URL-адреса и строки кода внутри текста выделяются моноширинным шрифтом — persistence, properties. □ Код выделяется двумя разными способами: Просто моноширинным шрифтом, если этот код уже приводился ранее или был создан автоматически. Полужирным, если код является новым или был изменен. Исходный код Исходный код всех примеров, рассмотренных в книге, доступен на Web-сайте издательства по адресу http://www.williamspublishing.com.
28 Введение От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: info@dialektika.com WWW: http://www.dialektika.com Информация для писем из: России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Украины: 03150, Киев, а/я 152
ЧАСТЬ I Язык С# В ЭТОЙ ЧАСТИ... Глава 1. Начальные сведения о языке С# Глава 2. Написание программы на языке С# Глава 3. Переменные и выражения Глава 4. Управление потоком выполнения Глава 5. Дополнительные сведения о переменных Глава 6. Функции Глава 7. Отладка и обработка ошибок Глава 8. Введение в объектно-ориентированное программирование Глава 9. Определение классов Глава 10. Определение членов класса Глава 11. Коллекции, сравнения и преобразования Глава 12. Обобщения Глава 13. Дополнительные приемы объектно-ориентированного программирования Глава 14. Расширения в языке С# 3.0
Начальные сведения о языке С# Добро пожаловать в первую главу первого раздела этой книги. В этом разделе предоставляется весь базовый материал, который необходим для начала работы с языком С#. В настоящей главе дается краткий обзор языка С# и среды .NET Framework с описанием того, что собой представляют эти технологии, для чего они применяются и как они связаны друг с другом. Первым приводится общее описание .NET Framework. В этой технологии имеется много понятий, разобраться в которых сразу же не так-то просто. Из-за этого в настоящем разделе многие из новых понятий описываются очень кратко. Однако даже беглое рассмотрение базовых понятий является существенно важным для понимания того, как выполняется программирование на языке С#. Позже в данной книге многие из затрагиваемых здесь тем будут рассматриваться снова и уже более детально. После предоставления общих сведений о .NET Framework далее в главе приводится базовое описание самого языка С# с указанием источника его происхождения и схожих черт с языком C++. И, наконец, в завершение вкратце рассказывается о главных средствах, которые используются в настоящей книге — Visual Studio 2008 (VS) и Visual C# 2008 Express Edition (VCE). Что собой представляет .NET Framework .NET Framework представляет собой новую революционную платформу, которая была создана Microsoft для разработки приложений. Самым интересным в этом утверждении является его неоднозначность, для которой, однако, имеются вполне веские причины. Для начала обратите внимание на то, что в нем не говорится о том, что эта платформа была создана для разработки приложений исключительно на базе операционной системы Windows. Хотя выпущенная Microsoft версия .NET Framework рассчитана на использование под управлением Windows, очень быстро становятся доступны-
Глава 1. Начальные сведения о языке с# 31 ми альтернативные версии, способные работать и в средах с другими операционными системами. Одним из примеров является версия Mono, которая распространяется с открытым исходным кодом (вместе с компилятором С#) и способна работать на базе нескольких операционных систем, включая некоторые разновидности таких систем, как Linux и Мае. Многие подобные проекты сейчас еще находятся на стадии разработки и в принципе к моменту прочтения настоящей книги уже могут стать доступными. Помимо этого еще существует и версия Microsoft .NET Compact Framework, которая, по сути, представляет собой усеченный вариант полной версии .NET Framework и может применяться на устройствах типа персональных цифровых помощников (personal digital assistant — PDA) и даже в некоторых смартфонах. Одним из главных стимулов к применению платформы .NET Framework является возможность использовать ее в качестве средства интеграции разных операционных систем. Помимо этого, приведенное выше определение .NET Framework не содержит никаких указаний касательно типов приложений, которые могут создаваться с помощью этой платформы. Объясняется это тем, что никаких ограничений подобного рода попросту не существует: .NET Framework позволяет создавать приложения для Windows, Web-приложения, приложения типа Web-служб и практически все остальные типы приложений, которые только можно себе представить. Платформа .NET Framework была спроектирована так, чтобы ее можно было использовать из любого языка, включая язык С# (являющийся предметом настоящей книги), а также языки C++, Visual Basic, JScript и даже некоторые более старые языки, вроде COBOL. Чтобы подобное было возможно, также были выпущены и предназначенные специально для применения с .NET версии этих языков, которые продолжают выпускаться и по сей день. Все они не только имеют доступ к .NET Framework, но еще также могут и взаимодействовать друг с другом. Это позволяет разработчикам, которые пользуются языком С#, легко применять код, который был написан разработчиками, применяющими язык Visual Basic, и наоборот. Все это открывает неимоверное количество возможностей и делает .NET Framework очень привлекательной платформой для использования. Что входит в состав .NET Framework Платформа .NET Framework по большей части состоит из гигантской библиотеки кода, который можно использовать из клиентских языков (вроде С#) путем применения различных приемов объектно-ориентированного программирования (Object- Oriented Programming), или ООП. Эта библиотека поделена на модули, которые применяются в зависимости от того, какие результаты требуется получить. Например, в одном модуле содержатся компоновочные блоки для приложений Windows, в другом — для программирования сетевого обмена, в третьем — для разработки Web-приложений. Некоторые из модулей содержат более специфические подмодули, вроде модуля для разработки Web-приложений, который содержит подмодуль для написания Web-служб. По замыслу разные операционные системы должны поддерживать некоторые или все эти модули, в зависимости от их характеристик. Устройство PDA, например, будет включать поддержку для всех ключевых функциональных возможностей .NET, но вряд ли будет нуждаться в наличии более специфических модулей. В одном из разделов библиотеки .NET Framework содержатся определения ряда базовых типов. Типы отвечают за способ представления данных, и указание некоторых наиболее фундаментальных из них (например, типа 2-битное целое число со знаком") способствует функциональной совместимости между языками, использующими .NET Framework. Это носит название системы общих типов (Common Type System — CTS).
32 Часть I. Язык С# Помимо библиотеки в состав .NET Framework еще также входит и так называемая общеязыковая исполняющая среда .NET (Common Language Runtime — CLR), которая отвечает за обслуживание процесса выполнения всех тех приложений, которые разрабатываются с помощью библиотеки .NET. Написание приложений с помощью .NET Framework Под написанием приложения с помощью .NET Framework подразумевается просто написание кода с использованием одного из языков, поддерживающих .NET Framework, и библиотеки кода .NET. В настоящей книге для разработки приложений будут применяться такие средства, как VS и VCE. Первое представляет собой мощную интегрированную среду разработки, которая поддерживает код на языке С# (а также управляемый и неуправляемый код на языке C++, Visual Basic и некоторых других языках), a VCE — усеченную (и бесплатную) версию среды VS, которая поддерживает код только на языке С#. Преимуществом этих сред является простота, с которой в них средства .NET могут интегрироваться в создаваемый разработчиком код. Весь создаваемый код будет полностью писаться на С#, но в нем везде все равно будет использоваться .NET Framework, а также, где необходимо, другие дополнительные средства из VS и VCE. Чтобы код на языке С# мог выполняться, он обязательно должен преобразовываться в код на том языке, который понимает целевая операционная система. Такой код называется родным кодом (native code), а сам процесс преобразования— компиляцией. Компиляция выполняется механизмом, который называется компилятором, и в .NET Framework состоит из двух этапов. Язык MSIL и ЛТ-компилятор При компиляции кода, в котором используется библиотека .NET, он не преобразовывается сразу же в родной код конкретной операционной системы. Вместо этого он сначала преобразуется в код MSIL (Microsoft Intermediate Language — промежуточный язык Microsoft). Этот код не является специфическим ни для какой-либо операционной системы, ни для языка С#. Другие языки, например, Visual Basic .NET, на первом этапе тоже компилируются в код на этом языке. В случае разработки приложений на С# такой процесс компиляции выполняется VS или VCE. Очевидно, что далее для запуска приложения требуется выполнить еще кое-какую работу. За это отвечает так называемый JIT-компилятор (Just-in-Time compiler — оперативный компилятор), который компилирует MSIL в родной код, отвечающий требованиям конкретной операционной системы и архитектуры целевого компьютера. Только после этого этапа у операционной системы появляется возможность запустить приложение. Аббревиатура JIT в названии компилятора указывает на то, что он выполняет компиляцию MSIL-кода только при возникновении соответствующей необходимости. Раньше код часто компилировался в несколько приложений, каждое из которых предназначалось для конкретной операционной системы и архитектуры ЦП. Такой подход обычно применялся в качестве одного из средств оптимизации (например, для принуждения кода работать быстрее на базе микросхем AMD), но в некоторых случаях играл даже критическую роль (например, при необходимости сделать так, чтобы приложения могли работать как в средах Win9x, так и в средах WinNT/2000). Сейчас в нем нет никакой необходимости, поскольку JIT-компиляторы используют MSIL-код, который не зависит ни от типа компьютера, ни от типа операционной системы, ни от типа ЦП. Существуют несколько JIT-компиляторов, каждый из которых рассчитан на
Глава 1. Начальные сведения о языке С# 33 конкретную архитектуру, и при создании требуемого родного кода применяться будет только тот из них, который подходит в данном случае. Вся прелесть этого механизма состоит в том, что он требует приложения гораздо меньшего количества усилий со стороны разработчика: на самом деле он даже позволяет ему вообще не думать о касающихся специфики системы деталях и концентрировать все внимание на более интересных функциональных возможностях кода. Сборки При компиляции приложении создаваемый MSIL-код сохраняется в сборке (assembly). С состав сборок входят как исполняемые файлы приложений, которые имеют расширение . ехе и могут запускаться прямо в среде Windows безо всяких других программ, так и файлы библиотек, которые имеют расширение . dll и предназначены для использования другими приложениями. Помимо MSIL-кода в сборки еще также включается метаипформация (т.е. информация о содержащихся в сборке данных, также называемая метаданными) и файлы необязательных ресурсов (файлы дополнительных данных, которые могут применяться в MSIL коде, вроде звуковых файлов и файлов изображений). Метаинформация делает сборки полностью самоописательными. Благодаря ей, для использования сборок больше никакой информации не требуется, а это означает исключение вероятности возникновений ситуаций, вроде невозможности добавления требуемых данных в системный реестр и тому подобного, каковые часто представляли проблему при выполнении разработки с помощью других платформ. Из-за этого развертывание приложений часто сводится просто к копированию файлов в каталог на удаленном компьютере. Поскольку никакой дополнительной информации на целевых системах не требуется, далее пользователь может просто запускать исполняемый файл из этого каталога и (при условии, что в системе установлена CLR-среда .NET) приступить к работе с приложением. Конечно, сохранять все необходимое для запуска приложения в одном месте вовсе необязательно. Разработчик может писать и какой-нибудь код, выполняющий задачи, требуемые нескольким приложениям. В подобных ситуациях зачастую удобнее помещать допускающий многократное использование код в то место, к которому смогут получать доступ все приложения. В .NET Framework таким местом является глобальный кэш сборок (Global Assembly Cache — GAC). Помещение в него кода осуществляется очень просто, а именно — копированием содержащей нужный код сборки в каталог, где расположен этот кэш. Управляемый код Роль CLR-среды не заканчивается после компиляции кода в MSIL-код и преобразования его JIT-компилятором в родной код. Код, который пишется с помощью .NET Framework, при выполнении (т.е. на этапе, который обычно называется временем выполнения) находится под управлением CLR-среды. Это означает, что CLR-среда следит за приложениями, осуществляя необходимое управления памятью, обеспечивая безопасность, позволяя выполнять межъязыковую отладку и т.д. Приложения, которые во время выполнения не попадают под контроль CLR-среды, называются неуправляемыми (unmanaged), и некоторые языки могут применяться для написания таких приложений, например, с целью доступа к низкоуровневым функциям операционной системы. В языке С#, однако, разрешается писать только код, выполняющийся в управляемой среде, т.е. использовать управляемые средства CLR и позволять среде .NET самостоятельно обрабатываться любые операции взаимодействия с операционной системой.
34 Часть I. Язык С# Сборка мусора Одной из наиболее важных функциональных возможностей управляемого кода является средство сборки мусора (garbage collection). Оно гарантирует в .NET уверенность в полном очищении использованной приложением памяти после завершения его эксплуатации. До появления .NET об этом по большей части нужно было заботиться самим программистам, и даже несколько простых ошибок в коде могли приводить к таинственному исчезновению крупных блоков памяти в результате их выделения в неправильном месте. Это обычно выливалось в постепенное замедление работы компьютера и в конечном итоге завершалось полным крахом системы. Механизм сборки мусора в .NET функционирует следующим образом: он инспектирует память компьютера время от времени и удаляет из нее все, что уже больше не нужно. Никаких установленных временных рамок для выполнения этой операции нет; она может происходить как тысячу раз в секунду, так и каждые несколько секунд или через любой другой промежуток времени, но в одном можно быть уверенным точно — она обязательно произойдет. Для программистов это означает появление кое-каких последствий. Из-за того, что данная операция выполняется автоматически через непредсказуемые промежутки времени, необходимо разрабатывать приложения с учетом этого факта. Код, требующий большого количества памяти для выполнения, должен осуществлять очистку после себя самостоятельно, а не ожидать того, когда ее выполнит механизм сборки мусора, но обеспечивается подобное поведение гораздо легче, чем звучит. Что получается в итоге Прежде чем продолжить, давайте суммируем все те рассмотренные выше шаги, которые требуются для создания .NET-приложения. 1. Сначала пишется код приложения с использованием совместимого с .NET языка, вроде С# (рис. 1.1). КодС# Рис. 1.1. Написание кода приложения с использованием языка С# 2. Далее этот код компилируется в MSIL-код, который сохраняется в сборке (рис. 1.2) Рис. 1.2. Компиляция кода приложения в MSIL-код и сохранение его в сборке 3. При первом запуске этого кода (в результате либо запуска его родного исполняемого файла, либо его запрашивания из другого кода) он первым делом компилируются в родной код с помощью JIT-компилятора (рис. 1.3).
Глава 1. Начальные сведения о языке С# 35 Сборка Г\ [J [J |ЛТ-компиляция^> Родной код V ) Рис. 1.3. Компиляция MSIL-код в родной код с помощью JIT-компилятора 4. После этого родной код выполняется в контексте управляемой CLR вместе со всеми остальными запущенными приложениями или процессами, как показано на рис. 1.4. Исполняющая среда системы CLR-среда.МЕТ Родной код i Родной код ' i _ _ j Родной код ' _ _ J L — "* Рис. 1.4. Выполнение родного кода в контексте CLR Связывание Обратите внимание на один дополнительный аспект в описанном процессе. Код С#, который на втором шаге компилируется в MSIL-код, вовсе не обязательно должен содержаться в одном файле. Код приложения может разбиваться на несколько файлов исходного кода, которые затем компилируются вместе в одну сборку. Этот процесс является чрезвычайно полезным и называется связыванием (linking). Дело в том, что с несколькими небольшими файлами гораздо проще работать, чем с одним огромным. Разработчик может выделять логически связанный код в отдельный файл, так чтобы поработать над ним отдельно, а затем по завершении практически забыть об этом. Такой подход еще и упрощает поиск конкретных фрагментов кода, когда в них возникает необходимость, и позволяет командам разработчиков делить связанную с программированием работу на отдельные приемлемые куски, т.е. "распределять" подлежащие обработке фрагменты кода без подвергания риску готовых разделов кода или разделов, над которыми работают другие. Что собой представляет язык С# Язык С#, как уже упоминалось ранее, является одним из тех, которые можно использовать для создания приложений, подлежащих запуску в .NET CLR. Он эволюционировал из языков С и C++ и был создан Microsoft специально для работы с платформой .NET. Благодаря тому, что язык С# разрабатывался недавно, в его состав вошли многие из наилучших функциональных возможностей других языков, но без присущих им проблем.
36 Часть I. Язык С# Разрабатывать приложения с помощью языка С# легче, чем с помощью C++, потому что синтаксис этого языка является более простым. Однако при этом С# все равно остается мощным языком, и существует очень мало вещей, которые может потребоваться делать на C++ из-за того, что их нельзя сделать на С#. Несмотря на это, те функциональные возможности языка С#, которые предлагаются параллельно более усовершенствованным функциональным возможностям языка C++, вроде прямого получения доступа к системной памяти и манипулирования ею, могут реализоваться только применением кода с пометкой unsafe. Этот усовершенствованный прием программирования является потенциально опасным (о чем и говорит ключевое слово unsafe), поскольку существует вероятность перезаписывания критических для системы блоков памяти. По этой и некоторым другим причинам он в настоящей книге не рассматривается. Иногда код на С# является немного более многословным, чем на C++. Объясняется это тем, что С# (в отличие от C++) представляет собой безопасный к шипам (type- safe) язык. Если выразиться просто, это означает, что после присваивания типу каких-то данных он уже далее не может преобразовываться в другой несвязанный тип. Следовательно, к преобразованию между типами должны применяться строгие правила, из-за чего для выполнения той же самой задачи на С# часто требуется писать больше кода, чем на C++, но, тем не менее, это дает определенные преимущества: код получается более надежным, процесс отладки — более простым, a .NET может всегда в любое время определить тип того или иного фрагмента данных. Таким образом, в С# невозможно делать, например, следующее: взять область памяти размером 4 байта под данные длинной 10 байтов и интерпретировать это как X; однако это вовсе не является недостатком. Язык С# представляет собой лишь один из многих языков, доступных для разработки .NET-приложений, но, несомненно, является наилучшим из них. Его главное преимущество состоит в том, что он единственный с самого начала разрабатывался специально для .NET Framework и из-за этого может быть главным кандидатом на использование в тех версиях .NET, которые переносятся на другие операционные системы. Чтобы языки вроде .NET-версии Visual Basic оставались насколько возможно похожими на своих предшественников и при этом совместимыми с CLR, некоторые средства из библиотеки кода .NET в них поддерживаются не полностью. В отличие от них, С# позволяет пользоваться каждым из тех средств, которые предлагаются в этой библиотеке. В состав последней версии .NET было добавлено несколько улучшений для языка С#, частично по просьбам конечных разработчиков, что существенно увеличивает его мощность. Приложения, которые можно писать на С# В .NET Framework не существует никаких ограничений касательно того, приложения какого типа можно создавать, как уже рассказывалось ранее. С# использует .NET Framework и потому тоже не имеет никаких ограничений касательно возможных типов приложений. Однако ниже перечислены некоторые наиболее распространенные из них. □ Приложения Windows. К приложениям этого типа относятся приложения, которые имеют знакомый пользователям Windows внешний вид и поведение, как, например, приложение Microsoft Office. Достигается такой внешний вид и поведение применением модуля .NET Framework под названием Windows Forms (Формы Windows), по сути, представляющего собой библиотеку элементов управления (вроде кнопок, панелей инструментов, меню и т.п.), с помощью которых можно создавать пользовательский интерфейс Windows (UI).
Глава 1. Начальные сведения о языке С# 37 □ Web-приложения. К приложениям этого типа относятся Web-страницы, вроде тех, что могут просматриваться посредством любого Web-браузера. В состав .NET Framework входит мощная система для генерации Web-содержимого динамическим образом, обеспечивающая персонализацию, безопасность и многое другое. Называется она ASP.NET (Active Server Pages .NET), и благодаря ей, язык С# можно использовать для создания приложений ASP.NET с применением Web-форм. □ Web-службы. Этот тип является новым и интересным способом для создания разнообразных распределенных приложений. С помощью Web-служб через Internet можно обмениваться практически любыми данными, используя тот же самый простой синтаксис, независимо от того, какой язык применялся для создания Web-службы, и того, на какой системе она расположена. Приложениям любого из этих типов может также требоваться доступ к базам данным, обеспечиваться который может либо с помощью такого средства .NET Framework, как ADO.NET (Active Data Object .NET), либо с помощью такого средства самого языка С#, как LINQ (Language Integrated Query— язык интегрированных запросов). Помимо этого еще могут быть задействованы и многие другие ресурсы, а именно — инструменты для создания сетевых компонентов, вывода графических объектов, выполнения сложных математических операций и т.д. Язык С# в этой книге В первой части этой книги описывается только базовый синтаксис и основные способы применения языка С#, a .NET Framework никакого особого внимания не уделяется. Такой подход был выбран потому, что работать с .NET Framework, не обладая базовым навыками программирования на С#, просто невозможно. Чтобы еще больше упростить изучение материала, рассмотрение такой более сложной темы, как объектно-ориентированное программирование (ООП), тоже было отложено на более позднее время. Таким образом, в этой книге изучение языка С# начинается с ознакомления с основополагающих принципов, и наличие каких-либо знаний в области программирования не требуется. После проработки этого материала читатель будет готов перейти к разработке приложений тех типов, которые демонстрируются в других частях. Во второй части настоящей книги демонстрируются приемы программирования приложений Windows Forms, в третьей — приемы программирования Web-приложений и Web-служб, в четвертой — способы доступа к данным (хранящимся в базе данных, файловой системе или XML), а в пятой— некоторые другие интересные темы в .NET, вроде дополнительных приемов программирования сборок и графики. Visual Studio 2008 Настоящая книга предполагает применение инструментальных средств разработки Visual Studio 2008 (VS) или Visual C# 2008 Express Edition (VCE) для программирования на С#, начиная от простых приложений с интерфейсом командной строки и заканчивая проектами более сложных типов. Инструменты разработки, а точнее — интегрированные среды разработки (Integrated Development Environment — IDE) вроде VS — не являются крайне необходимыми для создания приложений на С#, но они значительно упрощают этот процесс. При желании, конечно, можно манипулировать файлами исходного кода на С# в базовом текстовом редакторе, вроде повсеместно известного редактора Notepad, и компилировать код в сборки с помощью компилятора
38 Часть I. Язык С# командной строки, который является частью .NET Framework. Однако зачем это делать, если есть мощные средства IDE-среды, которые могут помочь? Ниже приведен краткий перечень ряда функциональных возможностей Visual Studio, которые могут сделать эту среду привлекательным выбором для разработки .NET-приложений. □ VS автоматизирует шаги, требуемые для компиляции исходного кода, и в то же время предоставляет полный контроль на любыми используемыми опциями на случай, если у разработчика вдруг возникнет желание их переопределить. □ Текстовый редактор VS подстроен под те языки, которые VS поддерживает (включая С#), так чтобы он мог интеллектуальным образом обнаруживать ошибки и предлагать код, где это необходимо, по мере его ввода. Это средство в VS называется IntelliSense. □ В состав VS входят конструкторы для приложений типа Windows Forms и Web Forms, позволяющие добавлять в них элементы пользовательского интерфейса простым перетаскиванием. □ Многие типы проектов на С# могут создаваться с помощью готового "стереотипного" кода. Вместо того чтобы начинать с нуля, разработчик будет часто обнаруживать, что у него в распоряжении имеются различные файлы кода, сокращающие количество времени, требуемого для работы с проектом. Это особенно верно в случае проекта нового типа Starter Kit (Начальный набор), который позволяет приступать к разработке на базе полностью функционального приложения. Некоторые проекты такого типа доступны непосредственно в составе VS, но еще больше из них можно найти в Internet. □ VS включает несколько мастеров, которые автоматизируют выполнение наиболее распространенных задач, многие из которых могут приводить к автоматическому добавлению соответствующего кода в существующие файлы и тем самым позволять разработчику не беспокоится (а в некоторых случаях даже и вообще забывать) о правильности синтаксиса. □ В VS содержится много мощных средств для визуализации и навигации по элементам проекта, где бы они не находились — хоть в файлах исходного кода на С#, хоть в файлах других ресурсов, вроде файлов растровых изображений или звуковых файлов. □ В VS можно как просто писать приложения, так и создавать целые проекты и тем самым упрощать процесс предоставления кода клиентам и позволять им устанавливать его без особых усилий. □ VS позволяет использовать усовершенствованные приемы отладки при разработке проектов, например, просматривать код по одной команде за раз и при этом следить за состоянием приложения. Подобных возможностей в VS, конечно, еще очень много, но этот перечень дает возможность получить хотя бы общее представление. Продукты Visual Studio 2008 Express Помимо Visual Studio 2008 компания Microsoft поставляет несколько более простых средств разработки, которые в общем называются набором продуктов Visual Studio 2008 Express и доступны бесплатно по адресу http://lab.msdn.microsoft.com/express. Два средства из этого набора продуктов — Visual C# 2008 Express Edition и Visual Web Developer 2008 Express Edition — вместе позволяют создавать практически любое
Глава 1. Начальные сведения о языке С# 39 приложение на С#, которое может потребоваться. Оба они представляют собой усеченные версии VS и обладают точно таким же внешним видом и поведением. Хотя в них и предлагаются многие из тех же функциональных возможностей, что и в VS, некоторых заметных средств в них все-таки нет, но таковых не настолько много, чтобы это не позволило применять их для проработки материала настоящей книги. Поэтому в этой книге везде, где можно, для разработки С#-приложений будет использоваться VCE и только при необходимости в конкретной функциональности — полная версия VS. Решения В случае использования VS или VCE для разработки приложений требуется создавать так называемые решения (solutions). Решения в VS и VCE представляют собой нечто большее, чем просто приложения. Эти решения содержат проекты (projects), каковыми могут быть проекты Windows Forms, Web Forms и т.д. Из-за того, что решения могут содержать несколько проектов, связанный между собой код можно группировать в одном месте, даже если впоследствии он будет компилироваться в несколько сборок и сохраняться в разных местах на жестком диске. Это очень удобно, поскольку позволяет работать над разделяемым (shared) кодом (который может помещаться в GAC) в то же самое время, что и над приложениями, в которых он используется. Отладку кода гораздо легче выполнять, когда применяется только одна среда разработки, поскольку тогда можно проходить по командам во множестве модулей кода. Резюме В этой главе было в общих чертах рассказано о среде .NET Framework и том, каким образом она упрощает процесс создания мощных и многофункциональных приложений. Здесь было показано, что необходимо для превращения кода на языках вроде С# в рабочие приложения, и какие преимущества дает использование управляемого кода, запускаемого в среде NET Common Language Runtime. Кроме того, в главе шла речь о том, что собой представляет язык С#, и какое отношение он имеет к .NET Framework, а также о тех средствах, которые можно применять для разработки приложений на С#, а именно — Visual Studio 2008 и Visual C# 2008 Express Edition. Далее перечислены ключевые моменты, с которыми вы ознакомились в этой главе. □ Что собой представляет продукт .NET Framework, зачем он был создан и что делается его такой привлекательной средой для программирования. □ Что собой представляет язык С# и что делает его идеальным инструментом для программирования в .NET Framework. □ Какие средства необходимы для эффективной разработки приложений .NET (каковыми являются среды разработки вроде VS и VCE). В следующей главе вы напишете некоторый код на С#, что позволит приобрести достаточные навыки для того, чтобы сконцентрировать внимание на самом языке, и не беспокоится о том, каким образом работает IDE-среда.
Написание программы на языке С# Теперь, когда уже в общем известно, что собой представляет язык С# и каким образом он вписывается в .NET Framework, наступила пора приложить настоящие усилия и попробовать написать какой-нибудь код. В этой книге везде будет использоваться либо Visual Studio 2008 (VS), либо Visual C# 2008 Express Edition (VCE), поэтому первое, что нужно сделать — это получить базовые сведения об этих средах разработки. VS представляет собой огромный и сложный продукт и может вызывать страх у пользователей, которые имеют с ним дело впервые, но его применение для создания базовых приложений характеризуется удивительной простотой. Начав использовать VS в настоящей главе, вы увидите, что знать очень много для того, чтобы начать работать с кодом С#, вовсе не обязательно. Позже в настоящей главе будут также продемонстрированы и более сложные операции, которые может выполнять VS, но пока что требуются лишь базовые практические знания. VCE является гораздо более простым продуктом для начала работы и потому на первых этапах в настоящей книге все примеры будут описываться в контексте именно этой IDE-среды. Однако при желании вместо нее можно использовать VS, и все примеры будут работать более-менее тем же самым образом. По этой причине в настоящей главе дается обзор обеих этих IDE-сред, начиная с VS. После обзора IDE-сред предлагаются примеры создания двух простых приложений. Волноваться особо об используемом в них коде пока нет нужды; главное здесь — убедиться, что все работает. Благодаря проработке процедур создания приложений в этих первых примерах, они очень быстро станут привычными. Первым демонстрируется пример создания простого консольного приложения (console application). Консольными называются такие приложения, которые не предусматривают применение графических окон, благодаря чему в них не нужно заботиться ни о кнопках, ни о меню, ни о взаимодействии с курсором мыши и т.п. Вместо этого они запускаются в окне командной строки и взаимодействуют с ним гораздо более простым образом.
Глава 2. Написание программы на языке С# 41 Вторым демонстрируется пример создания приложения Windows Forms. Внешний вид и поведение таких приложений очень знакомы пользователям Windows, и (что удивительно) их создание не требует приложения гораздо большего количества усилий. Однако синтаксис необходимого для них кода выглядит более сложным, хотя во многих случаях разработчику на самом деле не нужно заботиться о деталях. В следующих двух частях книги в примерах будут приводиться приложения обоих типов, хотя поначалу немного больший акцент все-таки будет делаться на консольных приложениях. В той дополнительной гибкости, которую обеспечивают приложения Windows, при изучении языка С# нет никакой необходимости, в то время как простота консольных приложений позволяет сконцентрироваться на изучении синтаксиса и не заботиться о внешнем виде и поведении приложения. В этой главе рассматриваются следующие вопросы. □ Базовые практические сведения о Visual Studio 2008 и Visual C# 2008 Express Edition. □ Процесс написания простого консольного приложения. □ Процесс написания приложения Windows Forms. Среды разработки В настоящем разделе приведено описание сред разработки VS и VCE, причем первой рассматривается VS. Эти среды похожи между собой, поэтому прочитать нужно оба раздела, независимо от того, какая из них используется. Visual Studio 2008 При первой загрузке VS сразу же отображает целый ряд окон, большинство из которых является пустыми, а также набор элементов меню и пиктограмм панели инструментов. По ходу прочтения книги вам доведется использовать большинство из них, поэтому уже совсем скоро они станут выглядеть вполне знакомо. При первом запуске VS на экране появится перечень предпочтений, предназначенный для тех пользователей, у которых имеется опыт работы с предыдущими версиями данной среды разработки. Выбираемые здесь настройки влияют на целый ряд вещей, например, на компоновку окон, на способ запуска окон консоли и т.д. Поэтому выберите в этом перечне опцию Visual C# Development Settings (Параметры разработки Visual C#), иначе позже может оказаться, что все работает немного не так, как описывается в настоящей книге. Обратите внимание, что доступные здесь опции будут варьироваться зависимости от того, какие опции выбирались при установке VS, но если во время инсталляции была выбрана опция установки С#, эта опция будет доступна обязательно. Если же это не первый запуск VS, и в первый раз была выбрана другая опция, паниковать не стоит. Чтобы сбросить настройки и выбрать опцию Visual C# Development Settings, необходимо просто импортировать их. Чтобы сделать это, выберите в меню Tools (Сервис) пункт Import и Export Settings (Параметры импорта и экспорта) и затем выберите переключатель Reset All Settings (Сбросить все настройки), как показано на рис. 2.1. Затем щелкните на кнопке Next (Далее) и укажите, хотите ли вы сохранить существующие параметры, прежде чем продолжить. Если вы уже выполняли какие-нибудь настройки, то наверняка захотите сделать это; в противном случае щелкните на кнопке No (Нет), а затем снова на кнопке Next. В следующем диалоговом окне выберите опцию Visual C# Development Settings, как показано на рис. 2.2. Опять-таки, доступные опции могут варьироваться. Напоследок щелкните на кнопке Finish (Готово), чтобы применить изменения.
42 Часть I. Язык С# Import and Export Settings Wizard 3* Wekoete lo the Import and Export Settings WU«d You can use this wizard to Import or export specific categories of settings, or to reset the environment to one of the default collections of settings. What do you want to do? 0 Export selected environ Kent settings Settings will be saved out to a file so they can later be Imported at anytime on any machine. ; Iaaport selected environ aaent settings Import settings from a file to apply them to the environment Reset all environment settings to one of the default collections of settings. Finish j UpjpjMaj|>. in ... . ———— Рис. 2.1. Выбор переключателя Reset All Settings i 1 _—— Import and Export Settings Wizard Ц£Ишв ■ i. ~^j) Choose a Default Collection of Settings Which collection of settings do you want to reset to? t}, General Development Settings i£ Visual Basic Development Settings ,> visual C* ♦ Development Settings V> VVeb Development Settings 1 Description 1 Customizes the environment to ■ maximize code editor screen space 1 and improve the visibility of 1 commands specific to C#. Increases 1 productivity with keyboard shortcuts I that are designed to be easy to learn r and use. 1 1 L-iH-euij t.rt ^Ш^\шШйЫ1 ■ Рис. 2.2. Выбор опции Visual C# Development Settings Схема расположения компонентов в среде VS является полностью настраиваемой, но здесь прекрасно подойдет и та, что предлагается по умолчанию. В случае выбора опции Visual C# Development Settings она будет выглядеть так, как показано на рис. 2.3.
Глава 2. Написание программы на языке С# 43 г Окно Toolbox Главное окно со страницей Start Page Окно Меню и панель Solution Explorer инструментов ■ HOwOol.. t Downl >»d *ddlt>on»l COIII* MSON Fcr.mj Viju4> '• DtMtoPV I 'n't' Irtfrd ЙИЯ1 Jtud.» M\ON Мфм neset»: <X VnuH Studtn UN t !M Now U Nov 2007 14 55 54 GMT ■ Get It here firstt w» Vbuti Studio 2000, you an dtvtlop conntcted. compelling «ppllctttoni for Windows virtt. fht 2007 Office system, mobile dtvlcti. *nd the Web. Mon. I» Nov 2007 14:5$ 52 «NTT - The V1su»l C» 2001 txpren Edition provld«> devei open with powerful too11 end tnguegt support to I (ppllcttJoni on the NET Fnsmewort rwiMk U Hunllar Now А*«*еЫс Mon. 19 Nov 2007 BOM GMT. NET Frtmework J 5 builds incrtmtnttlry on the new features tddtd in NET Fnmtwork ) 0 tnd is now «vtiltble 4i • stptrtte di Tut. 21 Aug 2007 17 44 4» GMT . Tnt tutnontttivt C# 3 0 Sptdflcttlon wts written by the ptoplt who crttttd too unpltmtnttd Wit C* itngutgt This 500 plus ptgt documtnt is now •vtntbit for download и»» ll< Tout I f*db«k mi Vuual Sludto 1ХнчттШкт tn. U Oct 20071*25 54 GMT. Htlp us htlp you by taking 10 minutes to Mi out our VTsurt Studio Content Survey on how to improve the viiuti Studio documtntkbon We tpprtottt it) NrallMvkon I tin «Hi St«««k>i A*«*e»t lot Vliu*l MiKko /fM Thu. 00 Aug 2007IMMI GMT. Use these resources to get fkimiiei with the Uttit version of your ftvonte C* compiler in Visual Studio — — Рис. 2.3. Предлагаемая по умолчанию схема компоновки в VS Главное окно, в котором по умолчанию при запуске VS содержится еще и полезная страница Start Page (Начало работы), является тем местом, где отображается весь код. Это окно может содержать много документов, каждый на отдельной вкладке, и тем самым позволяет легко переключаться между несколькими файлами, щелкая на их имени. У него также имеются и другие функции: оно может отображать GUI-интерфейсы, которые разрабатываются для проектов, простые текстовые файлы, HTML-код и различные встроенные в VS инструменты. Все это вы сможете увидеть по мере прочтения книги. Над главным окном находятся панели инструментов и меню VS. Здесь могут размещаться несколько панелей инструментов с возможностями от сохранения и загрузки файлов до компоновки и запуска проектов, и даже отладки элементов управления. Опять-таки, все они будут демонстрироваться далее в книге. Ниже приведено краткое описание тех основных средств, которыми придется пользоваться чаще всего. □ Окно Toolbox (Панель инструментов). Появляется при наведении курсора мыши на вкладку с соответствующим названием. Предоставляет доступ к компоновочным блокам, необходимым для создания пользовательского интерфейса для приложений Windows, и не только. Рядом с этой вкладкой еще также может отображаться и вкладка Server Explorer (Проводник сервера), доступная путем выбора в меню View (Вид) пункта Server Explorer (Проводник сервера) и включающая различные дополнительные возможности, вроде возможности получения доступа к источникам данных, настройками сервера, службам и т.д. □ Окно Solution Explorer (Проводник решений). В этом окне отображается информация о загруженном в текущий момент решении (solution). В терминологии VS решением называется один или более проектов вместе с их конфигурациями. Окно Solution Explorer позволяет отображать входящие в состав решения проекты в разных представлениях, которые демонстрируют, например, то, какие файлы в них содержатся и что содержится в этих файлах.
44 Часть I. Язык С# □ Окно Properties (Свойства). Располагается непосредственно под окном Solution Explorer. На рис. 2.3 оно не было показано потому, что появляется только во время работы над проектом (или после выбора в меню View (Вид) пункта Properties Window (Окно свойств)). Это окно дает более детализированное представление содержимого проекта и позволяет осуществлять дополнительную настройку его отдельных элементов. Например, его можно использовать для изменения внешнего вида кнопки в форме Windows. □ Окно Error List (Список ошибок). Это окно тоже не было показано на приведенном выше снимке экрана, но является чрезвычайно важным. Открывать его можно выбором в меню View (Вид) пункта Error List (Список ошибок). В нем отображаются ошибки, предупреждения и другая касающаяся проекта информация. Содержимое этого окна постоянно обновляется, хотя некоторая информация появляется в нем только при компиляции проекта. Может показаться, что этих основных средств чересчур много, но на привыкание к ним не потребуется много времени. Уже при создании первого предлагаемого здесь для примера проекта вам придется воспользоваться многими из описанных только что элементов. Среда VS способна отображать и многие другие окна, как информационные, так и функциональные. Многие из них могут разделять жранное пространство с теми окнами, что были перечислены выше, и переключаться между ними можно с помощью вкладок. Немало из этих кон будет демонстрироваться позже в этой книге, но еще больше можно будет обнаружить самостоятельно при более детальном изучении среды VS. Visual C# 2008 Express Edition В случае VCE волноваться об изменении параметров не нужно. Очевидно, что данный продукт не будет применяться для программирования на Visual Basic, поэтому никакой эквивалентной опции, о которой нужно беспокоиться, здесь нет. При первом запуске VCE появляется экран, очень похожий на то, что предлагается в VS (рис. 2.4). Окно Toolbox Главное окно со страницей Start Page Окно Solution Explorer Меню и панель Рис. 2.4. Экран, появляющийся при первом запуске VCE
Глава 2. Написание программы на языке С# 45 Консольные приложения В настоящей книге консольными приложениями нужно будет пользоваться регулярно, особенно поначалу, поэтому в следующем практическом занятии приводится пошаговое описание процесса создания такого простого приложения. В нем содержатся шаги для использования как в среде VS, так и в среде VCE. Практическое занятие Создание простого консольного приложения 1. Создайте новый проект консольного приложения, выбрав в меню File (Файл) пункт New1^ Project (Создать^Проект), если используется VS, или пункта New Project (Новый проект), если применяется VCE, как показано на рис. 2.5. ityf; Edit view Toon Test . Ctrl» Shirt* N Cto»e Solution Smcam cm» sum» s Recent Files I window Help J] Project.. «J web Sit».. ^Sh«»Alt»N J File... CW*N Project From basting Cod*.. \0Ш 55 *•**^^v*n<to» .J New Project., к Ctrl* Shift» N ^3 Open Project.. Ctrl» Shift» 0 £'■ Open Flit... Ctrl* О 2 Cloie Solution Sr* Selected tttn, Cbi»S Swe S»leeredPeir,(*< Swe All Ctrt» Shift, s tvport Templtte ... f.ae >etup Print CW»P Puc. 2.5. Создание нового проекта в VSu VCE 2. В VS выберите узел Visual C# в панели Project Types (Типы проектов), которое появится далее, и проект типа Console Application (Консольное приложение) в панели Templates (Шаблоны), как показано на рис. 2.6. В VCE просто выберите Console Application (Консольное приложение) в панели Templates (Шаблоны), как показано на рис. 2.7. В VS измените путь в текстовом поле Location (Размещение) на С: \BegVCSharp\Chapter02 (этот каталог будет создан автоматически, если его не существует). Далее, как в VS, так и в VCE, оставьте предлагаемый по умолчанию текст в текстовом поле Name (Имя) (ConsoleApplicationl), а также все остальные параметры без изменений (см. рис. 2.6) ш0 VruMl Stud#c i»n»*n*d tempt*** J») ASP Nf T web Application : IWW Apportion ДкопюМ АррИсаОол h'lxttlJNJWort /Оивоо» М0ГАДО.1П UBWCF StnKt Api : f/word mi cx>cu»»tnt |имм гогян ■и : .Jstwcn or*n»T««pi«t»i ConiokApplKatoool С ЛРлК ShwpKheptenIJ ContoteAppfcobonl tVOWH- Puc. 2.6. Создание стандартного консольного приложения в VS
46 Часть I. Язык С# (И я э э зя п 31 "з J Si-r ContOltApplKltlOnl i <* j с Puc. 2.7. Создание стандартного консольного приложения в VCE 3. Щелкните на кнопке ОК. 4. Если вы используете VCE, после инициализации проекта щелкните на кнопке Save All (Сохранить все) в панели инструментов или выберите пункт Save All (Сохранить все) в меню File (Файл), после чего укажите в поле Location (Размещение) каталог С: \BegVCSharp\Chapter02 и щелкните на кнопке ОК. 5. По завершении инициализации проекта добавьте в файл, отображающийся в основном окне, следующие строки кода: namespace ConsoleApplicationl { class Program { static void Main(string[] args) { // Вывод текста на экран. Console.WriteLine("The first app in Beginning C# Programming!11) ; // Первое приложение в книге Console.ReadKey(); } } } 6. Выберите в меню Debug (Отладка) пункт Start Debugging (Начать отладку). Через некоторое время после этого на экране должно появиться окно, показанное на рис. 2.8. ■ m#:///C:/B«gVCSh*rp/Ch«pttrt2/Conio«*A^ Рис. 2.8. Окно, появившееся после выбора пункта меню Start Debugging 7. Нажмите любую клавишу, чтобы выйти из приложения (может потребоваться перед этим сначала щелкнуть на окне консоли, чтобы навести на него фокус).
Глава 2. Написание программы на языке С# 47 Показанное выше окно появится только в том случае, если была выбрана опция Visual C# Developer Settings, как описывалось ранее в главе. Например, если была выбрана не эта опция, a Visual Basic Developer Settings, тогда отобразится пустое окно консоли, и вывод приложения появится в окне с заголовком Immediate (Непосредственный вывод). Еще в таком случае код Console.ReadKey () завершится неудачей и появится ошибка. При наличии такой проблемы устраните ее на период работы с примерами, предлагаемыми в настоящей книге, применив опцию Visual C# Developer Settings, после чего результаты, которые вы получите, должны совпадать с теми, что показаны здесь. Если проблема повторится, тогда выберите в меню Tools (Сервис) пункт Options (Параметры) и затем в окне Options (Параметры), которое появится после этого, снимите отметку с флажка Redirect all Output Window text to the Immediate Window (Перенаправлять весь текст из окна вывода в окно Immediate) в разделе Debugging (Отладка), как показано на рис. 2.9. -Т55Г Projecti and Solutions Sourct Control Text Editor Database Too» Debugging Edit ind Continue Just-In-Time ИМЯ Symbols Device Tools МШ1 Designer Office Tools Test Tools Text TempletJng Windows Forms Designer General : *": Enable Just My Code (Managed only) Show «li members for non-user objects in vanables windows Ф, Warn if no user code on Hunch V. Enable property evaluation end other implicit function cells ■> Cell string conversion function on objects in variables windo |.". Enebie source server support t source line for breakpoints end current statemes '• V: Require source files to exactly match the original version ^nWfWW.iafflHiilUILlL'^lleW'H ' * I Show raw structure of objects In variables windows 1 Suppress ДТ optimization on module load (Managed only) ; У.' Warn if no symbols on launch (Native only) / Warn if script debugging is disabled on launch :<.::::....;:::::[:::iLu(ui[::u:n::;i _♦„ n Рис. 2.9. Снятие отметки с флажка Redirect all Output Window text to the Immediate Window Описание полученных результатов Пока что мы не будем сильно дробить код, поскольку здесь главной задачей является просто показать, как можно использовать средства разработки для создания работоспособного кода. Очевидно, что как VS, так VCE выполняют многое вместо разработчика и тем самым делают процесс компиляции и выполнения кода довольно простым. На самом деле существует несколько способов для выполнения даже самых базовых шагов: например, новый проект можно создавать и через упоминавшийся выше пункт меню, и нажатием комбинации клавиш <Ctrl+Shift+N>, и щелчком на соответствующей пиктограмме в панели инструментов. Компилировать и выполнять код тоже можно несколькими способами. Например, помимо описанного ранее выбора в меню Debug (Отладка) пункта Start Debugging, можно также использовать и соответствующую клавиатурную комбинацию (<F5>) или соответствующую пиктограмму в панели инструментов. Еще можно запускать код и, не находясь в режиме отладки, выбирая в меню Debug (Отладка) пункт Start Without Debugging (Запустить без отладки) (или нажав <Ctrl+F5>), а также компилировать проект, не запуская его (как с включенным, так и с выключенным режимом отладки), выбирая в меню Build (Сборка) пункт Build Solution (Собрать решение) или нажав <F6>. Обратите внимание, что запускать проект на выполнение можно и без отладки, а компоновать — с помощью соответствующих пиктограмм в панели инструментов, хотя по умолчанию эти пиктограммы в панели инструментов не отображаются.
48 Часть I. Язык С# После компиляции код можно выполнить, запустив созданный файл . ехе в проводнике Windows или в командной строке. Чтобы запустить демонстрационный код из командной строки, потребуется открыть окно командной строки, перейти в каталог С:\BegVCSharp\Chapter02\ConsoleApplicationl\ConsoleApplicationl\bin\Debug\, ввести ConsoleApplicationl и нажать клавишу <Enter>. В последующих примерах, когда будут попадаться указания типа "создать новый проект консольного приложения" или "выполнить код", вы можете выбирать для выполнения этих шагов любой способ. При отсутствии противоположных требований весь код следует запускать с включенным режимом отладки. Кроме того, термины "начать " (start), "выполнить " (execute) и "запустить " (run) используются в этой книге как взаимозаменяемые, а во все следующих за примерами объяснениях всегда предполагается, что вы вышли из созданного в примере приложения. Работа консольных приложений завершается сразу же, как только заканчивается выполнение их кода, а это означает, что в случае запуска этих приложений непосредственно из IDE у вас нет шанса увидеть результаты. Для обхода этой проблемы в предыдущем примере коду с помощью следующей строки было указано перед завершением ожидать нажатия клавиши: Console.ReadKey() ; Этот прием придется применять еще не раз в последующих примерах. Теперь, при наличии созданного проекта, можно более детально ознакомиться с основными областями среды разработки. Окно Solution Explorer В первую очередь стоит ознакомиться с окном Solution Explorer (Проводник решений), которое располагается в правом верхнем углу экрана. В VS и VCE оно выглядит одинаково (как и все остальные рассматриваемые в настоящей главе окна, если только не указывается противоположное). По умолчанию это окно настроено так, чтобы оно автоматически скрывалось, но при желании его можно пристыковать к краю экрана, выполнив щелчок на пиктограмме с изображением канцелярской кнопки, когда оно является видимым. Окно Solution Explorer делит пространство на экране с еще одним полезным окном под названием Class View (Представление классов), отобразить которое можно, выбрав в меню View (Вид) пункт Class View (Представление классов). На рис. 2.10 показаны оба этих окна со всеми развернутыми узлами (переключаться между ними можно щелчками на вкладках с соответствующими названиями в нижней части окна, когда оно пристыковано). Solution Explorer- ConsoleApplicationl <» ¥ X ClassVlew * Ч X -j jp Л *< -i <■ T2 Solution ConsoleApplicationl" A project) <$»мгь> ■ £1 - ИИДЯДИД 3 & Properties ■jj AssembryInfo.es ^1 References •vJ System •J System.Core •\3 System.Data •vJ System.Data.DataSetExtensions •ui System.Xml •J System-Xml.linq i] Program.cs ♦ \A Project References - {) ConsoleApplicationl В j# Program в d Ba5e тУРе$ Л% Object .^Solution Explorer Щой%View" ..•"") Solution Explorer -Class View Puc. 2.10. Окна Solution Explorer и Class View
Глава 2. Написание программы на языке С# 49 В данном случае в окне Solution Explorer отображаются файлы, из которых состоит проект ConsoleApplicationl. Помимо файла Program, cs, в который вы добавляли код, в нем отображается еще один файл кода Assemblylnf о. cs и несколько ссылок. Все файлы кода в С# имеют расширение . cs. Беспокоится о файле Assemblylnf о. cs сейчас не нужно. В нем содержится дополнительная информация о проекте, которая пока нас не волнует. Это окно можно использовать для изменения кода, который отображается в главном окне, либо дважды щелкнув на файлах с расширением .cs, либо щелкнув на них правой кнопкой мыши и выбрав в контекстном меню пункт View Code (Показать код), либо же выделив их и выполнив щелчок на соответствующей кнопке в панели инструментов, которая отображается в верхней части окна. Здесь также можно выполнять и другие операции с файлами, например, переименовывать их или вообще удалять из проекта. В этом окне могут еще отображаться и файлы других типов, вроде файлов ресурсов проекта (файлами ресурсов называются такие файлы, которые используются в проекте, но при этом могут и не быть файлами кода С#, например, файлы растровых изображений или звуковые). Опять-таки, ими тоже можно манипулировать с помощью этого же самого интерфейса. В узле References (Ссылки) содержится перечень используемых в проекте библиотек .NET. Об этом более подробно будет рассказываться чуть позже; пока что стандартные ссылки являются вполне подходящими. Второе окно — Class View — предоставляет другой вид проекта, показывая структуру созданного кода. К нему мы тоже вернемся чуть позже в этой книге; пока что нас вполне устраивает тот вид, который предлагается в окне Solution Explorer. Щелкая на файлах или других пиктограммах в этих окнах, вы можете заметить, что меняется содержимое окна Properties (рис. 2.11). Окно Properties В этом окне (открыть которое, если оно еще не открыто, можно через пункт меню View1^Properties Window (Вид^Окно свойств)) отображается дополнительная информация о любом элементе, который выбирается в окне, расположенном над ним. Например, вид, показанный на рис. 2.11, оно приобретает в случае выбора в проекте файла Program, cs. В этом окне также отображается информация и о других выбираемых элементах, например, о компонентах пользовательского интерфейса (как будет показано позже в этой главе, в разделе "Приложения Windows Forms"). Зачастую изменения, которые вносятся в элементы Рш 2п Внешний вид окна в окне Properties, непосредственно отражаются на коде properties после выбора в проекта либо добавлением дополнительных строк, либо проекте фаила Program. cs изменением тех, что уже существуют в файлах. В случае некоторых проектов на настройку элементов в этом окне приходится тратить ровно столько же времени, сколько и на внесение изменений в код вручную. Окно Error List На текущий момент окно Error List (Список ошибок), доступное путем выбора в меню View (Вид) пункта Error List (Список ошибок), не представляет особого интереса, поскольку в приложении ConsoleApplicationl ошибки отсутствуют. Однако оно действительно является очень полезным окном. Для того чтобы убедиться в этом, | Properties Pi оды ям* File Properties : 11 : Build Action Copy to Output Directory Custom Tool Custom Tool Namespace ШВШШШШ lilrNdmr Name of the file or folder. Compile Do not copy Program.cs - 9 xl •
50 Часть I. Язык С# попробуйте удалить из какой-нибудь строки из числа добавленных в предыдущем разделе символ точки запятой. Через мгновение в окне Error List должно появиться примерно такое сообщение, как показано на рис. 2.12. Рис. 2.12. В окне Error List сообщается об ошибке Кроме того, после этого скомпилировать проект уже больше не получится. В главе 3, где будет рассматриваться синтаксис языка С#, вы узнаете, что наличие символов точки с запятой ожидается во всем коде, фактически — в конце большинства строк. Это окно помогает избавляться от дефектов в коде, поскольку следит за тем, что требуется сделать для успешной компиляции проектов. Если дважды щелкнуть на записи об ошибке, курсор переместится в позицию, где эта ошибка встретилась в коде (исходный файл, в котором содержится ошибка, откроется автоматически, если он еще не был открыт), благодаря чему ошибку можно быстро исправить. Кроме того, в тех местах в коде, где имеются ошибки, еще также будут отображаться и волнистые линии красного цвета, благодаря которым легко видеть все проблемные участки. Обратите внимание, что местонахождение ошибки указывается с помощью номера строки. По умолчанию номера строк не отображаются в текстовом редакторе VS, но это такая функция, которую действительно стоит включить. Для этого отметьте флажок Line Numbers (Номера строк) в окне Options (Параметры), доступном через пункт меню Tools^Options (Сервис=>Параметры); чтобы добраться до него, нужно последовательно раскрыть категории Text Editor (Текстовый редактор), С# (Язык С#) и General (Общие), как показано на рис. 2.13. Чтобы эта опция стала доступной в VCE, потребуется выбрать настройку Show All Settings (Показывать все параметры); к тому же сам перечень опций будет немного отличаться от того, что показан на рис. 2.13. В этом диалоговом окне доступно много полезных опций и некоторые из них еще придется использовать позже в этой книге. Environment Project» end Solution» Source Control Text Editor Gtnerii Flic Extension All languages Bulc C# Tab» Advanced Formatting IntelllSen»e C/O* CSS HTMl Pl/SQl Statement completion R; Auto ll»t member» WHIde edvenced member» Щ Parameter Information Setting» ИН Enable vlrtuel tpect £3 wordwrap Г! Show vuuel grvrh» for word wrer flfj Apply Cut or Copy commend» to blank line» when ttter* l> n {election Display BJUnt number» W: Enable jingie-ciirt URl navigation ■ Navigation bar Puc. 2.13. Отметка флажка Line Numbers
Глава 2. Написание программы на языке С# 51 Приложения Windows Forms Часто демонстрировать код легче путем его выполнения в виде части приложения Windows, чем через окно консоли или окно командной строки. Для создания такого приложения необходимо использовать компоновочные блоки и собирать пользовательский интерфейс. В следующем практическом занятии иллюстрируются лишь базовые шаги, которые позволят увидеть, как создавать и запускать приложение Windows, не погружаясь в детали того, что оно делает на самом деле. Позже приложения Windows будут рассматриваться более подробно. 1. Создайте новый проект типа Windows Forms Application (в VS или VCE) в том же каталоге, что и раньше (С: \BegVCSharp\Chapter02, и если вы используете VCE, сохраните в нем проект после создания) с именем по умолчанию Windows Forms Application].. Если вы используете VS и первый проект еще открыт, удостоверьтесь в том, что выбрана опция Create New Solution (Создать новое решение) для создания нового решения. Все описанные параметры показаны на рис. 2.14 и 2.15. 2. Щелкните на кнопке ОК для создания проекта. После этого на экране должна появиться пустая форма Windows. Переместите курсор мыши в окно Toolbox в левой части экрана, а затем наведите его на элемент Button (Кнопка) на вкладке All Windows Forms (Все формы Windows), после чего дважды щелкните на нем кнопкой мыши, чтобы добавить кнопку в главную форму приложения (Forml). 3. Дважды щелкните на кнопке, которую добавили в форму. 4. После этого в файле Forml. cs должен появиться код С#. 1 Mr* Protect 1 Project Ъ/pas: Templates: VlwtfC* Window) Wtb Smart Device Office DltlDllt Reporting Ttrt WCP Workflow Other languages Otner Project Types Ttst Projtcb 1 1 A project for creating an application v. 1 Name: windowsFormsAp 1 location: C:\ProCSharp\Cher, Vni>*l Studio mtUUtd template* ^Windows Forms Application Щ ASP NET Wab Application 4JWPF Application Я С on tola Application j£f Outlook 2007 Add-in fjf Word 2007 Document My Templates J3 Starch Onllnt Templates... ННИГ| «ЯМВШНШМв^^б 1 ■ Clan library 5Jasp.net web Service Application 1 FfpwPF Browser Application »Vi«el 2007 Workbook SwCF Service Application 1 flwindows Forms Control library I «ni windows Forms user interface (NET Framework 3.S) 1 jlicatlonl mm 1 Solution 1Г—МШ||ЧШ i ■! НС 1 Solution Nimt: WlndowsPormsAppllcetionl reate directory for solution 1. °» .1 ~<*mU [J Рис. 2.14. Создание приложения Windows Forms в VCE
52 Часть I. Язык С# Щш Visual Studio installed templates Ш si \Ш СЭ !Р Э Window} Class library WPF WPF Browser Console Empty Project Forms Application Application Application Application My Templates Ш Search Online Templates... A project for creating an application with a Windows Forms user interface (.NET Framework 3.5) Name: WindowsFormsApplicationl Puc. 2.15. Создание приложения Windows Forms в VS Измените его, как показано ниже (здесь для краткости приведена лишь часть кода в этом файле): private void buttonl_Click(object sender, EventArgs e) MessageBox. Show ("The first Windows app in the book!") ; // Первое приложение Windows в книге! } 5. Запустите приложение. 6. Щелкните на доступной кнопке, чтобы отобразить диалоговое окно с сообщением, как показано на рис. 2.16. 7. Выйдите из приложения путем, щелкнув на пиктограмме X в правом верхнем углу окна, как это обычно и делается в стандартных приложениях Windows. чг* Focml The first Windows app m the bookl J Puc. 2.16. Приложение WindowsFormsApp1icationl в действии Описание полученных результатов Опять-таки, очевидно, что IDE-среда выполнила большую часть работы самостоятельно и позволила создать функциональное приложение Windows с минимальными усилиями. Созданное приложение ведет себя точно так же, как и другие окна: его можно перемещать, уменьшать или увеличивать в размере, сворачивать и т.п. Писать специально код для этих операций не потребовалось — они и так все работают. То же самое касается и добавленной кнопки. Двойного щелчка на ней для IDE-среды было достаточно для генерации шаблона кода, который будет выполняться в результате щелчка на этой кнопке в работающем приложении. Все, что потребовалось сделать — это ввести специфический код, получив все связанные с выполнением щелчка на кнопке функциональные возможности просто так.
Глава 2. Написание программы на языке С# 53 Конечно, приложения Windows не ограничиваются простыми формами с кнопками. Если вы заглянете в панель инструментов, откуда брался элемент управления Button, то обнаружите там множество строительных блоков пользовательского интерфейса, часть из которых может выглядеть знакомо. Большинство из них обязательно придется использовать в тот или иной момент, и вы сможете убедиться, что все они очень просты в применении, и могут сэкономить разработчику массу времени и усилий. Код этого приложения, содержащийся в Forml.cs, не выглядит намного сложнее кода из предыдущего раздела, как, впрочем, и код во всех остальных файлах в окне Solution Explorer. Большая часть генерируемого кода по умолчанию скрыта. Это связано с расположением элементов управления в форме, из-за чего этот код можно просматривать в основном окне только в режиме визуального конструктора (Design View), который дает визуальное представление такого кода компоновки. Кнопка является лишь примером элемента управления, который можно использовать, как и все остальные компоновочные блоки пользовательского интерфейса, доступные в разделе Windows Forms внутри окна Toolbox. Можете изучить предложенную в качестве примера элемента управления кнопку более детально. Переключитесь обратно в режим конструктора формы, воспользовавшись соответствующей вкладкой в главном окне, и затем щелкните один раз на кнопке, чтобы выделить ее. После этого в окне Properties (Свойства) в правом нижнем углу экрана появятся свойства этой кнопки (у элементов управления, как и у продемонстрированных в предыдущем примере файлов, есть свои свойства). Удостоверьтесь в том, что приложение в текущий момент находится в незапущенном состоянии, прокрутите окно свойств немного вниз до свойства Text, у которого в настоящий момент должно быть установлено значение buttonl, и измените его значение на ClickMe, как показано на рис. 2.17. I Properties 7¥"х| buttonl System.Windows.Forms.Button • * '■ — Tag EE3HHHHHHHL,(l"kMc В TextAlign MiddleCenter TextlmageRelation Overlay UseCompatibleTextRendering False UseMnemonlc True UseVisualStyleBackColor True UseWaitCursor False Visible True Text The text associated with the control. Рис. 2.17. Изменение значения свойства Text Это изменение должно также отразиться и в тексте, отображаемом на кнопке в форме Forml. Для этой кнопки доступно множество свойств, начиная от простых свойств для форматирования цвета и размера кнопки и заканчивая более сложными свойствами, вроде параметров привязки данных, которые позволяют устанавливать связи с базами данных. Как уже вкратце упоминалось в предыдущем примере, изменение свойств часто приводит к непосредственным изменениям в коде, и данный случай тоже не является исключением. Однако если перейти обратно в режим кода Forml. cs, то никаких изменений в коде видно не будет.
54 Часть I. Язык С# Чтобы увидеть эти изменения, понадобится заглянуть в упоминавшийся ранее скрытый код. Чтобы просмотреть файл, содержащий этот код, сначала раскройте в окне Solution Explorer узел Forml. cs, что приведет к отображению узла Forml. Designer. cs. Далее дважды щелкните на этом файле, чтобы увидеть, что находится внутри него. На первый взгляд вы можете и вообще не заметить в этом коде ничего такого, что бы отражало внесенное в свойство кнопки изменение. Объясняется это тем, что те разделы кода на С#, которые отвечают за компоновку и форматирование элементов управления в форме, являются скрытыми (ведь если имеется графическое представление результатов, на код обычно уже смотреть не требуется). Для достижения подобного хитрого эффекта в VS и VCE используется система организации кода. То, как она работает можно увидеть на рис. 2.18. ЛШШШМ а ? у namespace WlndowsrormsAppllcatlonl ( ЭП partial class F гг. 5 1 щ ;:i • i (ft <зчмлгу/ '.'/ Requited designee variable. private System.ComponentHodel.[Contttlm I components ■ null; С9ШЯШГУ /// Clean up any resources being used. 1ШВМ1 f> •i'J <paran- MM*"dl«poalng">tEUa if managed resources should be disposed; otherwise, ialse.<7pararo> protected override void Dispose(bool disposing) ( all)) if (disposing ч (components ( components.Dispose(); ) base.Dispose(disposing); ) ■ ■ private System.Windows.Forms.I at I r buttonl; Puc. 2.18. Организация кода Взглянув на левую часть кода (расположенную рядом с номерами строк, если была активизирована функция их отображения), вы сможете заметить некие серые линии и прямоугольники с символами + и - внутри них. Эти прямоугольники применяются для разворачивания и сворачивания разделов кода. Ближе к концу файла отображается один такой прямоугольник со знаком + внутри и другой прямоугольник напротив него уже внутри основного тела кода с надписью Windows Form Designer generated code (Код, сгенерированный конструктором форм Windows). Эта надпись, по сути, говорит о том, что имеется некий код, сгенерированный VS автоматически, о котором разработчику знать вовсе необязательно. При желании, однако, его все равно можно просмотреть, например, чтобы увидеть, к чему привело изменение, внесенное в свойство Text кнопки. Для этого достаточно просто щелкнуть на прямоугольнике с символом + внутри. После этого весь скрытый код станет видимым, и где-то внутри него вы сможете увидеть следующую строку: this.buttonl.Text = "Click Me"; На синтаксис, который здесь используется, пока не нужно обращать внимание. Главное, что вы должны увидеть — это то, что тот текст, введенный в окне Properties, появился и непосредственно в самом коде.
Глава 2. Написание программы на языке С# 55 Такой механизм организации кода является очень удобным при написании кода, поскольку позволяет разворачивать и сворачивать и многие другие разделы, а не только те, которые скрываются обычно. Точно так же, как просмотр содержания позволяет быстро получать общее представление о содержимом книги, просмотр ряда свернутых разделов кода может значительно упрощать навигацию по коду на С#, объемы которого порой в приложениях могут быть просто огромными. Резюме В настоящей главе были описаны некоторые основные средства, которыми неоднократно придется пользоваться в оставшейся части книги. Здесь был приведен краткий обзор сред разработки Visual Studio 2008 и Visual C# 2008 Express Edition и показано их применение при создании консольных приложений и приложений Windows. Консольные приложения являются более простыми, подходят для удовлетворения большинства потребностей и позволяют фокусироваться на базовых средствах программирования С#. Что касается приложений Windows, то они, конечно, являются более сложными, но зато обладают более впечатляющим внешним видом, и более понятны в плане эксплуатации для тех, кто привык работать в среде Windows (каковых, надо признать, большинство). Теперь, когда вам известно, как создаются простые приложения, можно переходить непосредственно к изучению самого языка С#. В следующих главах речь пойдет сначала о базовом синтаксисе языка С# и структуре, которую должны иметь создаваемые на нем программы, затем — о более сложных методах объектно-ориентированного программирования, а после этого — о том, как язык С# можно использовать для получения доступа ко всем мощным возможностям, которые доступны в .NET Framework. В этой главе рассмотрены следующие вопросы. □ Как работают среды разработки Visual Studio 2008 и Visual C# 2008. □ Как можно создать простое консольное приложение. □ Как можно создать и запустить приложение Windows. Во всех последующих главах, если только не указывается противоположное, все инструкции подразумевают использование среды VCE, хотя, как было показано в настоящей главе, адаптировать их под VS вовсе не сложно, поэтому применять можно любую IDE-среду, какую хочется или к которой имеется доступ.
^^ Переменные и выражения Для того чтобы пользоваться языком С# эффективно, важно понимать, что на самом деле происходит при создании компьютерной программы. Пожалуй, наиболее простым определением компьютерной программы является то, которое гласит, что она представляет собой последовательность операций, манипулирующих данными. Это действительно так и даже в случае наиболее сложных примеров, вроде обширных многофункциональных приложений Windows (например, Microsoft Office Suite). Хотя в большинстве случаев пользователям приложений этого совершенно не видно, "за кулисами" все выглядит именно так. Чтобы нагляднее проиллюстрировать это, возьмем такую часть компьютера, как монитор. То, что мы видим на мониторе, зачастую кажется таким знакомым, что представить то, что это не "кинофильм", а нечто иное, довольно трудно. На самом же деле то, что мы видим на мониторе, является лишь представлением данных, которые в необработанном виде являются просто набором запрятанных где-то в памяти нулей и единиц. Любое выполняемое на экране действие, вроде перемещения курсора мыши, щелчка на пиктограмме или ввода текста в окне текстового редактора, приводит к перемещению этих данных в памяти. Конечно, можно взять и более простой пример. Пусть это будет приложение-калькулятор. Это приложение позволяет предоставлять данные в виде чисел и затем выполнять над этими числами различные математические операции во многом точно так же, как в случае использования бумаги и карандаша, но только гораздо быстрее. Поскольку действие компьютерных программ основывается на выполнении операций с данными, это означает, что они обязательно должны предусматривать какой-то способ для хранения этих данных и методы для работы с ними. Эти две функции обеспечиваются с помощью, соответственно, переменных и выражений. О том, что это означает, и рассказывается в настоящей главе, как в общих, так и в более конкретных чертах. В частности, в настоящей главе рассматриваются следующие темы. □ Базовый синтаксис С#. □ Переменные и способы их использования. □ Выражения и способы их использования.
Глава 3. Переменные и выражения 57 Первым делом, конечно же, необходимо ознакомиться с базовым синтаксисом, который применяется в программировании на С#, поскольку без контекста изучать способы работы с переменными и выражениями в языке С# не получится. Базовый синтаксис С# По виду и поведению код С# похож на код C++ и Java. Поначалу синтаксис языка С# может показаться запутанным. Однако после "погружения" в мир программирования на С# становится понятно, что применяемый стиль является вполне целесообразным и что он позволяет писать читабельный код без особых усилий. В отличие от компиляторов некоторых других языков, компиляторы С# не обращают внимания на дополнительные интервалы в коде, проставляемые с помощью символов пустой строки, возврата каретки и табуляции (все вместе они называются пробельными символами). Это предоставляет разработчику огромную свободу в плане форматирования кода, хотя следование определенным правилам, конечно же, может помочь делать код более удобным для чтения. Код на С# состоит из ряда операторов, каждый из которых оканчивается символом точки с запятой. Поскольку пробелы игнорируются, в одной строке может размещаться сразу несколько операторов, но для удобочитаемости все-таки принято после символов точки с запятой в конце каждого оператора добавлять возврат каретки во избежание появления в одной и той же строке нескольких операторов. Тем не менее, вполне допустимо (и довольно распространено) использовать операторы, занимающие по несколько строк кода. С# является языком с блочной структурой, т.е. все операторы относятся к какому-то блоку кода. В каждом блоке, который отделяется от остальной части кода с помощью фигурных скобок ({ и }), может содержаться любое количество операторов или вообще ни одного. Обратите внимание на то, что сопровождать символы фигурных скобок символами точки с запятой не требуется. Например, простой блок кода на С# может иметь следующий вид: { <строка_кода_1, оператор_1>; <строка_кода_2, оператор_2> <строка_кода_3, оператор_2>; } Здесь разделы <строка_кода_х, оператор_у> фактическими фрагментами кода С# не являются, а просто представляют собой метки-заполнители, указывающие на место, где должны находиться С#-операторы. В данном случае вторая и третья строки кода являются частью одного и того же оператора, поскольку в конце второй строки нет символа точки с запятой. В следующем простом примере уже используются отступы для внесения ясности внутри самого кода С#. Добавление отступов на самом деле является стандартным приемом и в принципе по умолчанию выполняется средой VS автоматически. В общем для каждого блока кода используется свое количество отступов, т.е. своя степень смещения вправо. Блоки кода еще также могут и вкладываться один в другой, т.е. одни блоки могут содержать другие блоки, и в таком случае вложенные блоки, соответственно, смещаются на большее количество отступов вправо: { <строка_кода_1>; { <строка_кода_2>; <строка_кода_3>; } <строка_кода_4 >; }
58 Часть I. Язык С# Отобразив в VCE диалоговое окно Options (Параметры) (путем выбора в меню Tools (Сервис) пункта Options (Параметры)), можно найти правила, которые VCE применяет для форматирования кода. Этих правил очень много и все они содержатся в различных подкатегориях узла Text Editor^ С#с>Formatting (Текстовый редактора С#=> Форматирование). Большинство из них касается тех частей С#, которые еще не рассматривались, но вы можете вернуться к ним позже и настроить так, чтобы они больше соответствовали вашему стилю. Для ясности сообщаем, в настоящей книге все приводимые фрагменты кода иллюстрируются в том виде, который они бы имели при использовании предлагаемых по умолчанию параметров форматирования. Разумеется, такой стиль ни в коем случае не является обязательным. Однако если его не придерживаться, то уже очень скоро по мере прочтения книги станет ясно, что без него код может становиться очень запутанным. Еще одним очень часто встречаемым в коде на С# элементом являются комментарии. Собственно кодом на С# комментарии не считаются, но зато они замечательно с ним сосуществуют. Что они делают, в принципе понятно и без объяснений: они позволяют добавлять в код описательный текст на обычном языке, который компилятором игнорируется. При создании длинных разделов кода очень удобно добавлять пометки, напоминающие о том, какие именно операции в них выполняются, вроде "эта строка кода предлагает пользователю ввести число" или "этот раздел кода написал Боб". С# позволяет делать это двумя способами: либо размещением соответствующих маркеров в начале и в конце комментария, либо использованием одного единственного маркера, означающего, что "остальная часть данной строки является комментарием". Последний способ представляет собой исключение из упоминавшегося ранее правила об игнорировании С#-компиляторами символов возврата каретки, но считается особым случаем. Для обозначения комментариев в случае применения первого способа в начале комментария ставятся символы /*, а в конце — символы */. Эти символы могут находиться как на одной и той же строке, так и на разных, в случае чего все находящиеся между ними строки тоже считаются комментарием. Единственной комбинацией символов, которую нельзя вводить в теле комментария, является */, потому что она воспринимается как маркер конца строки. Например, показанный ниже тип оформления комментариев является допустимым: /* Это комментарий */ /* И это... . . . тоже комментарий! */ А вот следующий тип оформления, однако, будет приводить к появлению проблем: /* Комментарии часто заканчиваются символами "*/" */ Здесь конец комментария (символы, идущие после "*/") будут восприниматься как код С#, из-за чего и будут возникать ошибки. Второй способ добавления комментариев подразумевает использование в начале комментария комбинации символов //. После этой комбинации можно вводить все, что угодно, главное — уместить все в одной строке. Например, следующий вариант является приемлемым: // Это другой вид комментария. А вот показанный ниже вариант будет приводить к появлению ошибок, поскольку вторая строка будет интерпретироваться не как комментарий, а как код С#: //И это тоже, а вот этот фрагмент уже нет.
Глава 3. Переменные и выражения 59 Такой способ комментирования удобно применять для документирования операторов, поскольку он позволяет размещать операторы и комментарии в одной строке: <оператор>; II Объяснение оператора В начале было сказано, что существует только два способа для добавления комментариев в код С#, но на самом деле существует еще и третий способ, который, в принципе, является расширенной версией синтаксиса //, потому что позволяет добавлять однострочные комментарии, начинающиеся не с двух, а с трех символов слэша, как показано ниже: /// Специальный комментарий При обычных обстоятельствах такие комментарии компилятором игнорируются, подобно все остальным комментариям, но среду VS можно настраивать так, чтобы при компиляции проекта она извлекала текст, идущий после таких комментариев, и создавала для него отформатированный специальным образом текстовый файл, который затем можно применять для создания документации. Этот процесс более подробно будет рассматриваться в главе 31. Очень важным, моментом, на который обязательно нужно обратить внимание в С#- коде, является его чувствительность к регистру. В отличие от некоторых других языков, в языке С# код обязательно нужно вводить в правильном регистре, поскольку использование прописной буквы вместо заглавной может привести к невозможности скомпилировать проект. Например, возьмем следующую строку кода, которая уже встречалась в главе 2: Console.WriteLine("The first app in Beginning C# Programming!"); Ее С#-компилятор сможет понять, потому что все буквы в команде Console. WriteLine () указаны в правильном регистре. Ни одна из приведенных ниже строк, однако, работать не будет: console.WriteLine("The first app in Beginning C# Programming!"); CONSOLE.WRITELINE("The first app in Beginning C# Programming!"); Console.Writeline("The first app in Beginning C# Programming!"); Во всех этих строках использован неправильный регистр, поэтому С#-компилятор не будет понимать, чего от него хотят. К счастью, как вы скоро узнаете, VCE очень помогает в вопросах, касающихся ввода кода, и в большинстве случаев догадывается (настолько, насколько может догадываться компьютерная программа) о том, что пытается сделать программист. По мере того, как программист вводит код, она предлагает команды, которые он может использовать, и старается исправлять ошибки, связанные с применением неправильного регистра. Структура базового консольного приложения на С# Давайте теперь детальнее рассмотрим приводившийся в главе 2 пример консольного приложения под названием ConsoleApplicationl и немного разберем его структуру. Вспомните, что код этого приложения выглядит так: using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplicationl {
60 Часть I. Язык С# class Program { static void Main(string[] args) { // Вывод текста на экран. Console.WriteLine("The first app in Beginning C# Programming!"); Console.ReadKey(); } } } Нетрудно заметить, что здесь присутствуют все обсуждавшиеся в предыдущем разделе синтаксические элементы]: и точки с запятой, и фигурные скобки, и комментарии, и соответствующие отступы. Наиболее важным на текущий момент является следующий раздел кода: static void Main(string [ ] args) { // Вывод текста на экран. Console.WriteLine("The first app in Beginning C# Programming!"); Console.ReadKey(); } Именно этот код и выполняется при запуске приложения. Если говорить точнее, то при запуске выполняется тот блок кода, который заключен в фигурные скобки. Строка комментария ничего не делает, о чем говорилось ранее; она присутствует здесь просто для внесения ясности. Две остальные строки кода выводят некоторый текст в окно консоли и ожидают ответа; каким именно образом они это делают, нас пока не касается. Теперь давайте посмотрим, как добиться организации кода, о которой рассказывалось в предыдущей главе. Обозначим с помощью ключевых слов #region и #endregion начало и конец раздела кода, который нужно сделать сворачиваемым. Это означает, что, например, код, сгенерированный для приложения ConsoleApplicationl, можно изменить следующим образом: #region Using directives using System; using System.Collections .Generic- using System.Linq; using System.Text; #endregion Это позволит сворачивать этот код до одной строки и снова разворачивать его позже при возникновении необходимости в просмотре деталей. О том, что собой представляют содержащиеся здесь операторы using и следующий сразу же за ними оператор namespace, более подробно будет рассказываться в конце этой главы. Любое ключевое слово, которое начинается с символа #, фактически является директивой препроцессора, а не ключевым словом языка С#. Другие подобные ключевые слова, кроме описанных здесь #region и tiendregion, могут быть довольно сложными и, как правило, имеют очень специфическую область применения. Эта одна из тем, изучением которых можно заняться самостоятельно после прочтения настоящей книги. На остальной код в этом примере пока не нужно обращать внимания, потому что задачей первых нескольких глав является рассмотрение базового синтаксиса С#, следовательно, точный способ, которым процесс выполнения приложения доходит до точки, где вызывается метод Console.WriteLine (), сейчас не важен. Позже значимость этого дополнительного кода будет обязательно разъяснена.
Глава 3. Переменные и выражения 61 Переменные Как уже упоминалось ранее, переменные имеют отношение к хранению данных. По сути, к переменным в памяти компьютера можно относиться как к коробкам на полке. В коробки можно класть какие-то вещи и затем снова доставать их оттуда, или просто заглядывать в них и проверять, есть ли там вообще что-нибудь. То же самое и с переменными: в них можно помещать данные и затем при необходимости либо извлекать их оттуда, либо проверять их наличие. Хотя все данные на компьютере, в сущности, имеют одинаковый вид (наборы нулей и единиц), переменные бывают разных видов, называемых в их случае типами. Если снова провести аналогию с коробками, то коробки бывают разных форм и размеров, из-за чего некоторые вещи могут умещаться только в коробках определенного вида. Объясняется наличие такой системы типов тем, что разные типы данных могут требовать использования разных методов для работы с ними, а ограничение переменных конкретными типами позволяет избегать их смешивания. Например, в обработке набора нулей и единиц, представляющего цифровое изображение, тем же образом, что и набора, представляющего аудио-ролик, не было бы никакого смысла. Чтобы использовать переменные, их нужно объявлять. Это означает, что им необходимо назначать имя и тип. После объявления их можно начинать использовать в качестве единиц хранения для данных того типа, для хранения которого они предназначены согласно объявлению. В синтаксисе С# объявление переменных осуществляется указанием типа и имени переменной: <тип> <имя>; При попытке использовать переменную, которая не была объявлена, код не будет компилироваться, но в отличие от остальных случаев компилятор будет точно сообщать причину возникшей проблемы, так что такой уж ужасной ошибкой это не является. При попытке использовать переменную без присваивания ей значения тоже будет возникать ошибка, но компилятор тоже ее распознает. Количество типов, которые можно использовать, практически бесконечно. Все дело в том, что допускается определять свои собственные типы переменных для хранения каких угодно сложных видов данных. Несмотря на сказанное, однако, существует ряд таких типов переменных, которые рано или поздно нужно будет использовать любому разработчику, вроде переменной, позволяющей хранить числа. Поэтому об этих нескольких простых предопределенных типах необходимо знать каждому. Простые типы К числу простых типов относятся типы вроде числовых и булевских (true или false) значений, которые выступают в роли основных строительных блоков в любом приложении. В отличие от сложных типов, простые типы не могут иметь ни дочерних типов, ни атрибутов. Большинство из доступных простых типов являются числовыми, что на первый взгляд может показаться странным, ведь для хранения числа точно необходим один тип? Причина такого изобилия числовых типов связана с механизмом хранения чисел в памяти компьютера в виде последовательностей нулей и единиц. В случае целочисленных значений просто берется ряд битов (отдельных цифр, которыми может быть О или 1) и представляется число в двоичном (бинарном) формате. Переменная, хранящая iVбитов, позволяет представлять любое число в диапазоне от 0 до B - 1). Любые числа больше этого значения не умещаются в такой переменной.
62 Часть I. Язык С# Например, предположим, что имеется переменная, способная хранить 2 бита. Соответствие между целыми числами и представляющими их битами в случае этой переменной будет выглядеть следующим образом: 0 = 00 1 = 01 2 = 10 3 = 11 При желании иметь возможность сохранять больше чисел, потребуется больше битов (например, 3 бита позволяют хранить числа от 0 до 7). Неизбежным результатом такой системы является то, что для получения возможности сохранять любое вообразимое число, понадобится бесконечное количество битов, которое никак не сможет уместиться в памяти обычного ПК. Даже при наличии определенного количества битов, которое можно применять для каждого числа, использовать все эти биты для переменной, требующейся для хранения, к примеру, только чисел в диапазоне от 0 до 10, будет не эффективным подходом (потому что будет приводить к растрате ресурсов памяти). Вместо этого гораздо лучше применять другие целочисленные типы, способные хранить разные диапазоны чисел и занимающие разные объемы памяти (вплоть до 64 битов). Все эти типы перечислены в табл. 3.1. Каждый из этих типов подразумевает использование одного из соответствующих стандартных типов, определенных в .NET Framework. Как уже рассказывалось в главе 1, именно такое применение стандартных типов обеспечивает функциональную совместимость языков. Имена, используемые для типов в С#, являются псевдонимами для тех типов, которые определены в .NET Framework. В табл. 3.1 имена типов представлены в виде, который они имеют в библиотеке .NET Framework. Таблица 3.1. Основные целочисленные типы Тип sbyte byte short ushort int uint long ulong Псевдоним System.SByte System.Byte System.Intl6 System.UIntl6 System.Int32 System.UInt32 System.Int64 System.UInt64 Допустимые значения Целое число в диапазоне от -128 до 127 Целое число в диапазоне от 0 до 255 Целое число в диапазоне от -32768 до 32767 Целое число в диапазоне от 0 до 65535 Целое число в диапазоне от -2147483648 до 2147483647 Целое число в диапазоне от 0 до 4294967295 Целое число в диапазоне от -9223372036854775808 до 9223372036854775807 Целое число в диапазоне от 0 до 18446744073709551615 Символы и в начале имен некоторых переменных являются сокращением от слова unsigned (без знака), которое указывает на то, что хранить отрицательные числа в переменных этих типов нельзя, о чем свидетельствуют значения в столбце "Допустимые значения" таблицы. Разумеется, также бывает необходимо хранить значения с плавающей запятой, т.е. значения, которые не являются целыми числами. Для них доступны переменные трех следующих типов: float, double и decimal. Первые две позволяют хранить значения с плавающей запятой в виде ±т*2 , причем допустимые значения для т и еу них
Глава 3. Переменные и выражения 63 отличаются. Что касается decimal, то переменная такого типа позволяет хранить значения в виде ±тх10 . Все эти три типа переменных показаны в табл. 3.2 вместе с их допустимыми значениями для т и е и ограничениями в виде настоящих числовых показателей. Таблица 3.2. Типы переменных для хранения чисел с плавающей запятой Тип Псевдоним Мин. для т Макс. Мин. Макс. Приблизительное Приблизительное т мин. значение макс, значение float double decimal System. Single System. Double System. Decimal 0 0 0 224 -149 104 1,5x10 -45 3,4x10: 38 253 -1075 970 5,0x10 -324 296 -26 1,0x10"; 1,7x10' 7,9x1021 308 Помимо числовых типов, еще также доступны и другие простые типы, которые перечислены в табл. 3.3. Таблица 3.3. Нечисловые простые типы Тип Псевдоним для Допустимые значения Одиночный символ Unicode, сохраняемый в виде целого числа в диапазоне от 0 до 65535 Булево значение, которое может иметь вид либо true, либо false Последовательность символов char bool string System.Char System.Boolean System.String Обратите внимание на то, что верхнего предела по количеству символов, которое может сохраняться в string, не существует, поскольку этот тип может использовать различные объемы памяти. Булевский тип bool является одним из наиболее часто используемых типов переменных в С#, и на самом деле аналогичные типы не менее часто применяются и в других языках. Наличие переменной, которая может содержать либо значение true, либо значение false, играет очень важную роль в процессе выполнения логики приложения. В качестве простого примера, представьте, на какое количество вопросов можно ответить с помощью "да" или "нет" (т.е. посредством true или false). Выполнение сравнений между значениями переменных и проверка правильности входных данных являются лишь двумя наиболее типичными примерами программного применения булевских переменных, которые очень скоро будут рассмотрены более подробно. Теперь, после ознакомления со всеми простыми типами, давайте разберем один небольшой пример, наглядно демонстрирующий способы их объявления и использования. В следующем практическом занятии приводится простой код, который объявляет две переменные, присваивает им значения и затем выводит эти значения на экран. Практическое занятие Использование переменных простых типов 1. Создайте новое консольное приложение по имени Ch03Ex01 и сохраните его в каталоге C:\BegVCSharp\Chapter03.
64 Часть I. Язык С# 2. Добавьте в Program.cs следующий код: static void Main(string [ ] args) { int mylnteger; string myString; mylnteger = 17; myString = "\"mylnteger\" is"; Console.WriteLine("{0} {1}.", myString, mylnteger); Console.ReadKey(); } 3. Выполните этот код. На рис. 3.1 показан результат, который должен получиться. Рис. 3.1. Результат выполнения приложения Ch03Ex01 Описание полученных результатов Добавленный код делает три вещи: □ объявляет две переменных; □ присваивает этим двум переменным значения; □ выводит значения этих двух переменных на консоль. Объявление переменных происходит в следующей части кода: int mylnteger; string myString; В первой строке объявляется переменная типа int с именем mylnteger, а во второй — переменная типа string с именем myString. При именовании переменных требуется соблюдать определенные правила; указывать просто любую последовательность символов нельзя. Более подробно об этом будет рассказываться чуть позже в этой главе, в разделе "Именование переменных ". В следующих двух строках кода осуществляется присваивание значений: mylnteger = 17; myString = "\"mylnteger\" is"; Здесь переменным присваиваются два фиксированных значения (называемые в коде литеральными значениями) с помощью операции присваивания = (об операциях более подробно речь пойдет позже в этой главе, в разделе "Выражения"). Переменной mylnteger присваивается целочисленное значение 17, а переменной myString — строка "mylnteger" (вместе с кавычками). При присваивании строковых литеральных значений подобным образом строки обязательно должны заключаться в двойные кавычки. Включение в саму строку некоторых символов, как, например, символов двойных кавычек, может приводить к появлению проблем, из-за чего такие символы должны обязательно отменяться (escape) путем подстановки специальной последовательности символов, называемой управляющей последовательностью и представ-
Глава 3. Переменные и выражения 65 ляющей символ или символы, которые требуется использовать. В данном примере для отмены символа двойной кавычки применяется последовательность \": myString = "\"mylnteger\" is"; Если бы ее не было, и строка кода выглядела следующим образом: myString = ""mylnteger" is"; компилятор сообщил бы об ошибке. Обратите внимание, что присваивание строковых литералов является еще одной ситуацией, в которой следует соблюдать осторожность при использовании символов разрыва строки, потому что компилятор С# будет отклонять все строковые литералы, занимающие более одной строки. При желании добавить символ разрыва строки, лучше использовать управляющую последовательность, представляющую символ возврата каретки, которая выглядит как \п. Например, следующий код: myString = "Эта строка содержит \псимвол разрыва строки."; будет приводить к отображению присвоенной строки на двух отдельных строках в окне консоли, как показано ниже: Эта строка содержит символ разрыва строки. Все управляющие последовательности состоят из символа обратной косой черты (слэша), за которым следует один символ из небольшого специального набора (этот набор будет представлен позже). Из-за того, что символ слэша применяется для этой цели, для него самого тоже существует управляющая последовательность, которая представляет собой два следующих друг за другом слэша (\\). Вернемся к рассматриваемому коду. В нем есть еще одна новая и заслуживающая внимания строка: Console.WriteLine ("{0} {1}.", myString, mylnteger); Она похожа на простой метод записи текста в окно консоли, который уже объяснялся в первом примере, но только теперь еще также указываются и переменные. Чтобы не забегать вперед, пока опустим рассмотрение многих касающихся этой строки кода деталей. Скажем только, что для вывода текста в окно консоли в первой части настоящей книги везде будет применяться именно такой прием. Внутри скобок содержатся две вещи: □ строка, подлежащая выводу; □ список разделенных запятыми переменных, значения которых должны вставляться в выходную строку. Кажется, что в выводимой строке — " { 0 } {1} . " — нет никакого особо полезного текста. Как уже показывалось ранее, на экране при выполнении кода отображается совсем не то. А все дело в том, что данная строка, по сути, представляет собой шаблон, в который вставляется содержимое переменных. Каждый набор фигурных скобок в строке является меткой-заполнителем, на месте которой во время выполнения будет находиться содержимое одной из указанных в перечне переменных. Каждая метка-заполнитель (или строка форматирования) представляется в виде заключенного в фигурные скобки целого числа. Отсчет этих целых начинается с нуля и затем просто увеличивается на 1. Общее количество меток-заполнителей должно соответствовать количеству переменных, указываемых в разделенном запятыми списке следом за строкой.
66 Часть I. Язык С# При выводе текста в окно консоли каждая метка-заполнитель заменяется значением соответствующей ей переменной. В рассматриваемом примере это означает, что {0} заменяется фактическим значением первой переменной, т.е. myString, а {1} — содержимым переменной mylnteger. Именно этот метод вывода текста в окно консоли и будет применяться для отображения выходных данных из кода во всех последующих примерах. И, наконец, в анализируемом сейчас коде также имеется продемонстрированная ранее строка для ожидания ввода от пользователя перед завершением работы: Console.ReadKey() ; Эта строка кода тоже пока подробно рассматриваться не будет, но зато будет довольно часто встречаться в последующих примерах. На данный момент о ней главное знать то, что она приостанавливает выполнение кода до тех пор, пока не будет нажата любая клавиша. Именование переменных Как уже упоминалось в предыдущем разделе, выбирать любую последовательность символов в качестве имени для переменной нельзя. Однако все не так сложно, как могло показаться на первый взгляд, потому что предлагаемая система именования все равно является очень гибкой. Основные правила по именованию переменных описаны ниже. □ Первым символом в имени переменной обязательно должна быть либо буква, либо символ подчеркивания (_), либо символ @. □ Последующими символами в имени переменной могут быть буквы, символы подчеркивания или числа. Помимо этого еще существуют определенные ключевые слова, которые имеют для компилятора С# особое значение, вроде показанных ранее ключевых слов using и namespace. В случае использования одного из таких слов компилятор будет выдавать соответствующее сообщение, поэтому сильно беспокоиться об этом не стоит. Например, ниже приведены приемлемые имена для переменных: myBigVar VAR1 _test Следующие имена являются недопустимыми: 99BottlesOfBeer namespace It's-All-Over Не следует забывать, что язык С# является чувствительным регистру, а это требует точно запоминать регистр букв, используемый в имени переменных при их объявлении. Добавление ссылок на них позже в программе с допущением ошибки в регистре даже одной единственной буквы будет препятствовать компиляции. Последствия будут даже еще более серьезными, если переменных много, и их имена отличаются только регистром. Например, все перечисленные ниже имена переменных будут являться совершенно разными именами: myVariable MyVariable MYVARIABLE
Глава 3. Переменные и выражения 67 Соглашения об именовании Имена переменных— это то, чем приходится пользоваться постоянно, поэтому совсем не помешает посвятить немного времени на рассмотрение того, какого вида имена следует применять. Прежде чем приступить к изучению этого вопроса, однако, запомните, что он является спорным. Разные системы постоянно появляются и исчезают, и многие разработчики горячо отстаивают преимущества своей личной системы. До недавнего времени наиболее популярной считалась венгерская нотация (Hungarian notation). Эта система подразумевает использование в именах всех переменных стандартных прописных префиксов, указывающих на тип этих переменных. Например, если переменная имеет тип int, тогда в начале ее имени может стоять префикс i (или п), т.е. оно может выглядеть, например, как iAge. Применение этой системы позволяет легко определять тип переменной по первому же взгляду на ее имя. В более современных языках вроде С# внедрение этой системы является не совсем простой задачей. Для типов, которые были рассмотрены пока, пожалуй, еще можно было бы подобрать одно- или двухбуквенные префиксы. Но из-за возможности создавать свои собственные типы и наличия многих сотен таких более сложных типов в базовой библиотеке .NET Framework, такой подход быстро теряет практичность. В случае работы над проектом нескольких людей легко может случиться так, что каждый из них придумает свои разные и сложные префиксы, что может привести к катастрофическим последствиям. Разработчики поняли, что гораздо лучше именовать переменные в соответствии с их предназначением. При возникновении каких-либо сомнений тип переменной можно всегда довольно легко выяснить. В VS и VCE для этого, например, достаточно просто навести на имя переменной курсор мыши: после этого появляется всплывающее окно с информацией о том, к какому типу она относится. В настоящее время в пространствах имен .NET Framework используются две таких системы именования: PascalCase (стиль языка Pascal) и camelCase ("верблюжий" стиль). В обеих системах на предназначение переменных указывает используемый в их именах регистр. Обе они применяются к именам, которые состоят из нескольких слов, и обе требуют, чтобы все буквы в каждом слове имени были прописными, кроме первой, которая должна быть заглавной. В системе camelCase присутствует еще одно дополнительное правило, требующее, чтобы первое слово начиналось с прописной буквы. Таким образом, имена переменных, созданные по системе camelCase, могут выглядеть, к примеру, так: age firstName timeOfDeath А имена переменных, созданные по системе PascalCase, соответственно, так: Age LastName WinterOfDiscontent Системой camelCase лучше пользоваться для именования простых переменных, а системой PascalCase, согласно рекомендациям Microsoft, — более сложных переменных. И, наконец, напоследок обратите внимание, что во многих прежних системах именования часто применялся символ подчеркивания, обычно в качестве разделителя между словами в именах переменных, например: yetanothervariable. Сейчас его применение не поощряется (что и хорошо, поскольку выглядит это неуклюже).
68 Часть I. Язык С# Литеральные значения В предыдущем практическом занятии было показано два примера литеральных значений— integer и string. У переменных других типов тоже могут быть ассоциируемые с ними литеральные значения, как видно в табл. 3.4. Многие из них подразумевают использование суффиксов, т.е. добавление в конце литерального значения последовательности символов для указания желаемого типа. Некоторые литералы имеют по несколько типов, которые определяются во время компиляции самим компилятором на основании их контекста (что тоже можно увидеть в табл. 3.4). Таблица 3.4. Литералы Тип (типы) bool int, uint, long, ulong uint,ulong long,ulong ulong float double decimal Char String Категория Булевская Целочисленная Целочисленная Целочисленная Целочисленная Вещественная Вещественная Вещественная Символьная Строковая Суффикс Нет Нет и или и 1 ИЛИ L ul, uL, Ul, UL, lu, 1U, Lu ИЛИ LU f ИЛИ F Нет, d или d тИЛИМ Нет Нет Пример/ допустимые значения true или false 100 100U 100L 100UL 1.5F 1.5 1.5М ' а' или управляющая последовательность символов "а... а", может включать управляющие последовательности символов Строковые литералы Ранее в этой главе уже было показано несколько управляющих последовательностей, которые могут использоваться в литералах string. В табл. 3.5 для справки перечислены все такие управляющие последовательности. Таблица 3.5. Управляющие последовательности, используемые в строковых литералах Управляющая последовательность V \" \\ \о \а Получаемый символ Символ одиночной кавычки Символ двойной кавычки Символ обратной косой черты Отсутствие символа. Символ предупреждения Unicode-значение символа 0x0027 0x0022 0х005С 0x0000 0x0007 \ь (приводит к выдаче звукового сигнала) Символ возврата 0x0008
Глава 3. Переменные и выражения 69 Окончание табл. 3.5 Управляющая Получаемый символ Unicode-значение последовательность получаемый символ символа \f Символ перевода страницы 0x000с \п Символ новой строки ОхОООА \г Символ возврата каретки OxOOOD \t Символ горизонтальной табуляции 0x0009 \v Символ вертикальной табуляции 0x000В В столбце "Unicode-значение символа" приведены шестнадцатеричные значения символов в том виде, в котором присутствуют в наборе символов Unicode. Как и раньше, любой символ Unicode можно указывать с помощью управляющей последовательности Unicode. Такие управляющие последовательности состоят из стандартного символа \, за которым идет символ и и состоящее из четырех цифр шестнадцатеричное значение (как, например, четыре цифры после х в табл. 3.5). Это означает, что следующие строки являются эквивалентными: "Karli\'s string." "Karli\u0027s string." Очевидно, что управляющие последовательности Unicode привносят больше разнообразия. Строки еще также можно задавать и дословно (verbatim), т.е. делать так, чтобы в строку включались все содержащиеся между знаками двойных кавычек символы вместе с символами конца строки и символами, которые в противном случае необходимо было бы отменять. Единственным исключением является управляющая последовательность для символа двойных кавычек, которая должна обязательно указываться во избежание окончания строки. Чтобы сделать это, достаточно просто добавить перед строкой символ @: @"Verbatim string literal." Эту строку точно так же легко можно было бы задать и обычным образом, но вот для следующей строки требуется обязательно использовать только такой метод: @"А short list: item 1 item 2" Задаваемые дословно строки особенно удобно применять для имен файлов, поскольку в них обычно используется очень много символов обратной косой черты. В случае применения обычных строк придется постоянно добавлять по два символа обратной косой черты: "С:\\Temp\\MyDir\\MyFile.doc" Задаваемые дословно строки позволяют получать более читабельный результат. Например, в случае использования задаваемой дословно строки предыдущая строка будет иметь такой вид: @"С:\Temp\MyDir\MyFile.doc" Как будет объясняться позже в этой книге, в отличие от других демонстрировавшихся типов, которые являются типами-значениями, строки являются ссылочными типами. Из этого следует, что строкам может также присваиваться и значение null, означающее, что данная строковая переменная не ссылается на строку (точнее— ни на что другое).
70 Часть I. Язык С# Объявление переменных и присваиванием им значений Вкратце напоминаем, что переменные объявляются просто путем указания их типа и имени: int age; Далее им с помощью операции = присваиваются значения: age = 25; Важно запомнить, что перед использованием переменные должны обязательно инициализироваться. Приведенная выше операция присваивания может применяться и в качестве операции инициализации. Существует еще и ряд других вещей, которые могут здесь делаться и которые часто можно встретить в коде С#. Первая заключается в одновременном объявлении сразу нескольких переменных одного и того же типа путем разделения их имен с помощью запятой, как показано ниже: int xSize, ySize; Здесь xSize и ySize одновременно объявляются как переменные типа int. Вторым приемом, который можно часто встретить в коде С#, является присваивание значений переменным сразу же при их объявлении, что фактически означает объединение двух строк кода: int age = 25; Оба этих приема могут применяться и вместе: int xSize = 4, ySize = 5; Здесь сразу объявляются две переменные — xSizenySize, которым сразу же присваиваются разные значения. Обратите внимание на то, что строка int xSize, ySize = 5; подразумевает инициализацию только переменной ySize, а переменная xSize лишь объявляется и по-прежнему требует инициализации, прежде чем ее можно будет использовать. Выражения Теперь, когда мы рассказали о том, как переменные можно объявлять и инициализировать, пришла пора переходить к рассмотрению способов работы с ними. В С# для этой цели предусмотрен целый ряд операций, в число которых входит и уже встречавшаяся операция присваивания (=). За счет объединения этих операций с переменными и литеральными значениями (которые в случае использования вместе с операциями называются операндами) и создаются выражения, которые являются главными строительными блоками процесса вычисления. Доступные операции варьируются от очень простых до очень сложных, часть из которых никогда не встречается за пределами математических приложений. К числу простых относятся все основные математические операции, вроде +, позволяющей складывать два операнда вместе; а к числу сложных — операции над содержимым переменных посредством его представления в двоичном формате. Также еще существуют логические операции, предназначенные специально для работы с булевскими значениями, и операции присваивания, вроде =.
Глава 3. Переменные и выражения 71 В настоящей главе основное внимание уделяется только математическим операциям и операциям присваивания. Логические операции будут более подробно рассматриваться в следующей главе, которая посвящена изучению булевской логики в контексте управления исполнением программы. Операции делятся на три категории: □ унарные, выполняемые на одним операндом; □ бинарные, выполняемые над двумя операндами; □ тернарные, выполняемые, соответственно, над тремя операндами. Большинство операций относится к категории бинарных операций, несколько — к категории унарных и лишь одна, называемая условной операцией (conditional operator) — к категории тернарных (эта условная операция является логической, и потому более подробно будет рассматриваться в главе 4). Давайте начнем с изучения математических операций, которые бывают как унарными, так и бинарными. Математические операции Всего существует пять простых математических операций, две из которых (+ и -) имеют как бинарную, так и унарную версию. Все они приведены в табл. 3.6 вместе с коротким примером их использования и результатом, который получается в случае их применения вместе с простыми числовыми типами (вроде целых чисел и чисел с плавающей запятой). Таблица 3.6. Простые математические операции Операция Категория Пример Результат Бинарная varl = var2 + var3; Бинарная varl = var2 - var3; Бинарная varl = var2 * var3; Бинарная varl = var2 / var3; Бинарная varl = var2 % var3; Унарная varl = +var2; Унарная varl = -var2; varl присваивается значение, представляющее собой результат сложения значений var2 и var3 varl присваивается значение, представляющее собой результат вычитания значения var3 из значения var2 varl присваивается значение, представляющее собой результат умножения значений var2 и var3 varl присваивается значение, представляющее собой результат деления значения var2 на значение var3 varl присваивается значение, представляющее собой остаток деления значения var2 на значение var3 varl присваивается значение var2 varl присваивается значение var2, умноженное на -1 Операция + (в унарной версии) является немного странной, потому что никак не влияет на результат. Она не вынуждает значения быть положительными, как могло показаться: если var2 содержит значение -1, тогда +var тоже будет равняться -1. Однако она является общеизвестной операцией, и потому как таковая и была включена в данную таблицу. О наиболее полезной особенности этой операции будет рассказано чуть позже в этой главе, при рассмотрении способов перегрузки операций.
72 Часть I. Язык С# В примерах применяются простые числовые типы, потому что в случае использования других простых типов результат может быть не ясен. Например, какого результата следует ожидать при складывании вместе двух булевских значений? Никакого, поскольку при попытке использовать операцию = (или любую другую математическую операцию) с переменными bool компилятор выдаст ошибку. Со складыванием переменных char дело тоже обстоит не совсем просто. Напомним, что переменные char на самом деле хранятся в виде чисел, поэтому складывание вместе двух переменных char будет приводить к получению числа (а точнее— целого (int) числа). Это является примером неявного преобразования, о котором (равно как и о явном преобразовании), будет рассказываться чуть позже в этой главе, потому что оно также имеет место и в тех случаях, когда varl, var2 и var3 содержат значения смешанных типов. В бинарной операции + действительно есть смысл, когда она применяется с переменными строкового типа. В таком случае она выглядит так, как показано в табл. 3.7. Таблица 3.7. Бинарная операция сложения Операция Категория Пример Результат Бинарная varl = var2 + var3; varl присваивается значение, представляющее собой результат соединения хранящихся в var2 и var3 строк Больше ни одна из других математических операций, однако, со строками не работает. Еще двумя операциями, с которыми осталось здесь познакомится, являются операции инкремента и декремента, которые представляют собой унарные операции и могут использоваться двумя способами: либо непосредственно перед, либо сразу же после операнда. Результаты, получаемые в случае применения этих операций в простых выражениях, показаны в табл. 3.8. Таблица 3.8. Операции инкремента и декремента Операция Категория Пример Результат Унарная varl =++var2; varl присваивается значение var2 + i. var2 увеличивается на 1 Унарная varl = ~var2; varl присваивается значение var2-i. var2 уменьшается на 1 Унарная varl = var2++; varl присваивается значение var2. var2 увеличивается на 1 Унарная varl = var2—; varl присваивается значение var2. var2 уменьшается на 1 Эти операции всегда приводят к изменению значения, хранящегося в их операнде: □ операция ++ всегда приводит к увеличению значения операнда на один; □ а операция — всегда приводит к уменьшению значения операнда на один. Отличия между результатами, сохраняемыми в varl, вытекают из того факта, что местоположение операции определяет то, когда она вступает в действие. В случае размещения одной из этих операций перед ее операндом, он будет обрабатываться перед выполнением любых других вычислений, а случае ее размещения после операнда, он будет обрабатываться после выполнения всех остальных вычислений в выражении.
Глава 3. Переменные и выражения 73 Это заслуживает еще одного примера. Рассмотрим следующий код: int varl, var2 = 5, var3 = 6; varl = var2++ * --var3; Какое значение будет присвоено переменной varl? Перед вычислением выражения в силу сначала вступит предшествующая переменной var3 операция —, которая изменит ее значение с 6 на 5. На операцию, следующую за переменной var2, можно не обращать внимания, поскольку она не вступит в силу до тех пор, пока не завершится вычисление выражения, а это значит, что переменной varl будет присвоено то значение, которое получится после умножения 5 на 5, т.е. 25. Эти простые унарные операции оказываются очень кстати в удивительно большом количестве ситуаций. Они, по сути, представляют собой сокращенную версию выражений вроде: varl = varl + 1; Выражения подобного рода имеют много применений, особенно в циклах, и об этом более подробно речь пойдет в главе 4. В следующем практическом занятии приводится пример использования математических операций, а также вводится несколько других полезных концепций. Код приглашает ввести строку и два числа и затем демонстрирует результаты выполнения над введенными числами ряда вычислений. прагшче^оезщят11е! Манипулирование переменными с помощью математических операций 1. Создайте новое консольное приложение по имени Ch03Ex02 и сохраните его в каталоге С:\BegVCSharp\Chapter03. 2. Добавьте в Program, cs следующий код: static void Main(string[] args) { double firstNumber, secondNumber; string userName; Console.WriteLine("Enter your name:") ; // Введите ваше имя userName = Console.ReadLine(); Console.WriteLine("Welcome {0}!", userName); // Добро пожаловать Console. WriteLine ("Now give me a number:") ; // Теперь введите число firstNumber = Convert. ToDouble (Console. ReadLine ()) ; Console. WriteLine ("Now give me another number:") ; // Теперь введите еще одно число secondNumber = Convert. ToDouble (Console. ReadLine ()) ; Console. WriteLine ("The sum of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber + secondNumber); // Результат сложения Console. WriteLine ("The result of subtracting {0} from {1} is {2}.", secondNumber, firstNumber, firstNumber - secondNumber); // Результат вычитания Console. WriteLine ("The product of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber * secondNumber); // Результат умножения Console. WriteLine ("The result of dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber / secondNumber); // Результат деления
74 Часть I. Язык С# Console.WriteLine("The remainder after dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber % secondNumber) ; // Остаток от деления Console.ReadKey(); } 3. Выполните добавленный код. На экране должно появиться то, что показано на рис. 3.2. Рис. 3.2. Выполнение приложения Ch03Ex02 4. Введите свое имя и нажмите клавишу <Enter>, как показано на рис. 3.3. Рис. 3.3. Ввод имени 5. Введите число и нажмите клавишу <Enter>, после чего введите еще одно число и снова нажмите <Enter>, как показано на рис. 3.4. I nie://.C:«eflVCSnefp/Ch«ptertK-Ch03Ex02/Ch03Ex02/lHn/Oeboe/Ch03Ex02.EXE L~—- Рис. 3.4. Ввод чисел Описание полученных результатов Помимо математических операций, в этом коде еще также впервые представляются два важных понятия, с которыми доведется сталкиваться довольно часто: □ пользовательский ввод; □ преобразование типов.
Глава 3. Переменные и выражения 75 Для пользовательского ввода применяется синтаксис, похожий ранее показанную команду Console. WriteLine (), а именно — Console. ReadLine (). Эта команда приглашает пользователя ввести данные, которые затем сохраняются в переменной типа string: string userName; Console.WriteLine("Enter your name:"); userName = Console.ReadLine (); Console.WriteLine("Welcome {0}!", userName); Приведенный код выводит содержимое присвоенной переменной userName прямо на экран. В этом примере еще также выполняется и считывание двух чисел. Данный процесс является немного более сложным, поскольку команда Console. Re a dLine() генерирует строку, а требуется число. Здесь в игру вступает механизм преобразования типов. Более подробно о нем будет рассказываться в главе 5, а пока давайте просто проанализируем код, используемый для его обеспечения в данном примере. Сначала объявляются переменные, в которых должны сохраняться вводимые числа: double firstNumber, secondNumber; Далее обеспечивается отображение соответствующего приглашения, и в отношении получаемой Console. ReadLine () строки применяется команда Convert. ToDouble () для преобразования ее в число double. Это число затем присваивается объявленной ранее переменной firstNumber: Console.WriteLine("Now give me a number:"); firstNumber = Convert.ToDouble(Console.ReadLine ()); Показанный здесь синтаксис является удивительно простым и позволяет выполнять похожим образом и много других преобразований. В остальной части кода аналогичным образом получается второе число: Console.WriteLine("Now give me another number:"); secondNumber = Convert.ToDouble(Console.ReadLine ()); После этого производится вывод на экран результатов сложения, вычитания, умножения и деления двух полученных чисел, а также отображение остатка от деления, получаемого с помощью операции %: Console.WriteLine("The sum of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber + secondNumber); Console.WriteLine("The result of subtracting {0} from {1} is {2}.", secondNumber, firstNumber, firstNumber - secondNumber); Console.WriteLine("The product of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber * secondNumber); Console.WriteLine("The result of dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber / secondNumber); Console.WriteLine("The remainder after dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber % secondNumber); Обратите внимание, что выражения, вроде firstNumber + secondNumber и т.д., передаются оператору Console .WriteLine () напрямую в виде параметра, без использования промежуточной переменной: Console.WriteLine("The sum of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber + secondNumber); Применение синтаксиса подобного рода может делать код очень удобочитаемым и сокращать количество подлежащих написанию строк кода.
76 Часть I. Язык С# Операции присваивания Пока что в этой главе встречалась только простая операция присваивания (=) и то, что доступны и другие операции присваивания, вообще может стать сюрпризом. Однако другие операции присваивания действительно существуют и, что даже может, пожалуй, стать еще большим сюрпризом, являются довольно полезными! Все остальные операции присваивания, кроме операции =, работают подобным образом. Как и операция =, они все приводят к присваиванию значения той переменной, которая находится в их левой части, на основании операндов и операций, находящихся в их правой части. Увидеть, что это за операции можно в табл. 3.9. Таблица 3.9. Другие операции присваивания Операция Категория Пример Результат Бинарная varl = var2; Бинарная varl += var2; Бинарная varl -= var2; /= Бинарная varl *= var2; Бинарная varl /= var2; Бинарная varl %= var2; varl присваивается значение var2 varl присваивается значение, получающееся в результате сложения varl и var2 varl присваивается значение, получающееся в результате вычитания значения var2 из значения varl varl присваивается значение, получающееся в результате умножения varl на var2 varl присваивается значение, получающееся в результате деления varl на var2 varl присваивается значение, получающееся в качестве остатка после деления varl на var2 В этой таблице видно, что все дополнительные операции приводят к включению varl в производимые расчеты, а это значит, что строка кода вроде varl += var2; будет иметь точно такой же результат, как и строка кода varl = varl + var2; Операция += может еще также использоваться и со строками, точно так же, как операция +. Применение этих операций, особенно в случае длинных имен переменных, может делать код гораздо более удобочитаемым. Приоритеты операций При вычислении выражения каждая операция обрабатывается по очереди, что вовсе не обязательно означает, что вычисление этих операций происходит строго в порядке слева направо. В качестве типичного примера, возьмем следующее выражение: varl = var2 + var3; Здесь операция + будет вступать в силу раньше, чем =. Однако бывают ситуации, когда приоритеты выполнения операций и не является столь очевидными, как, например, в следующем выражении: varl = var2 + var3 * var4;
Глава 3. Переменные и выражения 77 В этом выражении первой будет выполняться операция *, за ней — операция + и только потом — операция =. Такой порядок выполнения соответствует тому, который применяется в математике, и обеспечивает получение точно такого же результата, как и в случае выполнения аналогичного алгебраического подсчета вручную. Использование круглых скобок, как и в математике, позволяет управлять порядком выполнения операций; например, рассмотрим следующее выражение: varl = (var2 + var3) * var4; Здесь первым будет вычисляться содержимое, заключенное в круглые скобки, т.е. операция + будет выполняться перед операцией *. В табл. 3.10 показано, как выглядят приоритеты операций, которые уже встречались в этой главе, причем если приоритеты совпадают (как, например, у операций * и /), тогда операции вычисляются просто в порядке слева направо. Таблица 3.10. Приоритеты операций Приоритет Операции Высший ++, — (префиксные формы); +, - (унарная) *, /, % + , - — •— /— 9-— 4-— - — Низший ++, — (постфиксные формы) Для переопределения стандартных приоритетов можно пользоваться круглыми скобками, как уже рассказывалось выше. Кроме того, следует обратить внимание, что операции ++ и — в постфиксной форме имеют, как видно в таблице, низшие приоритеты только с концептуальной точки зрения. Они не выполняются над результатом, скажем, выражения присваивания, поэтому можно считать, что они обладают более высоким приоритетом, чем все остальные операции. Однако из-за того, что они меняют значение своего операнда после вычисления выражения, легче считать, что их приоритеты выглядят так, как указано в табл. 3.10. Пространства имен Прежде чем продолжить, не помешает рассмотреть еще одну важную тему, а именно— пространства имен (namespace). Они являются в .NET своего рода способом предоставления контейнеров для кода приложения, позволяющих идентифицировать этот код и его содержимое уникальным образом. Еще они применяются и в качестве средства категоризации элементов в .NET Framework. Большую часть этих элементов составляют определения типов, наподобие определений тех простых типов, которые демонстрировались в настоящей главе (System. Int32 и т.д.). Код на языке С# по умолчанию содержится в глобальном пространстве имен. Это означает, что к содержащимся в этом коде элементам можно получать доступ из любого другого находящегося в глобальном пространстве имен кода просто путем указания их имени. Можно использовать ключевое слово namespace и явным образом определять пространство имен для блока кода, заключаемого в фигурные скобки. Имена из такого пространства должны обязательно квалифицироваться в случае их применения в коде, находящемся за пределами этого пространства имен.
78 Часть I. Язык С# Квалифицированное имя — это такое имя, в котором содержится вся иерархическая информация. По сути, это значит, что при наличии кода в одном пространстве имен и необходимости использовать в нем имя, определенное в другом пространстве имен, нужно обязательно включать вместе с именем и ссылку на другое пространство имен. Символ точки (.) в квалифицированных именах служит для обозначения различных имеющихся в пространстве имен уровней, например: namespace LevelOne { // код в пространстве имен LevelOne // определение имени "NameOne" } // код в глобальном пространстве имен В этом коде определяется одно пространство имен — LevelOne — и одно имя в этом пространстве имен — NameOne (сам код не приводится для придания описанию общего характера, но зато приводится комментарий на том месте, где должно находиться определение). В коде, добавляемом внутри пространства имен LevelOne, на это имя можно ссылаться с использованием NameOne, т.е. квалифицировать его не обязательно. В коде, добавляемом внутри глобального пространства имен, однако, на него обязательно нужно ссылаться с помощью квалифицированного имени LevelOne.NameOne. Внутри любого пространства имен с использованием ключевого слова namespace можно определять и другие пространства имен, называемые в таком случае вложенными пространствами имен. Ссылаться на вложенные пространства имен нужно с применением информации об их иерархии и отделения каждого уровня иерархии символом точки. Лучше всего будет показать это на примере. Возьмем следующий код: namespace LevelOne { // код в пространстве имен LevelOne namespace LevelTwo { // код в пространстве имен LevelOne.LevelTwo // определение имени "NameTwo" } } // код в глобальном пространстве имен Здесь на имя NameTwo нужно ссылаться в глобальном пространстве имен как LevelOne.LevelTwo.NameTwo, в пространстве имен LevelOne— LevelTwo.NameTwo, a в пространстве имен LevelOne. LevelTwo — NameTwo. Главным тут является то, что пространства имен идентифицируют определяемые в них имена уникальным образом. Например, имя NameThree можно определить и в пространстве имен LevelOne и в пространстве имен LevelTwo: namespace LevelOne { // определение имени "NameThree" namespace LevelTwo { // определение имени "NameThree" } } Это приведет к получению двух отдельных имен LevelOne .NameThree и LevelOne. LevelTwo.NameThree, которые можно будет использовать совершенно независимо друг от друга.
Глава 3. Переменные и выражения 79 После создания пространств имен для упрощения доступа к содержащимся в них именам можно применять оператор using. С помощью этого оператора, по сути, заявляется следующее: "Далее будут нужны имена из этого пространства имен, поэтому нет желания постоянно уточнять их". Например, следующий код указывает, что у кода в пространстве имен LevelOne должна быть возможность получать доступ к именам из пространства имен LevelOne. LevelTwo без уточнения: namespace LevelOne { using LevelTwo; namespace LevelTwo { // определение имени "NameTwo" } } Благодаря этому, в коде пространства имен LevelOne ссылаться на LevelTwo. NameTwo можно будет с использованием просто NameTwo. В некоторых случаях, однако, как и в приведенном выше примере с NameThree, такой подход может привести к появлению проблем из-за конфликтов между идентичными именами в разных пространствах имен (т.е. при использовании такого имени код не будет компилироваться — и компилятор будет выдавать сообщение, информирующее об отсутствии однозначности). В подобных случаях в операторе using можно указывать псевдоним (alias) пространства имен: namespace LevelOne { using LT = LevelTwo; // определение имени "NameThree" namespace LevelTwo { // определение имени "NameThree" } } Благодаря этому, в коде пространства имен LevelOne теперь на LevelOne .NameThree можно будет ссылаться как NameThree, а на LevelOne . LevelTwo .NameThree — LT.NameThree. Действие операторов using распространяется как на те пространства имен, в которых они содержатся, так и на любые вложенные пространства имен, которые также могут содержаться в этих пространствах имен. Приведенный выше код не позволяет применять в глобальном пространстве имен LT.NameThree. Однако если бы оператор using в нем был объявлен следующим образом: using LT = LevelOne.LevelTwo; namespace LevelOne { // определение имени "NameThree" namespace LevelTwo { // определение имени "NameThree" } } тогда в коде глобального пространства имен и пространства имен LevelOne можно было бы использовать LT.NameThree.
80 Часть I. Язык С# Обратите внимание на еще один важный момент: сам по себе оператор using не предоставляет доступа к именам в другом пространстве имен. Если только код в пространстве имен не будет каким-то образом связан с проектом, например, его определением в файле исходного кода проекта или в другом связанном с проектом коде, доступа к содержащимся в нем именам не будет. Кроме того, в случае связывания содержащего пространство имен кода с проектом, доступ к его именам будет открыт вне зависимости от того, используется оператор using или нет. Оператор using просто упрощает получение доступа к этим именам и может сократить количество вводимого кода, делая его более удобным для чтения. Теперь вернемся к приведенному в начале этой главы коду приложения ConsoleApplicationl с тремя следующими касающимися пространств имен строками: using System; using System.Collections.Generic; using System.Text; namespace ConsoleApplicationl { } Здесь три строки, начинающиеся с ключевого слова using, применяются для объявления того, что в последующем коде С# будут использоваться пространства имен System, System.Collections .Generic, System.Text и что к ним нужно обращаться из всех пространств имен в данном файле без квалификации. Пространство имен System является корневым для приложений .NET Framework и содержит все базовые функциональные возможности, которые могут быть необходимы для создания консольных приложений. Два других пространства имен очень часто используются в консольных приложениях и потому указаны здесь просто так, на всякий случай. И, напоследок, в четвертой строке, начинающейся с ключевого слова using, объявляется пространство имен под названием ConsoleApplicationl, предназначенное для кода самого приложения. Резюме В этой главе был предоставлен большой объем основополагающего материала, необходимого для создания практичных простых приложений на С#. В частности, был продемонстрирован базовый синтаксис языка С#, а также проведен анализ кода простого консольного приложения, который VS и VCE генерируют автоматически при создании соответствующего проекта. Большая часть этой главы была посвящена рассмотрению переменных и способов работы с ними. Здесь было показано и то, что собой вообще представляют переменные, и то, как их можно создавать, а также то, как им можно присваивать значения и работать с ними и значениями, которые они содержат. По ходу еще также были продемонстрированы и некоторые основные приемы взаимодействия с пользователем, показавшие, как выводить текст в консольном приложении и как считывать вводимые пользователем данные. Не обошлось и без иллюстрации элементарных приемов преобразования типов, являющихся сложной темой, которая будет более подробно рассматриваться в главе 5. Кроме того, в этой главе было показано, как собирать операции и операнды в выражения, а также, каким образом и в каком порядке они выполняются. И, наконец, в завершение главы было рассказано о том, что собой представляют пространства имен, которые являются очень важными для изучения дальнейшего ма-
Глава 3. Переменные и выражения 81 териала в настоящей книге. Эта тема была пока освещена лишь в общих чертах, однако это обеспечило основу для последующих обсуждений. В этой главе рассмотрены следующие вопросы. □ Как выглядит базовый синтаксис языка С#. □ Что делает Visual Studio, когда создается новый проект консольного приложения. □ Что собой представляют переменные и как ими пользоваться. □ Что собой представляют выражения и как их применять. □ Что собой представляют пространства имен. Пока что все программирование принимало форму построчного выполнения кода. В следующей главе будет показано, как делать код более эффективным за счет управления его выполнением с помощью циклов и операторов условных переходов. Упражнения 1. Как в следующем коде можно было бы сослаться на имя great из кода в пространстве имен fabulous? namespace fabulous { // код в пространстве имен fabulous } namespace super { namespace smashing { // определение имени great } } 2. Какое из следующих значений не является допустимым именем для переменной? • myVariablelsGood • 99Flake • _floor • time2GetJiggyWidIt • wrox.com 3. Является ли строка "supercalif ragilisticexpialidocious" слишком большой для того, чтобы уместиться в переменной string? Если да, то почему? 4. Учитывая приоритеты операций, перечислите шаги вычисления следующего выражения: resultVar += varl * var2 + var3 % var4 / var5; 5. Напишите консольное приложение, способное получать от пользователя четыре значения int и на их основании отображать название продукта. Подсказка: напоминаем о том, что для преобразования поступающего из консоли ввода в double применялась команда Convert. ToDouble (); эквивалентная команда для преобразования ввода из string в int выглядит как Convert .ToInt32 ().
Управление потоком выполнения У всех приводимых до сих пор примеров кода С# имелась одна общая черта. В каждом случае выполнение программы переходило от одной строки к следующей в порядке сверху вниз, без всяких пропусков. Если бы все приложения работали подобным образом, тогда разработчики были бы очень ограничены в своих возможностях. В этой главе описываются две таких методики для управления потоком выполнения программы (т.е. порядком выполнения строк кода С#), как ветвление (branching) и организация циклов (looping). Первая позволяет обеспечивать выполнение кода на основании условия, т.е. в зависимости от результата вычисления этого условия, которое может требовать, к примеру, выполнять код только в том случае, если значение переменной myVal меньше 10. Вторая методика обеспечивает повторное выполнение одних и тех же операторов (либо определенное количество раз, либо до тех пор, пока не будет соблюдено проверочное условие). Обе этих методики подразумевают применение булевской логики. В предыдущей главе тип bool встречался, но почти никак не использовался. В этой главе он будет использоваться очень много, поэтому глава начинается с описания того, что собой представляет булевская логика, чтобы в последующих примерах управления исполнением ее применение не вызывало никаких вопросов. В частности, в настоящей главе рассматриваются следующие основные темы. □ Булевская логика и способы ее применения. □ Способы управления исполнением кода. Булевская логика Тип bool, о котором уже вкратце упоминалось в предыдущей главе, может содержать только одно из следующих двух значений: true или false. Этот тип часто применяется для фиксирования результата какой-нибудь операции, чтобы в зависимости
Глава 4. Управление потоком выполнения 83 от этого результата далее могло выполняться то или иное действие. В частности, тип bool часто используется для сохранения результата такой операции, как сравнение. Историческая справка: источником происхождения булевской логики являются труды английского математика Джорджа Буля (George Boole), жившего в середине девятнадцатого столетия. Например, предположим (как уже предлагалось во вводной части данной главы), что требуется сделать так, чтобы код выполнялся только в том случае, если значение переменной myVal меньше 10. Это требует каким-то образом узнать о том, ложным или истинным является утверждение "myVal меньше 10й, т.е. получить булевский результат сравнения. Для осуществления булевских сравнений необходимо использовать булевские операции сравнения, которые еще также называются операциями отношения и перечислены в табл. 4.1. В этой таблице во всех случаях предполагается, что varl является переменной типа bool, в то время как типы переменных var2 и var3 могут варьироваться. Таблица 4.1. Операции сравнения Операция Категория Пример Результат Бинарная varl = var2 == var3; Бинарная varl = var2 != var3; Бинарная varl = var2 < var3; Бинарная varl = var2 > var3; Бинарная varl = var2 <= var3; Бинарная varl = var2 >= var3; varl присваивается значение true, если var2 равна var3, в противном случае varl присваивается значение false varl присваивается значение true, если var2 не равна var3, в противном случае varl присваивается значение false varl присваивается значение true, если var2 меньше var3, в противном случае varl присваивается значение false varl присваивается значение true, если var2 больше var3, в противном случае varl присваивается значение false varl присваивается значение true, если var2 меньше или равна var3, в противном случае varl присваивается значение false varl присваивается значение true, если var2 больше или равна var3, в противном случае varl присваивается значение false Перечисленные операции могут применяться в коде в отношении числовых значений, например: bool isLessThanlO; isLessThanlO = myVal < 10; Эта строка кода будет приводить к присваиванию isLessThanlO значения true в том случае, если в переменной хранится значение меньше 10, и значения false в противном случае. Эти операции также могут применяться и в отношении других типов, например, строк: bool isKarli; isKarli = myString == "Karli";
84 Часть I. Язык С# Здесь isKarli будет присваиваться значение true только в том случае, если в переменной myString хранится строка "Karli". Операции сравнения могут использоваться и в отношении исключительно булевских значений, например: bool isTrue; isTrue = myBool == true; В таком случае, однако, применять разрешено только операции == и ! =. Типичная ошибка в коде часто возникает, когда разработчик неумышленно предполагает, что если vail < val2 возвращает false, значит, vail > val2 будет возвращать true. Если vail == val2, тогда false будут возвращать оба этих оператора. Существует еще несколько других булевских операций, которые предназначены специально для работы с только булевскими значениями. Все эти операции перечислены в табл. 4.2. Таблица 4.2. Операции, предназначенные специально для работы с булевскими значениями Операция Категория Пример Результат Унарная varl = !var2; Бинарная varl = var2 & var3; Бинарная varl = var2 | var3; Бинарная varl = var2 A var3; varl присваивается значение true, если var2 равна false, или значение false, если var2 равна true. (Логическое НЕ.) varl присваивается значение true, если и var2, и var3 равны true; в противном случае varl присваивается значение false. (Логическое И.) varl присваивается значение true, если либо var2, либо var3 (либо и var2 и var3) равна true; в противном случае varl присваивается значение false. (Логическое ИЛИ.) varl присваивается значение true, если либо только var2, либо только var3 (т.е. не и var2 и var3 одновременно) равна true; в противном случае varl присваивается значение false. (Логическое исключающее ИЛИ, либо просто исключающее ИЛИ.) Следовательно, получается, что приведенный выше фрагмент кода может быть представлен и так: bool isTrue; isTrue = myBool & true; У операций & и | еще имеются две похожие операции, которые называются условными булевскими операциями и показаны в табл. 4.3. Результат этих операций выглядит точно так же, как и у операций & и |, но их важным отличием является способ, по которому этот результат получается, поскольку он может обеспечивать более высокую производительность. Обе они смотрят на значение своего первого операнда (var2 в табл. 4.3) и на его основании могут вообще не переходить к обработке второго операнда (var3 в табл. 4.3).
Глава 4. Управление потоком выполнения 85 Таблица 4.3. Условные булевские операции Операция Категория Пример Результат && Бинарная varl = var2 && var3; varl присваивается значение true, если и var2, и var3 равны true, в противном случае varl присваивается значение false. (Логическое И.) И Бинарная varl = var2 || var3; varl присваивается значение true, если либо var2, либо var3 (или var2 и var3) равна true, в противном случае varl присваивается значение false. (Логическое ИЛИ.) Если значением первого операнда в операции && является false, тогда брать в расчет значение второго операнда не имеет смысла, поскольку результатом все равно будет false. Точно так же и операция | | будет всегда возвращать true, если значением первого операнда является true, каким бы ни было значение второго операнда. В случае показанных ранее операций & и | подобного происходить не будет. При их использовании будут всегда вычисляться оба операнда. Благодаря такому условному вычислению операндов, применение операций && и | | вместо & и | позволяет обеспечить более высокую производительность. Это особенно заметно в тех приложениях, в которых операции используются очень часто. Поэтому операции & & и | | рекомендуется применять вместо & и | всегда и везде, где это возможно. Эти операции действительно оказываются очень кстати в более сложных ситуациях, когда вычисление второго операнда является возможным только при определенных значениях первого операнда, как, например, в показанном ниже примере: varl = (var2 != 0) && (var3 / var2 > 2); Здесь в случае если var2 равна 0 деление var3 на var2 будет приводить либо к возникновению ошибки из-за невозможности выполнения деления на ноль, либо к определению переменной varl как содержащей бесконечное значение (которое является допустимым и поддается обнаружению в случае некоторых типов, например, float). Поразрядные операции К этому моменту вполне может возникнуть вопрос, а зачем вообще нужны операции & и | ? Все дело в том, что они могут использоваться для выполнения операций над числовыми значениями, ведь их действие распространяется скорее на ряд хранящихся в переменной битов (разрядов), чем на само ее значение. Давайте рассмотрим их по очереди, начиная с операции &. Каждый бит в первом операнде сравнивается с находящимся в такой же позиции битом во втором операнде, после чего соответствующему биту результата присваивается значение из табл. 4.4. Таблица 4.4. Таблица истинности операции & Бит в первом операнде Бит во втором операнде Бит результата & 1 1 1 1 0 0 О 1 0 0 0 0
86 Часть I. Язык С# В операции | все происходит подобным образом, но только результирующие биты выглядят по-другому, как показано в табл. 4.5. Таблица 4.5. Таблица истинности операции | Бит в первом операнде Бит во втором операнде Бит результата I 1 1 1 1 0 1 О 1 1 о о о Например, рассмотрим показанную ниже операцию: int result, opl, op2; opl = 4; ор2 = 5; result = opl & op2; В этом случае требуется взять двоичные представления opl и ор2, которые выглядят, соответственно, как 100 и 101. Результат можно получить путем сравнения двоичных цифр, находящихся в этих двух представлениях в эквивалентных позициях, описанным ниже образом. □ Если крайний левый бит и в opl и в ор2 равен 1, тогда крайний левый бит в result тоже будет равен 1, а если нет, тогда он будет равен 0. □ Если следующий бит и в opl и в ор2 равен 1, тогда следующий бит в result тоже будет равен 1, а если нет, тогда он будет равен 0. □ Сравнить таким же образом и все остальные биты. В данном примере крайние левые биты у opl и op2 равны 1, поэтому у result крайний левый бит тоже будет равен 1. Вторые биты у обоих равны 0, а третьи — соответственно, 1 и 0, поэтому второй и третий биты у result будут равны 0. Следовательно, конечное значение result в двоичном представлении будет выглядеть как 100, т.е. result получает значение 4. Весь этот процесс графически представлен ниже. 10 0 4 & 1 0 1 & 5 10 0 4 Тот же самый процесс будет происходить и в случае использования операции |, но только результирующий бит будет равен 1 всякий раз, когда хоть один из битов, находящихся в такой же позиции в операндах, будет равен 1, как показано далее. 10 0 4 | 1 0 1 J 5_ 10 1 5 Подобным образом еще можно использовать и операцию А, в случае которой результирующий бит будет равен 1, если либо один, либо другой из битов (но не оба сразу), находящихся в той же позиции в операндах, равен 1, как показано в табл. 4.6.
Глава 4. Управление потоком выполнения 87 Таблица 4.6. Таблица истинности операции А Бит в первом операнде Бит во втором операнде Бит результата А 1 1 О 1 0 1 О 1 1 о о о В языке С# также предусмотрена и возможность использования унарной поразрядной операции (~), действие которой заключается в инвертировании каждого из битов операнда, в результате чего получается переменная со значениями 1 для тех битов, которые в операнде равны 0, и значениями 0 для тех битов, которые в операнде равны 1 (табл. 4.7). Таблица 4.7. Таблица истинности операции ~ Бит в операнде Бит результата ~ 1 О О 1 Способ, которым числа типа int сохраняются в .NET, также называемый дополнительным двоичным кодом, означает, что унарная операция ~ может приводить к получению результатов, выглядящих несколько странно. Если вспомнить, что тип int представляет собой состоящее из 32 битов число, например, тогда знание того, каким образом операция - воздействует на все эти 32 бита, может помочь увидеть, что же происходит. Например, число 5 в своем полном двоичном представлении будет выглядеть следующим образом: 000000000000000000000000000000101 А число -5 — так: 111111111111111111111111111111011 Дело в том, что в дополнительном двоичном коде (-х) на самом деле определяется как (~х + 1). Это может показаться странным, но такая система является очень удобной, когда дело доходит до сложения чисел. Например, сложение 10 и -5 (т.е. вычитание 5 из 10) в двоичном формате будет выглядеть следующим образом: 000000000000000000000000000001010 + 111111111111111111111111111111011 = 1000000000000000000000000000000101 Если не обращать внимания на 1 с самого края слева, тогда остается двоичное представление числа 5. Так что, хотя результаты вроде ~1 = -2 и могут выглядеть странно, их такими делают лежащие в основе структуры. Поразрядные операции, которые были продемонстрированы в этом разделе, являются довольно полезными в определенных ситуациях, поскольку позволяют легко использовать отдельные биты переменной для хранения нужной информации. Рассмотрим простой пример представления цвета с использованием трех битов для сохранения содержимого red, green и blue. Эти биты можно задать отдельно и сделать так, чтобы они предусматривали такие конфигурации, которые показаны в табл. 4.8.
88 Часть I. Язык С# Таблица 4.8. Использование битов для сохранения цветов Биты 000 100 010 001 101 110 011 111 Десятичное представление 0 4 2 1 5 6 3 7 Значение Черный (black) Красный (red) Зеленый (green) Голубой (blue) Пурпурный (magenta) Желтый (yellow) Светло-голубой (cyan) Белый (white) Предположим, что все эти значения сохраняются в переменной типа int. Тогда, начиная с черного цвета, т.е. переменной int со значением 0, над ними могут выполняться следующие операции: int myColor = 0; bool containsRed; myColor = myColor 2; myColor = myColor | 4; containsRed = (myColor & 4) == 4; // Добавление бита, представляющего зеленый // цвет, в результате которого // в myColor теперь сохраняется значение 010 // Добавление бита, представляющего красный // цвет, в результате которого в myColor // теперь сохраняется значение 110 // Проверка значения бита, представляющего // красный цвет В последней строке приведенного кода containsRed будет присваиваться значение true, поскольку бит, представляющий красный цвет в myColor, действительно равен 1. Такой прием может оказаться довольно полезным для эффективного использования информации, поскольку задействованные операции могут применяться для проверки значений сразу множества битов одновременно (всех 32 битов в случае значений типа int). Однако существуют и более удобные способы для хранения дополнительной информации в одиночных переменных (подразумевающих использование дополнительных типов переменных, о которых более подробно будет рассказываться позже в этой главе). Помимо четырех продемонстрированных поразрядных операций, в настоящем разделе рассматриваются еще две других, которые перечислены в табл. 4.9. Таблица 4.9. Поразрядные операции » и « Операция Категория Пример выражения Результат Бинарная Бинарная varl = var2 >> var3; varl = var2 << var3; varl присваивается значение, получаемое после сдвига вправо двоичного содержимого var2 на указанное в var3 количество разрядов varl присваивается значение, получаемое после сдвига влево двоичного содержимого var2 на указанное в var3 количество разрядов
Глава 4. Управление потоком выполнения 89 Эти операции, которые еще также часто называют поразрядными операциями сдвига, лучше всего проиллюстрировать на коротком примере: int varl, var2 = 10, var3 = 2; varl = var2 << var3; Здесь переменной varl будет присвоено значение 4 0. Объяснить это можно, вспомнив, что в двоичном представлении 10 выглядит как 1010, но после сдвига на две позиции влево оно превратится в 101000, что уже является двоичным представлением числа 40. По сути, получается, что будет выполняться операция умножения. Каждый сдвиг влево на один бит приводит к умножению на 2, а это значит, что сдвиг на два бита влево вызовет умножение на 4. Аналогично сдвиг на один бит вправо будет приводить к делению операнда на 2, причем остаток этого деления будет отбрасываться: int varl, var2 = 10; varl = var2 >> 1; В данном примере в переменной varl будет содержаться значение 5, а после выполнения следующего кода она получит значение 2: int varl, var2 = 10; varl = var2 » 2; Эти операции вряд ли нужно будет часто использовать в коде, но знать об их существовании не помешает. Их главной сферой применения является высоко оптимизированный код, в котором накладные расходы других математических операций просто не допустимы. Из-за этого они часто применяются, к примеру, в коде драйверов устройств или в коде системы. Булевские операции присваивания Последними в этом разделе рассматриваются операции, позволяющие объединять некоторые из рассмотренных ранее операций с помощью знака присваивания, во многом подобно математическим операциям присваивания (вроде +=, *= и т.д.), о которых рассказывалось в предыдущей главе. Все эти операции перечислены в табл. 4.10. Эти операции работают с булевскими и числовыми значениям тем же образом, что операции &, | и А. Обратите внимание, что операции &= и \ = подразумевают использование операций & и \, а не && и | |, и на то, что ассоциируемые с ними накладные расходы выглядят точно так же, как и у этих более простых операций. Таблица 4.10. Булевские операции присваивания Операция Категория Пример выражения Результат &= Бинарная varl &= var2; l= Бинарная varl |= var2; Бинарная varl A= var2; varl присваивается значение, получаемое в результате выполнения операции varl & var2 varl присваивается значение, получаемое в результате выполнения операции varl | var2 varl присваивается значение, получаемое в результате выполнения операции varl A var2
90 Часть I. Язык С# У поразрядных операций сдвига тоже имеются операции присваивания, которые перечислены в табл. 4.11. Таблица 4.11. Операции присваивания для поразрядных операций сдвига Операция Категория Пример выражения Результат Унарная Унарная vari »= var2; Vari присваивается значение, получаемое после сдвига вправо двоичного содержимого vari на указанное в var2 количество разрядов vari «= var2; Vari присваивается значение, получаемое после сдвига влево двоичного содержимого vari на указанное в var2 количество разрядов Теперь подошла пора примера. В следующем практическом занятии показан код, приглашающий ввести целое число и затем выполняющий над этим числом различные булевские вычисления. Практическое заня Применение булевских и поразрядных операций 1. Создайте новое консольное приложение по имени Ch04Ex01 и сохраните его в каталоге C:\BegVCSharp\Chapter04. 2. Добавьте в файл Program.cs следующий код: static void Main(string[] args) { Console.WriteLme("Enter an integer:"); // Введите целое число int mylnt = Convert.ToInt32 (Console.ReadLine()); Console.WriteLine("Integer less than 10? {0}", mylnt < 10); // Это целое число меньше 10? Console.WriteLine("Integer between 0 and 5? {0}", @ <= mylnt) && (mylnt <= 5) ) ; // Это целое число находится в диапазоне между 0 and 5? Console.WriteLine ("Bitwise AND of Integer and 10 = {0}", mylnt & 10); // Результат выполнения поразрядной операции И // для указанного целого числа и 10 Console.ReadKey(); } 3. Запустите это приложение, и после появления соответствующего приглашения введите какое-нибудь целое число. На рис. 4.1 показан результат, который должен получиться. Рис. 4.1. Результат выполнения приложения Ch04Ex01
Глава 4. Управление потоком выполнения 91 Описание полученных результатов В первых двух строках кода отображается приглашение на ввод целочисленного значения и его считывание с помощью приемов, которые уже встречались и объяснялись ранее: Console. WriteLine ("Enter an integer:"); int mylnt = Convert.ToInt32(Console.ReadLine()); Для получения целого числа из введенной строки используется команда Convert. То Int 32 (), которая является просто еще одной командой из того же семейства, что и уже демонстрировавшаяся ранее Convert. ToDouble (). В трех остальных строках кода выполняются различные операции над полученным числом и отображаются их результаты. В данном случае предполагается, что пользователь ввел число 6 (см. рис. 4.1). Первым выводится результат выполнения операции mylnt < 10. Если в mylnt содержится значение 6, которое меньше 10, тогда в качестве результата выводится true, которое и видно на приведенном снимке экрана. В случае если mylnt имеет значение 10 или выше, в качестве результата выводится false. Вторым выводится результат такого более сложного выражения, как @ <= mylnt) && (mylnt <= 5) Это выражение подразумевает выполнение двух операций сравнения (для выяснения того, является ли значение mylnt большим и равным нулю и меньшим или равным 5), а также выполнение над полученными после них результатами булевской операции И. В случае значения б операция @ <= mylnt) возвращает true, а операция (mylnt <= 5) — false. В результате этого получается выражение (true) && (false), которое в итоге дает значение false, что опять-таки видно на приведенном снимке экрана. Напоследок над значением mylnt выполняется поразрядная операция И. В роли второго операнда в этой операции выступает 10, которое в двоичном представлении выглядит как 1010. Если значением mylnt является число б, двоичное представление которого — 110, тогда в результате этой операции возвращается значение 10, которое в десятичном представлении соответствует числу 2, как показано ниже. 0 110 6 & 1 0 1 0 & 10 0 0 10 2 Дополнение таблицы приоритетов операций Теперь, когда были рассмотрены еще несколько дополнительных операций, пришла пора дополнить приводившуюся в предыдущей главе таблицу приоритетов операций. Теперь она должна выглядеть так, как показано в табл. 4.12. Эта таблица содержит немало новых уровней, но зато позволяет четко уяснить, каким образом будут вычисляться выражения вроде того, что показано ниже и в котором операция && обрабатывается после операций <= и >=: varl = var2 <= 4 && var2 >= 2; Обратите внимание, что для внесения большей ясности вовсе не помешает добавлять в выражения, подобные этому, круглые скобки. Разумеется, компилятору известно, в каком порядке нужно обрабатывать операции, но люди склонны забывать эти вещи. Написание приведенного выше выражения следующим образом: varl = (var2 <= 4) && (var2 >= 2); устранит такую проблему за счет явного указания порядка вычислений.
92 Часть I. Язык С# Таблица 4.12. Дополненная таблица приоритетов операций Приоритет Операции Высший ++, -- (префиксные формы); (),+,- (унарная); *, /, % +, - «, » <, >, <=, >= & Низший && I I =, *=, /=, %=, +=, -=, «=, »=, &= ++, ~ (постфиксные формы) Оператор goto Язык С# позволяет снабжать строки кода метками и затем переходить сразу же к ним с помощью оператора goto. У такого подхода имеются как преимущества, так и недостатки. Главным его преимуществом является то, что он предоставляет простой способ для управления тем, какой код должен выполняться и когда, а главным недостатком — то, что чрезмерное применение оператора goto может делать код запутанным и трудным для восприятия. Оператор goto используется следующим образом: goto <имя_метки>; Метки определяются следующим образом: <имя_метки>: Например, рассмотрим такой код: int mylnteger = 5; goto myLabel; mylnteger += 10; myLabel: Console.WriteLine("mylnteger = {0}", mylnteger); Выполнение этого кода будет происходить так, как описано ниже. □ Сначала объявляется переменная mylnteger с типом int, которой затем присваивается значение 5. □ Далее оператор goto прерывает обычный ход выполнения кода и передает управление строке, помеченной myLabel:. □ После этого сразу же осуществляется вывод в окно консоли значения mylnteger.
Глава 4. Управление потоком выполнения 93 Таким образом, получается, что в этом коде никогда не будет выполняться следующая строка, выделенная для большей ясности: int mylnteger = 5; goto myLabel; mylnteger += 10; myLabel: Console.WriteLine("mylnteger = {0}", mylnteger); На самом деле, после добавления данного кода в приложение и попытки скомпилировать его, в окне Error List (Список ошибок) появится предупреждением с соответствующим заголовком "Unreachable code detected" (Обнаружен недостижимый код) и деталями о местонахождении проблемного кода. Кроме того, под недостижимой строкой кода будет также отображаться и волнистая линия зеленого цвета. Операторы goto имеют свои случаи применения, но на самом деле могут сильно все запутать. Ниже показан пример такого запутанного кода: start: int mylnteger = 5; goto addVal; writeResult: Console.WriteLine("mylnteger = {0}", mylnteger); goto start; addVal: mylnteger += 10; goto writeResult; Этот код вполне допустим, но очень труден для чтения. Попробуйте прочитать его сами и удостоверьтесь в этом. Прежде чем делать это, попытайтесь, глядя на код, определить, что он будет делать, и если догадаетесь, можете похвалить себя. Мы еще снова вернемся к оператору goto позже в этой главе, поскольку есть особенности его использования с некоторыми другими структурами. Ветвление Ветвлением называется процесс управления тем, какая строка кода должна выполняться следующей. За то, на какую строку должен осуществляться переход, отвечает определенный вид условного оператора. Действие этого условного оператора основано на сравнении проверочного значения с одним или более возможными значениями с применением булевской логики. В настоящем разделе описаны три доступных в С# методики разветвления: □ тернарная операция; □ оператор if; □ оператор switch. Тернарная операция Самый простой способ выполнить сравнение — это воспользоваться тернарной (или условной) операцией, о которой упоминалось в предыдущей главе. Унарные операции, работающие с одним операндом, уже встречались, бинарные операции, работающие с двумя операндами — тоже, поэтому совершенно не должно показаться удивительным то, что существует еще и операция, работающая с тремя операндами. Синтаксис этой операции выглядит следующим образом: <проверка> ? <результат_если_Тгие> : <результат_если_Еа1зе>
94 Часть I. Язык С# Здесь <проверка> вычисляется для получения булевского значения, а результатом операции в зависимости от этого значения является либо <результат_если_Тгие>, либо < результат_если_Га1зе>. Применять этот синтаксис можно, например, так: string resultString = (mylnteger < 10) ? "Меньше 10" : "Больше или равно 10"; Результатом приведенной тернарной операции будет какая-то одна из двух строк, обе из которых могут присваиваться переменной resultString. Выбор того, какая из них должна присваиваться, будет делаться сравнением значения mylnteger с числом 10, при этом в случае, если это значение меньше десяти, присваиваться будет первая строка, а если больше или равно 10 — то вторая. Например, в случае, если значение mylnteger равно 4, resultString будет присвоена строка Меньше 10. Такая операция вполне подходит для простых присваиваний, подобных показанному, но не совсем удобна для выполнения на основании сравнения более длинных фрагментов кода. Для них больше подходит оператор if. Оператор if Оператор i f является гораздо более многогранным и удобным средством для принятия решений. В отличие от ?:, оператор if не имеет результата (поэтому использовать его в операциях присваивания нельзя); вместо этого if можно применять для условного выполнения других операторов. Наиболее простой способ использования оператора if выглядит так, как показано ниже, и подразумевает вычисление выражения <проверка> и выполнение следующей за ним строки кода в случае, если <проверка> возвращает true: if (<проверка>) <код, выполняемый, если <проверка> равна true>; После выполнения этого кода, или невыполнения из-за того, что в результате вычисления выражения <проверка> было получено false, выполнение программы снова возобновляется со следующей строки кода. Вместе с оператором i f также может указываться и дополнительный код с помощью оператора else. Этот оператор выполняется в случае, если при вычислении выражения <проверка> получается false: if (<проверка>) <код, выполняемый, если <проверка> равна true>; else <код, выполняемый, если <проверка> равна false>; Оба раздела кода могут занимать несколько строк и представлять собой заключенные в фигурные скобки блоки: if (<проверка>) <код, выполняемый, если <проверка> равна true>; else <код, выполняемый, если <проверка> равна false>; В качестве быстрого примера можно переписать приведенный в предыдущем разделе код с тернарной операцией: string resultString = (mylnteger < 10) ? "Меньше 10" : "Больше или равно 10";
Глава 4. Управление потоком выполнения 95 Поскольку результат оператора if не может присваиваться переменной, потребуется отдельный шаг: string resultString; if (mylnteger < 10) resultString = "Меньше 10"; else resultString = "Больше или равно 10"; Код вроде этого хотя и является более многословным, но зато легче поддается прочтению и пониманию, чем тот, в котором используется тернарная операция; кроме того, он также обеспечивает гораздо большую гибкость. Практ* ческое занятие Применение оператора if 1. Создайте новое консольное приложение по имени Ch04Ex02 и сохраните его в каталоге C:\BegVCSharp\Chapter04. 2. Добавьте в файл Program, cs следующий код: static void Main(string[ ] args) { string comparison; Console.WriteLine("Enter a number:"); // Введите число double varl = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("Enter another number:"); // Введите еще одно число double var2 = Convert.ToDouble(Console.ReadLine()); if (varl < var2) comparison = "less than"; // меньше чем else if (varl == var2) comparison = "equal to"; // равно else comparison = "greater than"; // больше чем } Console.WriteLine("The first number is {0} the second number.", comparison); // вывод результата сравнения чисел Console.ReadKey (); } 3. Запустите приложение и по приглашению введите два числа, как показано на рис. 4.2. Рис. 4.2. Приложение Ch04Ex02 в действии
96 Часть I. Язык С# Описание полученных результатов Первый раздел приведенного кода выглядит очень знакомо. В нем из вводимых пользователем данных получаются два значения типа double: string comparison; Console.WriteLine("Enter a number:"); double varl = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("Enter another number:"); double var2 = Convert.ToDouble(Console.ReadLine()); Далее переменной comparison типа string на основании значений, полученных для varl и var2, присваивается строка. Перед этим, однако, выполняется проверка на предмет того, не является ли значение varl меньше значения var2: if (varl < var2) comparison = "less than"; Если нет, это означает, что значение varl либо больше, либо равно значению var2. Это требует добавления внутри первой операции сравнения в рамках раздела else второй операции сравнения: else { if (varl == var2) comparison = "equal to"; Раздел else внутри второй операции сравнения будет достигнут только в случае, если значение varl будет больше значения var2: else comparison = "greater than"; } И, наконец, напоследок значение переменной value выводится на консоль: Console.WriteLine("The first number is {0} the second number.", comparison); Примененный здесь метод вложения является лишь одним из возможных способов. С точно таким же успехом вместо него можно было бы записать и так: if (varl < var2) comparison = "less than"; if (varl == var2) comparison = "equal to"; if (varl > var2) comparison = "greater than"; Недостатком такого подхода является то, что он предусматривает выполнение всех трех операций сравнения, как бы ни выглядели значения varl и var2. Первый же подход предусматривает выполнение только одной операции сравнения, если varl меньше var2, и двух в противном случае (вместе с операцией сравнения varl == var2), что сокращает количество подлежащих выполнению строк кода. Здесь разница в показателях производительности будет незначительной, однако в тех приложениях, где скорость выполнения играет решающую роль, она будет очень серьезной. Проверка большего количества условий с помощью операторов if В предыдущем примере в отношении переменной varl выполнялась проверка трех условий, что охватывало все возможные ее значения. Однако иногда может возникать необходимость в выполнении проверки переменной на предмет соответствия каким-то конкретным значениям, например, равна ли varl числу 1, 2, 3 или 4 и т.д.
Глава 4. Управление потоком выполнения 97 Применение в подобных случаях такого подхода, как предлагался в предыдущем разделе, чревато излишним насыщением кода вложенными блоками: if (varl == 1) { // Выполнение какого-нибудь действия. } else { if (varl == 2) { // Выполнение какого-то другого действия. } else { if (varl == 3 | | varl == 4) { // Выполнение еще какого-то действия. } else { // Выполнение еще какого-нибудь другого действия. Одной из типичных ошибок является написание условий, вроде третьего условия в этом коде, в виде if (varl == 3 \ \ 4). При таком варианте первой операцией, согласно приоритетам, будет обрабатываться ==, из-за чего операции \ \ будет оставаться для работы, соответственно, булевский и числовой операнд, что и будет приводить к возникновению проблем. В таких ситуациях лучше применять нескольку иную схему отступов и сжимать блок кода для else (т.е. использовать после else одну строку кода, а не блок). Такой подход приводит к получению в конечном итоге структуры с операторами else if: if (varl == 1) // Выполнение какого-нибудь действия, else if (varl == 2) // Выполнение какого-то другого действия, else if (varl == 3 | | varl == 4) // Выполнение еще какого-то действия, else // Выполнение еще какого-нибудь другого действия. Эти операторы else if на самом деле являются двумя отдельными операторами, а код по функциональности полностью идентичен предыдущему, но зато более удобен для восприятия. При выполнении множества сравнений, как в этом примере, в качестве альтернативной структуры ветвления можно также использовать оператор switch.
98 Часть I. Язык С# Оператор switch Оператор switch похож на if тем, что тоже умеет условно выполнять код на основании проверяемого значения. Однако в отличие от него, switch позволяет проверять переменную сразу на предмет соответствия множеству различных значений, а не с помощью множества отдельных условий. В таких проверках разрешено использовать только дискретные значения, а не конструкции вроде "больше чем X", поэтому и способ применения этого оператора немного отличается. Базовая структура оператор switch выглядит следующим образом: switch (< проверяемая_переменная>) { case <значенш_для_сравпения_ 1 >: <код, подлежащий выполнению, если <проверяемая_переменная> ==<значение_для_сравнения_1» break; case <значение_для_сравнения_2>: <код, подлежащий выполнению, если <проверяемая_переменная> == <значение_для_сравнения_2» break; case <значение_для_сравнения_М>: <код, подлежащий выполнению, если <проверяемая_переменная> = break; default: <код, подлежащий выполнению, если <проверяемая_переменная> ! break; } Значение в <проверяемая_переменная> сравнивается с каждым из значений <зна- чение_для_сравнения_Х> (задаваемых с помощью операторов case) и если удается обнаружить совпадение, тогда выполняется тот код, который содержится в разделе, соответствующем обнаруженному совпадению. Если не удается обнаружить ни одного совпадения, выполняется тот код, который содержится в разделе default, при условии, что такой раздел существует. В конце кода в каждом разделе обязательно добавляется такая дополнительная команда, как break, исключающая вероятность перехода после обработки одного блока case к обработке следующего оператора case, потому что это недопустимо. Такое поведение относится к одному из отличий языка С# от C++, в котором разрешено переходить от обработки одного оператора case к другому. В данном случае оператор break просто завершает работу оператора switch, после чего процесс обработки продолжается с оператора, следующего после данной структуры. Существуют и альтернативные способы для предотвращения в коде С# перехода потока выполнения от одного оператора case к следующему. Первый подразумевает применение оператора return, приводящего к завершению работы текущей функции, а не только структуры switch (о нем более подробно будет рассказываться в главе 6), а второй — использование оператора goto. Операторы goto (которые рассматривались ранее в главе) пригодны для подобных случаев, поскольку операторы case фактически определяют метки в коде. Ниже приведен пример: switch {<проверяемая_переменная>) { case <значение_для_сравнения_ 1 >: <код, подлежащий выполнению, если<проверя€мая_переменная> == <значение_для_сравнения_1» goto сазе <значение_для_сравнения_2>; <значение_для_сравнения_М> > <значение_для_сравнения_Х> >
Глава 4. Управление потоком выполнения 99 case < значенш_для_сравнения_2 >: <код, подлежащий выполнению, если<проверяемая_переменная> == <значение_для_сравнения_2» break; Важно обратить внимание на одно исключение из правила, состоящего в том, что выполнение одного оператора case не может свободно переходить на следующий: в случае размещения нескольких операторов case вместе (в виде стопки) перед одиночным блоком кода, по сути, обеспечивается проверка одновременно нескольких условий. Удовлетворение любого из этих условий будет приводить к выполнению кода. Например: switch (<проверяемая_перемепная>) { case <значение_для_сравнения_1>: case <значение_для_сравнения_2>: <код, подлежащий выполнению, если <проверяемая_переменная> == <значение_для_сравнения_1> или <проверяемая_переменная> == <значение_для_сравнения_2» break; Следует также обратить внимание и на то, что все эти условия будут применяться и к оператору default. Правила, гласящего, что данный оператор должен быть обязательно последним в списке сравнений, не существует, поэтому при желании его можно помещать в одну стопку с операторами case. Добавление точки прерывания с помощью оператора break, goto или return будет гарантировать наличие безопасного пути выполнения кода в структуре во всех случаях. Каждая из конструкций <значение_для_сравнения_Х> должна обязательно представлять собой константное значение. Существуют два способа добиться этого. Первый подразумевает использование литеральных значений подобно тому, как показано ниже: switch (mylnteger) { case 1: <код, подлежащий выполнению, если mylnteger == 1> break; case -1: <код, подлежащий выполнению, если mylnteger == -1> break; default: <код, подлежащий выполнению, если mylnteger ! = сравниваемым_значениям> break; } Второй способ предусматривает применение константных переменных. Константные переменные (для простоты называемые просто константами) похожи на любые другие переменные во всем, кроме одного: значение, которое они содержат, никогда не изменяется. После присваивания константной переменной значения, оно остается на протяжении всего выполнения кода. В рассматриваемой ситуации константные переменные могут быть очень кстати, поскольку обычно легче читать код, в котором фактические сравниваемые значения скрываются от глаз на момент сравнения. Объявляются константные переменные путем указания перед назначаемым им типом ключевого слова const, а также обязательного присваивания им значения, как показано ниже: const int intTwo = 2;
100 Часть I. Язык С# рактическое занятие Приведенный выше код является вполне допустимым, а вот попытка использовать такой код: const int intTwo; intTwo = 2; обязательно приведет к появлению ошибки и невозможности его компиляции. То же самое произойдет и при попытке изменить значение константной переменной каким- либо образом после того, как ей было присвоено первоначальное значение. В следующем практическом занятии демонстрируется применение оператора switch для вывода в окне консоли различных строк в зависимости значения, вводимого пользователем для проверочной строки. Применение оператора switch 1. Создайте новое консольное приложение с именем Ch04Ex03 и сохраните его в каталоге C:\BegVCSharp\Chapter04. 2. Добавьте в файл Program, cs следующий код: static void Main(string[ ] args) { const string myName = "karli"; const string sexyName = "angelina"; const string sillyName = "ploppy"; string name; Console.WriteLine("What is your name?"); // Как вас зовут? name = Console.ReadLine() ; switch (name.ToLower() ) { case myName: Console. WriteLine ("You have the same name as me!"); // У вас такое же имя, как и у меня! break; case sexyName: Console.WriteLine("My, what a sexy name you have!"); // Забавное у вас имя! break; case sillyName: Console.WriteLine("That's a very silly name."); // Это очень глупое имя. break; } Console.WriteLine("Hello {0}!", name); // Приветствие Console.ReadKey(); 3. Запустите это приложение и введите какое-нибудь имя, когда появится соответствующее приглашение (рис. 4.3). Рш. 4.3. Приложение Ch04Ex03 в действии
Глава 4. Управление потоком выполнения 101 Описание полученных результатов В приведенном коде выполняются следующие действия: создаются три константных переменных строкового типа, считывается вводимая пользователем строка и на ее основании выводится на консоль определенный текст. Здесь строкой, которую вводит пользователь, является его имя. Перед сравнением введенного пользователем имени (в переменной name) со значениями существующих константных переменных, сначала оно принудительно преобразуется в нижний регистр с помощью метода name.ToLower (). Этот метод является стандартной командой, которая работает со всеми строковыми переменными и оказывается очень кстати, когда нет уверенности в том, каким именно образом пользователь ввел имя. Благодаря ей, строки Karli, kArLi, karli и все остальные подобные варианты все равно будут считаться совпадающими с проверочной строкой karli. Сам оператор switch пытается отыскать для введенной строки совпадение среди значений определенных константных переменных, и если это удается, происходит вывод на консоль персонализированного приветственного сообщения для пользователя, а если нет, на консоль выводится универсальное общее приветствие. Операторы switch не имеют никаких ограничений касательно того, какое количество разделов case они могут содержать, поэтому приведенный код можно расширить и так, чтобы он включал другие возможные имена, хотя это, конечно, потребует определенного времени на реализацию. Организация циклов Организация циклов подразумевает повторяющееся выполнение операторов. Эта методика является очень полезной, поскольку позволяет делать так, чтобы необходимые операции выполнялись повторно столько, сколько требуется раз, без повторного написания одного и того же кода снова и снова. В качестве простого примера рассмотрим следующий код, предназначенный для вычисления количества денег на банковском счету по прошествии 10 лет при условии ежегодной выплаты процентной ставки и отсутствия снятий и поступлений средств на счет: double balance = 1000; double interestRate = 1.05; // годовая ставка 5% balance *= interestRate; balance *= interestRate; balance *= interestRate; balance *= interestRate; balance *= interestRate; balance *= interestRate; balance *= interestRate; balance *= interestRate; balance *= interestRate; balance *= interestRate; Написание одной и той же строки 10 раз уже выглядит как неэффективная трата времени, а что если захочется заменить срок в 10 лет каким-нибудь другим значением? Тогда придется вручную копировать эту строку кода требуемое число раз, что весьма утомительно. К счастью, поступать подобным образом вовсе необязательно. Вместо этого можно создать цикл, выполняющий необходимую инструкцию нужное количество раз. Существует еще один важный тип циклов, позволяющий делать так, чтобы цикл выполнялся до тех пор, пока не будет удовлетворено определенное условие. Такие циклы выглядят немного проще, чем те, что требуется создавать для описанной выше ситуации (хотя и не менее полезны), поэтому давайте с них и начнем.
102 Часть I. Язык С# Циклы do Циклы do функционируют следующим образом. Сначала выполняется код, предназначенный для прогона в цикле, затем производится булевская проверка, и если в ее результате возвращается true, данный код выполняется снова, и так повторяется до тех пор, пока после проверки не будет возвращено значение false, после чего цикл завершается. Структура цикла do выглядит, как показано ниже, и подразумевает, что выражение <проверка> должно возвращать одно из булевских значений: do { <код, подлежащий выполнению в цикле> } while (<проверка>); Символ точки с запятой после оператора while является обязательным. Например, для вывода чисел от 1 до 10 в столбик можно было бы использовать такой цикл do: int i = 1; do { Console.WriteLine("{0}", i++) ; } while (i <= 10) ; Здесь используется постфиксная версия операции ++ для увеличения значения i на 1 после его вывода на экран, что требует выполнения проверки условия i <= 10 для включения числа 10 в состав выводимых на консоль чисел. В следующем практическом занятии эта операция тоже используется для демонстрации применения циклов do на примере слегка измененной версии приводившегося ранее кода для вычисления баланса на счету по прошествии 10 лет, а в частности — для вычисления количества лет, которое потребуется для накопления на счету определенной суммы денег, на основании суммы имеющихся на счету средств и процентной ставки. Практическое занятие[ Применение циклов do 1. Создайте новое консольное приложение по имени Ch04Ex04 и сохраните его в каталоге С: \BegVCSharp\Chapter04. 2. Добавьте в файл Program, cs следующий код: static void Main (string [ ] args) { double balance, interestRate, targetBalance; Console. WriteLine ("What is your current balance?"); // Каков текущий баланс? balance = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("What is your current annual interest rate (in %)?"); // Какова ежегодная ставка (в процентах)? interestRate = 1 + Convert.ToDouble(Console.ReadLine()) / 100.0; Console.WriteLine("What balance would you like to have?"); // Какой баланс необходимо получить? targetBalance = Convert.ToDouble(Console.ReadLine()) ; int totalYears = 0; do { balance *= interestRate; ++totalYears;
Глава 4. Управление потоком выполнения 103 while (balance < targetBalance) ; Console.WnteLine ("In {0} year{l} you'll have a balance of {2}.", totalYears, totalYears == 1 ? "" : "s", balance); // Вывод баланса через заданное количество лет Console.ReadKey(); } 3. Запустите это приложение и по приглашению введите какие-нибудь значения (рис. 4.4). Рис. 4.4. Приложение Ch04Ex04 в действии Описание полученных результатов В приведенном коде операция ежегодного подсчета баланса с использованием фиксированной процентной ставки просто повторяется такое количество раз, которое необходимо для того, чтобы баланс стал удовлетворять конечному условию. Подсчет количества необходимых для достижения указанного баланса лет ведется путем увеличения значения соответствующей переменной-счетчика на 1 с каждым проходом цикла: int totalYears = 0; do balance *= interestRate; ++totalYears; } while (balance < targetBalance) ; Это позволяет использовать значение данной переменной-счетчика в виде части выводимого на экран результата: Console.WriteLine ("In {0} уеаг{1} you'll have a balance of {2}.", totalYears, totalYears == 1 ? "" : "s", balance); Такое применение тернарной операции ?: для условного форматирования текста с минимумом кода является, пожалуй, наиболее распространенным подходом. В частности, в данном случае эта операция используется для отображения после слова year окончания s в случае, если значение totalYears неравно 1. К сожалению, данный код идеальным не назовешь. Например, в случае указания пользователем для желаемого баланса числа меньшего, чем для текущего баланса, вывод будет выглядеть примерно так, как показано на рис. 4.5. Рис. 4.5. Вывод приложения Ch04Ex04 в случае, если указанный желаемый баланс меньше текущего
104 Часть I. Язык С# Циклы do всегда выполняются как минимум один раз. Иногда, как, например, в данной ситуации, такой вариант не является идеальным. Разумеется, можно добавить оператор if: int totalYears = 0; if (balance < targetBalance) { do { balance *= interestRate; ++totalYears; } while (balance < targetBalance); } Console.WriteLine("In {0} year{l} you'll have a balance of {2}.", totalYears, totalYears == 1 ? "" : "s", balance); Очевидно, что это привносит в код совершенно ненужную сложность. Поэтому гораздо лучше применять вместо этого цикл while. Циклы while Циклы while очень похожи на циклы do, но имеют одно важное отличие: булевская проверка в них выполняется в начале, а не в конце цикла. Если после выполнения этой проверки возвращается false, код в цикле вообще не выполняется. Вместо этого поток выполнения переходит сразу на код, следующий непосредственно за циклом. Ниже показано, как специфицируется цикл while: while (<проверка>) { <код, подлежащий выполнению в цикле> } Они могут применяться практически так же, как циклы do: int i = 1; while (i <= 10) { Console.WriteLine("{0}", i++); } Этот код будет делать то же самое, что и показанный ранее код с циклом do, т.е. выводить в столбик числа от 1 до 10. В следующем практическом занятии показано, как модифицировать продемонстрированное в предыдущем разделе приложение, чтобы в нем использовался не цикл do, a while. практическое занятие Применение циклов while 1. Создайте новое консольное приложение по имени Ch04Ex05 и сохраните его в каталоге C:\BegVCSharp\Chapter04. 2. Добавьте в файл Program, cs следующий код (тот же код, что добавляли для приложения Ch04Ex04, но только измените его показанным ниже образом): static void Main(string [ ] args) { double balance, interestRate, targetBalance; Console .WriteLine ("What is your current balance?11);
Глава 4. Управление потоком выполнения 105 balance = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("What is your current annual interest rate (in %)?"); interestRate = 1 + Convert.ToDouble(Console.ReadLine()) / 100.0; Console.WriteLine("What balance would you like to have?"); targetBalance = Convert.ToDouble(Console.ReadLine()); mt totalYears = 0; while (balance < targetBalance) { balance *= interestRate; ++totalYears; } Console.WriteLine ("In {0} year{l} you'll have a balance of {2}.", totalYears, totalYears == 1 ? "" : "s", balance); Console.ReadKey(); } 3. Запустите приложение, но на этот раз укажите в качестве желаемого баланс, меньший текущего (рис. 4.6). Рис. 4.6. Приложение Ch04Ex05 в действии Описание полученных результатов Простая замена цикла do циклом while устранила проблему, которая существовала в предыдущем примере. Перемещение булевской проверки в начала позволило предусмотреть вариант развития событий в случае возникновения ситуации, не требующей прохождения цикла, и обеспечить переход в такой ситуации непосредственно к выводу результата. Конечно, для такой ситуации можно предусмотреть и другие варианты развития событий, например, обеспечить проверку вводимых пользователем данных на предмет того, является ли указанный желаемый баланс больше текущего. В таком случае разместить раздел кода, отвечающего за вводимые пользователем данные, в цикле можно следующим образом: Console.WriteLine("What balance would you like to have?"); do { targetBalance = Convert.ToDouble(Console.ReadLine()); if (targetBalance <= balance) Console.WriteLine ("You must enter an amount greater than " + "your current balance!\nPlease enter another value.") ; // Желаемый баланс должен быть больше // текущего! Введите другое значение. } while (targetBalance <= balance) ; Это приводит к отклонению всех не имеющих смысла значений, в результате чего вывод будет выглядеть примерно так, как показано на рис. 4.7.
106 Часть I. Язык С# Рис. 4.7. Вывод модифицированного приложения Ch04Ex05 Подобная проверка правильности, или верификация, вводимых пользователем данных становится довольно важным аспектом, когда дело доходит до дизайна приложения, и в настоящей книге будет встречаться немало примеров ее применения. Циклы for Последним типом цикла, который осталось рассмотреть в главе, является for. Цикл этого типа выполняется определенное количество раз и обладает собственным счетчиком. Для определения цикла for требуется описанная ниже информация: □ начальное значение для инициализации переменной, играющей роль счетчика; □ условие для продолжения цикла, в котором участвует переменная-счетчик; □ операция, которая должна выполняться над переменной-счетчиком в конце каждого цикла. Например, при желании создать цикл for и сделать так, чтобы значение его счетчика увеличивалось на 1 с 1 до 10, в качестве начального значения потребуется указать 1, в качестве условия — то, что значение счетчика должно быть меньше или равно 10, а в качестве операции, которая должна выполняться в конце каждого такого цикла— инкремент счетчика на 1. Вся эта информация должна размещаться в структуре цикла loop следующим образом: for {<инициализация>; <условие>; <операция>) { <код, подлежащий выполнению в цикле> } Работать этот цикл будет точно так же, как и показанный ниже цикл while: < инмциализация> while {<условие>) { <код, подлежащий выполнению в цикле> <операция> } Формат цикла for, однако, делает код более удобным для восприятия, поскольку его синтаксис подразумевает задание всех деталей цикла в одном месте, а не разнесение его по нескольким операторам и их размещение в разных частях кода. Как использовать циклы do и while для вывода чисел от 1 до 10 в столбик, уже показывалось ранее.
Глава 4. Управление потоком выполнения 107 Ниже приведен код, демонстрирующий, как то же самое можно делать с применением цикла for: int i; for (i = 1; i <= 10; ++i) { Console.WriteLine("{0}", i); } Здесь счетчик, представляющий собой переменную типа int по имени i, начинается со значения 1. В конце каждого цикла его значение увеличивается на 1 и выводится на консоль. На момент, когда возобновляется выполнение кода, идущего после цикла, переменная i имеет значение 11. Объясняется это тем, в конце цикла, в котором значение переменной i становится равным 10, оно все равно успевает увеличиться до 11, поскольку это происходит до обработки условия i <= 10, после вычисления которого цикл завершается. Как и while, цикл for выполняется только в том случае, если перед началом первого цикла проверка условия завершается,возвратом значения true, а это означает, что содержащийся в цикле код может вообще не выполниться. И, наконец, последним заслуживающим внимания моментом является то, что переменная-счетчик может объявляться и в виде части оператора for, т.е. приведенный выше код можно написать и так: for (int i = 1; i <= 10; ++i) { Console.WriteLine ("{О}", i); } Тем не менее, если поступить подобным образом, к переменной i нельзя будет получать доступ из кода, находящегося за пределами цикла (об этом более подробно пойдет речь в главе 6). В следующем практическом занятии приводится пример применения циклов for, а поскольку ранее демонстрировалось использование других циклов, этот пример является более интересным и подразумевает отображение множества Мандельброта (но с использованием простых текстовых символов, поэтому особо зрелищных результатов не дает). практическое *ажт*е| Применение циклов for 1. Создайте новое консольное приложение по имени Ch04Ex06 и сохраните его в каталоге C:\BegVCSharp\Chapter04. 2. Добавьте в файл Program, cs следующий код: static void Main(string[] args) { double realCoord, imagCoord; double realTemp, imagTemp, realTemp2, arg; int iterations; for (imagCoord = 1.2; imagCoord > = -1.2; imagCoord -= 0.05) { for (realCoord = -0.6; realCoord <= 1.77; realCoord += 0.03) { iterations = 0; realTemp = realCoord; imagTemp = imagCoord; arg = (realCoord * realCoord) + (imagCoord * imagCoord);
108 Часть I. Язык С# while ( (arg < 4) && (iterations < 40)) { realTemp2 = (realTemp * realTemp) - (imagTemp * imagTemp) - realCoord; imagTemp = B * realTemp * imagTemp) - imagCoord; realTemp = realTemp2; arg = (realTemp * realTemp) + (imagTemp * imagTemp) ; iterations += 1; } switch (iterations % 4) { case 0: Console.Write (".") ; break; case 1: Console.Write("o"); break; case 2: Console.Write("); break; case 3: Console.Write("@"); break; } } Console.Write("\n")( } Console.ReadKey(); } 3. Запустите приложение. На рис. 4.8 показан результат, который должен получиться. • :.'< B*gVCSh*rp.ChapterO4/Ch04Ex0eA:h04Ex06.biaOebugA:h04Ex0«EXE Рис. 4.8. Результат выполнения приложения Ch04Ex06
Глава 4. Управление потоком выполнения 109 Описание полученных результатов Рассмотрение деталей, касающихся вычисления множества Мандельброта, выходит за рамки данной главы, но разобраться в том, почему необходимы циклы в приведенном коде, все равно нужно. Те, кому не интересны математические детали, могут спокойно пропустить два следующих параграфа, потому что здесь важным является все-таки понимание кода. Каждая позиция в изображение множества Мандельброта соответствует комплексному числу вида N = х + y*i, где х представляет вещественную часть, у— мнимую, а i — квадратный корень из -1. Координаты х и у позиции на изображении соответствуют частям х и у в комплексном числе. Для каждой позиции в изображении выполняется проверка аргумента N, получаемого вычислением квадратного корня из х*х + у*у. Если его значение больше или равно 2, тогда считается, что позиция, соответствующая данному числу, имеет значение 0. Если же значение аргумента оказывается меньше 2, тогда оно заменяется значением N*N - N (дающим в результате N = (х*х - у*у - х) + B*х*у - у) *i) и снова подвергается проверке. Если это значение оказывается больше или равно 2, тогда считается, что позиция, соответствующая этому числу, имеет значение 1. И этот процесс продолжается до тех пор, пока либо не будет присвоено значение позиции в изображении, либо не будет выполнено определенное количество итераций. На основании тех значений, что присваиваются каждой точке в изображении, в графической среде на экране отображался бы пиксель определенного цвета. Однако поскольку в данном примере используется текстовый режим, вместо пикселей на экран выводятся символы. Теперь вернемся к коду и содержащимся в нем циклам. Начинается код с объявления необходимых для вычислений переменных: double realCoord, imagCoord; double realTemp, imagTemp, realTemp2, arg; int iterations; Здесь переменные realCoord и imagCoord представляют собой вещественную и мнимую часть числа N, а остальные переменные типа double предназначены для сохранения временной информации во время вычислений. Переменная iterations отвечает за фиксирование информации о количестве итераций, которые будут производиться перед тем, как значение аргумента N (arg) станет равно 2 или больше. Далее создаются два цикла for для циклической обработки охватывающих все изображение координат (с использованием для изменения счетчиков более сложного синтаксиса, а не ++ и —, который тоже является весьма распространенным приемом): for (imagCoord = 1.2; imagCoord > = -1.2; imagCoord -= 0.05) { for (realCoord = -0.6; realCoord <= 1.77; realCoord += 0.03) { Здесь применялись ограничения, необходимые для отображения основной части множества Мандельброта. Однако их, конечно, можно изменять каким угодно образом и получать, например, крупный план определенной части изображения. Внутри этих двух циклов содержится код, который имеет отношение только к одной точке во множестве Мандельброта и предоставляет для манипуляций значение N. Именно здесь вычисляется количество требуемых итераций, которое дает значение для прорисовывания для текущей точки.
110 Часть I. Язык С# Сначала инициализируются некоторые переменные: iterations = 0; realTemp = realCoord; imagTemp = imagCoord; arg = (realCoord * realCoord) + (imagCoord * imagCoord); Далее применяется цикл while для выполнения итерации. В случае, когда первоначальное значение аргумента N уже больше 2, лучше использовать цикл while, чем do, потому что тогда подходящий ответ выглядит как iterations = Ои никакие дальнейшие вычисления не требуются. Обратите внимание, что здесь речь идет не совсем о полном вычислении аргумента. Здесь просто получается значение х*х + у*уи осуществляется проверка на предмет того, является ли оно меньше 4. Это упрощает процесс вычисления, поскольку известно, что корнем квадратным из 4 будет 2, и вычислять другие корни квадратные самостоятельно не требуется: while ((arg < 4) && (iterations < 40)) { realTemp2 = (realTemp * realTemp) - (imagTemp * imagTemp) - realCoord; imagTemp = B * realTemp * imagTemp) - imagCoord; realTemp = realTemp2; arg = (realTemp * realTemp) + (imagTemp * imagTemp) ; iterations += 1; } Максимально возможное количество итераций данного цикла, вычисляющего значения показанным выше образом, составляет 40 раз. После сохранения значения для текущей точки в iterations применяется оператор switch для выбора символа, выводимого на экран. Здесь используются только 4 разных символа вместо 40 возможных, а также операция % для того, чтобы значения 0, 4, 8 и т.д. давали один символ, значения 1, 5, 9 и т.д. — другой и далее в том же духе: switch (iterations % 4) { case 0: Console.Write("."); break; case 1: Console.Write("o"); break; case 2: Console.Write("); break; case 3: Console.Write("@"); break; } Команда Console.Write () применяется вместо команды Console.WriteLine () потому, что в данной ситуации не нужно, чтобы каждый выводимый символ отображался в новой строке. В конце одного из находящихся в самой глубине циклов for, однако, все же требуется завершить строку, поэтому в нем просто выводится символ конца строки с использованием соответствующей управляющей последовательности, которая уже демонстрировалась ранее: } Console.Write("\n"); }
Глава 4. Управление потоком выполнения Это обеспечивает отделение строк друг от друга и их надлежащее выравнивание. Результат, который данное приложение выдает в конечном итоге, хоть и не является особо зрелищным, все равно прилично впечатляет и уж конечно иллюстрирует то, насколько полезным может быть применение приемов ветвления кода и организации циклов. Прерывание циклов В некоторых случаях бывает необходимо иметь возможность более точного управления ходом обработки содержащегося в циклах кода. В языке С# для этого предусмотрено четыре показанных ниже команды, применение трех из которых уже демонстрировалось ранее при рассмотрении других ситуаций: □ break — приводит к немедленному завершению цикла; □ continue — приводит к немедленному завершению текущей итерации цикла (выполнение продолжается со следующей итерации); □ goto — позволяет переходить из цикла к выполнению кода с указанной меткой (не рекомендуется использовать, если требуется, чтобы код был легким для восприятия и понимания); □ return — приводит к выходу из цикла и содержащей его функции (более подробно будет рассматриваться в главе 6). Команда break просто завершает цикл, после чего выполняется строка кода, находящаяся сразу после этого цикла, например: int i = 1; while (i <= 10) { if (i == 6) break; Console.WriteLine("{0}", i++) ; } Этот код будет выводить на консоль числа от 1 до 5, поскольку по достижении переменной i значения 6 команда break вынуждает цикл завершиться. Команда continue подразумевает прерывание только текущей итерации, а не всего цикла, как показано ниже: int i; for (i = 1; i <= 10; i++) { if ((i % 2) == 0) continue; Console.WriteLine(i); } В этом примере, всякий раз, когда остатком деления значения переменной i на 2 будет ноль, оператор continue будет останавливать выполнение текущей итерации, в результате чего на экране будет отображаться только числа 1, 3, 5, 7 и 9. Третьим способом прерывания цикла является использование оператора goto, как уже показывалось ранее: int i = 1; while (i <= 10) { if (i == 6) goto exitPoint; Console.WriteLine("{0}", i++) ; }
112 Часть I. Язык С# Console.WriteLine("This code will never be reached."); // Этот код недостижим exitPoint: Console.WriteLine ("This code is run when the loop is exited using goto." // Этот код выполняется при выходе из цикла посредством goto Обратите внимание, что использование оператора goto для завершения цикла является допустимым (хотя и немного неаккуратным), а вот его применение для перехода внутрь цикла извне — нет. Бесконечные циклы Существует возможность с помощью кодирования ошибок и дизайна определять так называемые бесконечные циклы, т.е. циклы, которые никогда не завершаются. В качестве очень простого примера, рассмотрим следующий код: while (true) { // код в цикле } Такой цикл может быть полезен и его всегда можно прервать либо с помощью оператора break, либо вручную через диспетчер задач Windows. Однако когда подобные циклы встречаются случайно, они могут и раздражать. Возьмем следующий цикл, который похож на приведенный в предыдущем разделе цикл for: int i = 1; while (i <= 10) { if ((i % 2) == 0) continue; Console.WriteLine("{0}", i++) ; } Здесь значение переменной i не увеличивается до самой последней строки кода в цикле, а выполнение этой операции предусмотрено только после оператора continue. При достижении continue (которое будет иметь место, когда переменная i равна 2) на следующей итерации цикла будет использоваться то же самое значение переменной i, что приведет к продолжению цикла, проверке i, снова продолжению цикла и т.д. В конечном итоге это приведет к зависанию приложения. Работу зависшего приложения можно прервать, и перезагружать систему при этом не потребуется. Резюме Благодаря этой главе, вы пополнили свои знания в области программирования, рассмотрев различные структуры, которые можно использовать в коде. Правильное применение этих структур играет существенную роль при создании более сложных приложений, поэтому они еще не раз будут встречаться в книге. Сначала в главе рассказывалось о том, что собой представляют булевская и поразрядная логика. Данная тема действительно очень важна при реализации в программах циклов и ветвления кода. Тщательное ознакомление с описанными в этом разделе операциями и приемами крайне необходимо. Методика ветвления позволяет обеспечивать выполнение кода на основе соблюдения тех или иных условий, а вместе с организацией циклов — еще и создавать в коде С# разнообразные сложные структуры. По мере добавления циклов внутри других циклов, в свою очередь, находящихся внутри структур if еще каких-то циклов, стано-
Глава 4. Управление потоком выполнения 113 вится понятно, почему использование отступов в коде является настолько полезным. Смещение всего кода влево мгновенно делает его трудным для восприятия глазом и более трудным для отладки. Очень важно уже на этом этапе хорошо разобраться в правилах использования отступов, позже это обязательно окупится. Среда VS добавляет многие отступы автоматически, но все равно не помешает научиться ставить их там, где надо, самостоятельно, по мере ввода своего кода. Следующая глава посвящена более детальному изучению переменных и способов их применения. Упражнения 1. При наличии двух целых чисел, хранящихся в переменных varl и var2, какую булевскую проверку можно выполнить для выяснения того, является ли то или другое (но не оба вместе) больше 10? 2. Напишите приложение, включающее логику из первого упражнения, которое получает два числа от пользователя и отображает их на экране, но отклоняет варианты, когда оба числа больше 10, и предлагает в таком случае ввести два других числа. 3. Что неверно в следующем коде? int i; for (i = 1; i <= 10; i++) { if ((i % 2) = 0) continue; Console.WriteLine (i); } 4. Измените приложение, отображающее множество Мандельброта, так, чтобы оно запрашивало информацию о границах изображения у пользователя и отображало только выбранный раздел рисунка. Текущий код предусматривает отображение стольких символов, сколько сможет уместиться на одной строке консольного приложения; подумайте, как сделать так, чтобы каждый выбираемый рисунок умещался в одном и том же объеме пространства, максимально увеличив доступную для просмотра область.
5 Дополнительные сведения о переменных Теперь, когда вы уже немного познакомились с языком С#, можно вернуться рассмотреть некоторые более сложные вопросы, касающиеся переменных. Сначала в настоящей главе будет рассказано о преобразовании типов, т.е. о том, как преобразовывать значения из одного типа в другой. Данный вопрос уже затрагивался ранее, но здесь он рассматривается более полно. Это позволит лучше понять, что будет происходить в случае (намеренного и непреднамеренного) смешивания типов в выражениях, а также более точно управлять способом обработки данных, что поможет упростить код и избежать неприятных сюрпризов. Далее будут показаны еще несколько доступных для использования типов переменных. □ Перечисления. Типы переменных, имеющие определяемый пользователем дискретный набор возможных значений, которые, в свою очередь, могут использоваться пригодным для восприятия человеком образом. □ Структуры. Сложные типы переменных, состоящие из определяемого пользователем набора переменных другого типа. □ Массивы. Типы, которые хранят множество переменных одного типа и позволяют получать доступ к отдельным значениям по индексу. Эти типы переменных немного сложнее простых типов, которые демонстрировались ранее, но могут значительно упростить жизнь разработчика. И, наконец, в завершение главы будет рассмотрена еще одна полезная тема, касающаяся строк — базовые приемы манипулирования строками.
Глава 5. Дополнительные сведения о переменных 115 Преобразование типов Ранее в книге уже показывалось, что все данные, независимо от типа, представляют собой последовательность битов, т.е. последовательность нулей и единиц. Предназначение переменной определяет способ, которым эти данные интерпретируются. Наиболее простым примером является тип char. Этот тип представляет символ из набора символов Unicode с использованием числа. На самом деле он сохраняется в точности так же, как тип ushort, поскольку они оба могут хранить числа в диапазоне от 0 до 65 535. Однако, в общем, в разных типах переменных применяются разные схемы для представления данных. Это означает, что даже если бы можно было помещать последовательность битов из одной переменной в переменную другого типа (например, если в обоих используется одинаковый объем хранения или если в целевом типе хватает места для размещения всех битов из исходного типа), результаты могли бы быть совершенно не такими, как ожидалось. Вместо такого взаимно однозначного сопоставления битов из одной переменной в биты другой переменной, для данных необходимо применять технологии преобразования шипов. Они бывают двух видов. □ Неявное преобразование. Применяется, когда преобразования из типа А в тип В возможно при любых обстоятельствах, и правила для выполнения преобразования являются достаточно простыми для того, чтобы его можно было доверить компилятору. □ Явное преобразование. Применяется тогда, когда преобразование из типа А в тип В возможно только при определенных обстоятельствах или когда правила для преобразования являются достаточно сложными для того, чтобы было выгодным использование дополнительной обработки. Неявное преобразование Неявное преобразование не требует от разработчика ни приложения усилий, ни написания дополнительного кода. Рассмотрим приведенный ниже код: varl = var2; Эта операция присваивания может как подразумевать неявное преобразование, если тип var2 может быть неявно преобразован в тип varl, так и не подразумевать, если типы обеих переменных одинаковы. Например, значения ushort и char являются по существу взаимозаменяемыми, поскольку оба этих типа позволяют хранить числа в диапазоне от 0 до 65 535. Поэтому между ними может осуществляться неявное преобразование, как показано в следующем коде: ushort destinationVar; char sourceVar = 'a1; destinationVar = sourceVar; Console.WriteLine("значение sourceVar: {0}", sourceVar); Console.WriteLine("значение destinationVar: {0}", destinationVar); Здесь значение, хранящееся в sourceVar, помещается в destinationVar, а вывод переменных с помощью двух команд Console .WriteLine () приводит к отображению на экране следующего результата: значение sourceVar: a значение destinationVar: 97
116 Часть I. Язык С# Хотя в этих двух переменных хранится одинаковая информация, интерпретируются они разными способами, зависящими от их типа. Неявное преобразование поддерживается между многими простыми типами; типы bool и string не поддерживают ни одного варианта, но зато числовые типы имеют их несколько. Для справки в табл. 5.1 перечислены все числовые преобразования, которые компилятор может делать неявно (помните, что значения типа char сохраняются как числа, потому тип char тоже считается числовым). Таблица 5.1. Неявные числовые преобразования Тип Может быть безопасно преобразован в тип Byte short, ushort, int, uint, long, ulong, float, double, decimal Sbyte short, int, long, float, double, decimal Short int, long, float, double, decimal Ushort int, uint, long, ulong, float, double, decimal Int long, float, double, decimal Uint long, ulong, float, double, decimal Long float, double, decimal Ulong float, double, decimal Float double Char ushort, int, uint, long, ulong, float, double, decimal He волнуйтесь, учить эту таблицу наизусть не нужно, поскольку процесс вычисления того, какие операции преобразования компилятор может выполнять неявно, на самом деле выглядит довольно просто. В главе 3 приводилась таблица с перечнем всех возможных значений для каждого простого числового типа. Правило касательно неявного преобразования этих типов гласит, что любой тип А, диапазон возможных значений которого полностью вписывается в диапазон возможных значений типа В, может преобразовываться в этот тип неявным образом. Объясняется это очень просто. При попытке уместить в переменную значение, выходящее за пределы диапазона значений, которые переменная этого типа может принимать, обязательно возникнет проблема. Например, переменная типа short способна хранить числа до 32 767, а максимальным значением, которое может сохраняться в переменной типа byte, является 255, поэтому при попытке преобразовать значение short в byte, естественно, могут возникнуть проблемы. В частности, в случае, если значением в переменной short окажется число в диапазоне от 256 до 32 767, тогда оно просто не поместится в переменную byte. Если известно, что значение в переменной типа short будет меньше 255, можно ли его в таком случае преобразовать? Простым ответом, будет, конечно же, да, а более сложным — можно, но с использованием явного преобразования. Явное преобразование означает взятие ответственности за то, что произойдет, на себя. Явное преобразование Уже по названию понятно, что явное преобразование имеет место только тогда, когда разработчик явным образом просит компилятор преобразовать значение из одного типа данных в другой. Из-за этого оно подразумевает написание дополнительного кода, формат которого может выглядеть по-разному, в зависимости от того, какой
Глава 5. Дополнительные сведения о переменных 117 конкретно метод преобразования требуется использовать. Прежде чем рассматривать какой-либо осуществляющий такое явное преобразование код, следует посмотреть, что будет происходить, если его не добавлять. Например, ниже приведен измененный код из предыдущего раздела, в котором предпринимается попытка преобразовать значение short в byte: byte destinationVar; short sourceVar = 7; destinationVar = sourceVar; Console.WriteLine("значение sourceVar: {0}", sourceVar); Console.WriteLine("значение destinationVar: {0}", destinationVar); При попытке скомпилировать этот код будет выдана следующая ошибка: Cannot implicitly convert type 'short' to 'byte'. An explicit conversion exists (are you missing a cast?) He удается неявным образом преобразовать тип 'short' в 'byte'. Возможно выполнение явного преобразования (может быть, пропущено приведение?) К счастью для разработчиков, компилятор С# способен выявлять недостающие операции явного преобразования типов. Чтобы этот код компилировался, в него необходимо добавить код, выполняющий явное преобразование. Наиболее простой способ предусматривает приведение переменной типа short к типу byte (как и было предложено в сообщении об ошибке). Операция приведения, по сути, заключается в принудительном преобразовании данных из одного типа в другой и подразумевает использование следующего простого синтаксиса: (целевой_тип) исходная_переменная Это строка кода позволяет преобразовывать значение, содержащееся в sourceVar, в значение типа destinationType. Выполнение подобной операции приведения типов возможно лишь в некоторых ситуациях. Типы, имеющее очень отдаленное отношение или вообще никакого отношения друг к другу, скорое всего, не будут поддерживать преобразования посредством приведения. С помощью этого синтаксиса изменить пример так, чтобы он предусматривал выполнение принудительного преобразования из short в byte, можно следующим образом: byte destinationVar; short sourceVar = 7; destinationVar = (byte) sourceVar ; Console.WriteLine("значение sourceVar: {0}", sourceVar); Console.WriteLine("значение destinationVar: {0}", destinationVar); После этого вывод будет выглядеть так: значение sourceVar: 7 значение destinationVar: 7 А что будет происходить при попытке принудительно преобразовать значение переменной в такое, в которое оно не сможет уместиться? Изменим предыдущий код, как показано ниже: byte destinationVar; short sourceVar = 281; destinationVar = (byte) sourceVar; Console.WriteLine("значение sourceVar: {0}", sourceVar); Console.WriteLine("значение destinationVar: {0}", destinationVar);
118 Часть I. Язык С# Это приведет к получению следующего результата: значение sourceVar: 281 значение destinationVar: 25 Почему результат стал выглядеть так? Чтобы понять это, давайте посмотрим на двоичные представления двух данных чисел, а также максимального значения, которое может сохраняться в переменной типа byte — число 255: 281 = 100011001 25 = 000011001 255 = 0111Ц111 Здесь видно, что произошла потеря левого крайнего бита исходных данных. Сразу же возникает вопрос о том, как узнать, когда такое будет происходить. Очевидно, что случаи, когда нужно явно приводить один тип к другому, обязательно будут встречаться, и потому не помешало бы заранее знать о том, не вызовет ли это приведение потерю каких-то данных. В противном случае возможны очень серьезные ошибки, особенно в приложениях вроде бухгалтерского учета или вычисления траектория полета ракеты на Луну. Для решения этой проблемы существуют два способа. Первый состоит в проверке значения исходной переменной и его сравнении с известными ограничениями целевой переменной, а второй — в принуждении системы обращать особое внимание на данную операцию преобразования во время выполнения. Попытка уместить значение в переменной, когда оно является слишком большим для переменной такого типа, приводит к переполнению и именно на предмет наличия такой ситуации нужно выполнять проверку. Для установки так называемого контекста проверки на предмет переполнения для выражения предусмотрены два ключевых слова: checked и unchecked. Применяются они следующим образом: checked{выражение) unchecked{выражение) В последнем примере обеспечить принудительное выполнение проверки на предмет переполнения можно так: byte destinationVar; short sourceVar = 281; destinationVar = checked((byte)sourceVar); Console.WriteLine("значение sourceVar: {0}", sourceVar); Console.WriteLine("значение destinationVar: {0}", destinationVar); Попытка выполнения этого кода приведет к аварийному завершению и отображению сообщения об ошибке, подобного показанному на рис. 5.1 (которое было получено при компиляции проекта Overf lowCheck). Если, однако, заменить в этом коде checked на unchecked, то он выдаст точно такой же результат, как и раньше, без сообщений об ошибках. Такое поведение идентично уже продемонстрированному ранее поведению по умолчанию. С помощью этих двух ключевых слов можно настроить приложение, чтобы оно вело себя так, будто каждое выражение такого типа включает слово checked, если только в этом выражении явно не используется ключевое слово unchecked (другими словами, изменить параметр, используемый для проверки на предмет переполнения по умолчанию). Чтобы сделать это, нужно изменить свойства проекта в VS, щелкнув правой кнопкой мыши на проекте в окне Solution Explorer (Проводник решений), выбрав в контекстном меню пункт Properties (Свойства) и щелкнув на элементе Build (Сборка) в левой части окна. Это приведет к отображению параметров сборки, как показано на рис. 5.2.
Глава 5. Дополнительные сведения о переменных 119 ^jOverfiowChec* Progr»m lt.g System; ng System.Collections.Generic; r.y System.Linq; :r.-j System.Text; Z I usim i usim 4 L uslm . r. °v в : >espace Overfloetheck class Prcgtara < static void laln(strmg[] args) ( .у- destlnationVar; short sourceVar - 261; (festlnatlonVar - checked! (byte)sourceVtr'; •Jon!<oi^.W£ilteLlne("sourceV*r trail <0<", sourceVar); Сопло;*.Hritellne('*<tes«tinat lonVar val: @)", destlnationVar); I ЫШШ*Ш+Ш »s. Ш+ШЛШ Arithmetic operation resulted In »n overflow. TioubtMhoottngl^t: • j Mike lure vou »»« not dividing by i«ic : ' Get general ne'p tor thtl raptJofl ; 5e»icn for more Help ОгЛпе... ЯЙН : \fttm QiW : С ору exception detiii to the dipboerd ""лЗ! 4 Рис. 5.1. Сообщение об ошибке Condition»! completion symbol»: В Define DEBUG con»t»nt •ft Dehne TRACE ci В Allow unsafe code S^ Optimize code Output p»th В XMl documentation file: Generate >en»iiz»tion »j sembhr !**•. 7.' Рис. 5.2. Параметры сборки Свойство, которое необходимо изменить, относится к расширенным параметрам, поэтому щелкните на кнопке Advanced (Дополнительно) и в диалоговом окне, которое появится после этого, отметьте флажок Check for arithmetic overflow/underflow (Выполнять проверку на предмет арифметического переполнения/потери значимости), как показано на рис. 5.3. По умолчанию этот флажок не отключен, а его включение обеспечивает описанное выше поведение checked.
120 Часть I. Язык С# / '■ 1 Advanced Build Settings 1 1 General language Version: default ;V; Check for arithmetic overflow/underflow Do not reference mscorlib.dll Output Debug info: \9ЩЩ I - » ' File Alignment [щ DU Base Address: 1 °* J \ in» ■■ i ■ | Pwc. 5.3. Отметка флажка Check for arithmetic overflow/underflow Явные преобразования с помощью команд Convert Операции явного преобразования, которые использовались в различных практических занятиях, немного отличаются от тех, что рассматривались до сих пор в настоящей главе. В частности, они подразумевали преобразование строковых значений в числа с помощью команд вроде Convert. ToDouble (), что явно не является подходом, который будет работать для всех возможных строк. Попытка, например, преобразовать строку Number в значение типа double с помощью команды Convert. ToDouble () при выполнении кода приведет к отображению диалогового окна, показанного на рис. 5.4. Input string was not in a correct format Troubleshooting tips: 15 3 » »ure wour method arguments are in the riaht formatl When converting a string to DateTime. parse the string to take the date before putting each variable into the DateTime object. Get general help for this exception. Search for more Help Online... ■ View Detail.. Copy exception detail to the clipboard Рис. 5.4. Диалоговое окно с сообщением о недопустимом преобразовании строки в число Как видно, выполнить подобную операцию не получится. Чтобы такие операции преобразования работали, предоставляемая строка должна обязательно иметь вид допустимого представления числа, и при этом данное число должно быть обязательно таким, которое не будет приводить к переполнению. Допустимым представлением числа считается то, в котором содержится (не обязательно) какой-то знак (плюс или минус), ноль или более цифр, а также, не обязательно, точка, за которой следует еще одна или более цифр, символ е или Е, за которым следует еще какой-то знак и одна или более цифр, и больше ничего, кроме символов пробела (перед и после этой последовательности). В случае использования всех необязательных элементов допустимым представлением числа могут становиться такие сложные строки, как -1.2451е-24. Существует еще много явных операций преобразования, которые могут задаваться подобным образом, как показано в табл. 5.2.
Глава 5. Дополнительные сведения о переменных 121 Таблица 5.2. Команды для явных преобразований Команда Convert, Convert, Convert, Convert. Convert, Convert, Convert, Convert, Convert, Convert, Convert, Convert, Convert, Convert, .ToBoolean(val) .ToByte(val) .ToChar(val) .ToDecimal(val) .ToDouble(val) ,ToIntl6(val) .ToInt32(val) .ToInt64(val) .ToSByte(val) .ToSingle(val) .ToString(val) .ToUIntl6(val) .ToUInt32(val) .ToUInt64(val) Результат val, преобразованная в bool val, преобразованная в byte val, преобразованная в char val, преобразованная в decimal val, преобразованная в double val, преобразованная в short val, преобразованная в int val, преобразованная в long val, преобразованная в sbyte val, преобразованная в float val, преобразованная в string val, преобразованная в ushort val, преобразованная в uint val, преобразованная в uiong Здесь на месте val может указываться переменная практически любого типа (о том, могут ли данные команды работать с этим типом, будет сообщать сам компилятор). К сожалению, как видно в таблице, имена всех команд преобразования немного отличаются от используемых в С# имен типов; например, для выполнения преобразования в тип int нужно использовать команду Convert. ToInt32 (). Объясняется это тем, что данные команды принадлежат пространству имен System в .NET Framework, а не являются "родными" командами языка С#. Это, однако, позволяет использовать их не только в С#, но и в других совместимых с .NET языках. Важный аспект этих команд преобразования, на который обязательно нужно обратить внимание, состоит в том, что они всегда подразумевают выполнение проверки на предмет переполнения, и ни ключевые слова checked и unchecked, ни параметры свойств проекта на них никак не влияют. В следующем практическом занятии демонстрируется пример, охватывающий применение большинства из рассмотренных в этом разделе типов операций преобразования. В частности, в нем сначала объявляется и инициализируется ряд переменных различных типов, а затем выполняются операции преобразования между их типами как неявным, так и явным образом. практическое занятие Преобразование типов 1. Создайте новое консольное приложение по имени Сп05Ех01 и сохраните его в каталоге С:\BegVCSharp\Chapter05. 2. Добавьте в файл Program, cs следующий код: static void Main(string [ ] args) { short shortResult, shortVal = 4;
122 Часть I. Язык С# int integerVal = 67; long longResult; float floatVal = 10.5F; double doubleResult, doubleVal = 99.999; string stringResult, stringVal = 7"; bool boolVal = true; Console.WriteLine ("Variable Conversion ExamplesW) ; // Примеры преобразования переменных doubleResult = floatVal * shortVal; Console.WriteLine ("Implicit, - > double: {0} * {1} - > {2}", floatVal, shortVal, doubleResult); // Неявное преобразование shortResult = (short)floatVal; Console.WriteLine("Explicit, - > short: {0} - > {1}", floatVal, shortResult); // Явное преобразование stringResult = Convert.ToString(boolVal) + Convert.ToString(doubleVal); Console.WriteLine("Explicit, - > string: \"{0}\" + \"{1}\" - > {2}", boolVal, doubleVal, stringResult); // Явное преобразование longResult = integerVal + Convert.ToInt64(stringVal); Console.WriteLine ("Mixed, - > long: {0} + {1} - > {2}", integerVal, stringVal, longResult); // Смешанное преобразование Console.ReadKey(); } 3. Запустите приложение. На рис. 5.5 показан результат, который должен получиться. Рис. 5.5. Приложение Ch05Ex01 в действии Описание полученных результатов В этом примере присутствуют все рассмотренные до сих пор типы преобразований, как в простых операциях присваивания, подобных тем, что демонстрировались в приведенных ранее коротких примерах кода, так и в выражениях. Рассмотрению подлежат оба случая, поскольку к преобразованию типов может приводить обработка не только операций присваивания, но и каждой не унарной операции. Например: shortVal * floatVal Здесь значение short умножается на значение float. В ситуациях, подобных этой, где не указано никакой явной операции преобразования, по возможности будет выполняться неявная операция преобразования. В данном примере единственной неяв-
Глава 5. Дополнительные сведения о переменных 123 ной операцией преобразования, в которой есть смысл, является преобразование типа short в тип float (поскольку для преобразования float в short требуется явная операция), поэтому именно она и будет использоваться. При желании, однако, это поведение можно переопределить, как показано ниже: shortVal * (short)floatVal Это не означает, что в таком случае после операции будет возвращаться значение short. Из-за того, что результат перемножения двух значений short, скорее всего, будет превышать 32 767 (максимальное значение, которое может храниться в переменных типа short), на самом деле после этой операции будет возвращаться значение int. Явные преобразования, выполняемые с помощью такого синтаксиса приведения, имеют те же приоритеты, что и другие унарные операции (вроде префиксной формы операции ++), т.е. — высшие. При наличии операторов, в которых принимают участие смешанные типы, преобразование происходит при обработке каждой операции, в соответствии с приоритетами. Это означает, что могут иметь место и "промежуточные" преобразования: doubleResult = floatVal + (shortVal * floatVal); Здесь первой будет обрабатываться операция *, которая, как уже рассказывалось ранее, будет приводить к преобразованию shortVal во float. Далее будет обрабатываться операция +, которая не требует преобразования типов, поскольку имеет дело с двумя значениями float (а именно— floatVal и результатом вычисления shortVal * floatVal). И, наконец, последней будет обрабатываться операция =, при которой результат предыдущей операции, имеющий тип float, будет преобразовываться в тип double. Такой процесс преобразования может показаться на первый взгляд довольно сложным, но разделение выражений на отдельные части с принятием во внимание приоритетов операций должно позволить разобраться, что в нем к чему. Сложные типы переменных Пока что рассматривались только все простые типы переменных, которые поддерживаются в С#. В этом разделе речь пойдет о трех немного более сложных (но очень полезных) видах переменных: перечислениях, структурах и массивах. Перечисления У каждого из рассмотренных до сих пор типов (за исключением string) имеется четко определенный набор допустимых значений. Правда, у типов вроде double он настолько велик, что может практически считаться бесконечным, но при этом он все равно остается строго фиксированным набором. Самым простым тому примером является тип bool, который может принимать только одно из двух значений — true или false. Встречается и много других ситуаций, в которых может оказаться необходимым иметь переменную, способную принимать какое-то одно значение из фиксированного набора результатов. Например, может потребоваться переменная типа orientation, способная хранить одно из таких значений, как north, south, east или west. В таких ситуациях применяются перечисления. Для orientation перечисления делают именно то, что нужно — позволяют определять тип, способный принимать только какое-нибудь одно значение из фиксированного набора, который предоставляется. Тогда все, что требуется сделать — создать свою собственную переменную типа перечисления по имени orientation, способную принимать одно из четырех возможных значений.
124 Часть I. Язык С# Обратите внимание, что здесь необходим еще один дополнительный шаг: нужно не просто объявлять переменную конкретного типа, а сначала объявлять и детализировать определяемый пользователем тип и только затем объявлять переменную этого типа. Определение перечислений Перечисления могут определяться с помощью ключевого слова enum следующим образом: enum имя_типа { значение 1, значение 2, значениеЗ, значениеЫ } После этого можно начинать объявлять переменные такого нового типа с помощью такого синтаксиса: имя_типа имя_переменной; Присваивать значения таким переменным можно следующим образом: имя_переменной = имя_типа. значение; У перечислений имеется базовый тип, используемый для хранения. Каждое из значений, которое может принимать тип перечисления, сохраняется в виде значения именно этого базового типа, каковым по умолчанию является int. Указать другой базовый тип можно, добавив желаемый тип в объявление перечисления: enum имя_типа: базовый_тип значение!, значение2, значениеЗ, значениеЫ } В общем, для перечислений в качестве базовых могут использоваться типы byte, sbyte, short, ushort, int, uint, long и ulong. По умолчанию каждому значению автоматически присваивается значение соответствующего базового типа в соответствии с порядком, в котором оно определяется, начиная с нуля. Это означает, что значение 1 получает 0, значение2 — 1, значениеЗ — 2 и т.д. Операции присваивания таких значений можно переопределять путем использования операции = и указания фактических желаемых значений для каждого значения перечисления: enum имя_типа: базовый_тип { значение1 = фактическое_значение1, значение2 = фактическое_значение2, значениеЗ = фактическое_значениеЗ, значение^ = фактическое_значениеЯ, }
Глава 5. Дополнительные сведения о переменных 125 Помимо этого, еще также можно указывать идентичные значения для нескольких значений перечисления с использованием одного значения в качестве базового для другого значения: enum имя_типа: базовый_тип { значение1 = фактическое_значение1, значение2 = значение1, значениеЗ, значениеЫ = фактическое_значениеК, } Любым значениям, которым ничего явно не было присвоено, автоматически назначается базовое значение, вследствие чего они получают значение, на 1 большее, чем у предыдущего объявленного явным образом. В приведенном выше коде, например, значениеЗ автоматически получит значение на 1 большее, чем значение 1. Обратите внимание, что это может приводить к появлению проблем в случае указания после определения вроде значение2 = значение 1 значений, идентичных другим. Например, в следующем коде значение4 получит точно такое же значение, что и значения2: enum имя_типа: базовый_тип { значение! = фактическое_значение1, значение2, значениеЗ = значение!, значение4, значением = фактическое_значениеЫ, } Конечно, если такое поведение является именно тем, что нужно, тогда этот код вполне пригоден для использования. Еще обратите внимание, что присваивание значений по кругу будет приводить к появлению ошибки: enum имя_типа: базовый_тип { значение1 = значение2, значение2 = значение! } В следующем практическом занятии предлагается пример, иллюстрирующий все описанное выше. В частности, в нем сначала определяется перечисление по имени orientation, а затем демонстрируются различные способы его применения. праггмческо^нятне Использование перечисления 1. Создайте новое консольное приложение по имени Сп05Ех02 и сохраните его в каталоге С:\BegVCSharp\Chapter05. 2. Добавьте в файл Program, cs следующий код: namespace Ch05Ex02 { enum orientation : byte { north = 1,
126 Часть I. Язык С# south = 2, east = 3, west = 4 } class Program { static void Main(string[] args) { orientation myDirection = orientation.north; Console.WriteLine("myDirection = {0}", myDirection) ; Console.ReadKey(); } 3. Запустите приложение. На рис. 5.6 показан вывод, который должен получиться. I B|file://AC:/B^VCSh*ayChapfertM«:h05Ej@2/...'—— 1 ■ \ 1 L^MIM^HHHHMH _^'\ ■ И d штат ^ ' Рис. 5.6. Приложение Ch05Ex02 в действии 4. Выйдите из приложения и измените его код следующим образом: byte directionByte; string directionString; orientation myDirection = orientation.north; Console.WriteLine("myDirection = {0}", myDirection); directionByte = (byte)myDirection; directionString = Convert.ToString(myDirection); Console.WriteLine("byte equivalent = {0}", directionByte); // байтовый эквивалент Console.WriteLine ("string equivalent = {0}", directionString) ; // строковый эквивалент Console.ReadKey(); 5. Запустите приложение снова. Теперь вывод должен получиться таким, как показано на рис. 5.7. Рис. 5.7. Вывод модифицированного приложения Ch05Ex02 Описание полученных результатов Приведенный код демонстрирует определение и применение типа перечисления по имени orientation. Первым, на что нужно обратить внимание, является то, что код определения типа размещается в отдельном пространстве имен Ch05Ex02, а не в том же месте, что и остальная часть кода. Объясняется это тем, что определения в принципе не выполняются, т.е. во время выполнения проход по коду в определении
Глава 5. Дополнительные сведения о переменных 127 не осуществляется, как по строкам кода остальной части приложения. Приложение начинает выполняться в привычном месте и имеет доступ к новому типу потому, что относится к тому же самому пространству имен. Первая итерация примера демонстрирует базовый метод создания переменной нового типа, присваивания ей значения и вывода этого значения на экран. Далее код изменяется так, чтобы он иллюстрировал преобразование значений перечисления в другие типы. Обратите внимание, что здесь требуются явные преобразования. Хотя базовым типом orientation и является byte, для преобразования значения myDirection в byte все равно нужно использовать приведение (byte): directionByte = (byte)myDirection; Точно такое же явное приведение необходимо и в обратном направлении, при желании преобразовать byte в orientation. Например, для преобразования переменной типа byte по имени myByte в переменную типа orientation и присваивания полученного значения переменной myDirection можно воспользоваться следующим кодом: myDirection = (orientation)myByte; Конечно, здесь нужно соблюдать осторожность, поскольку не все допустимые значения переменной типа byte отображаются на определенные значения переменной orientation. Тип orientation может хранить другие значения byte, поэтому никакая ошибка сразу не возникнет, но это может привести к нарушению логики позже в приложении. Для получения строковой версии значения перечисления можно применять команду Convert.ToString(): directionString = Convert.ToString(myDirection); Операция приведения (string) здесь работать не будет, поскольку необходимая обработка подразумевает нечто более чем просто помещение хранящихся в переменной типа перечисления данных в переменную string. В качестве альтернативного варианта можно использовать команду ToString () на самой переменной. Следующий код даст тот же самый результат, что и применение Convert. ToString (): directionString = myDirection.ToString(); Преобразование строки (string) в значение перечисления тоже возможно, но только требуемый для этого синтаксис выглядит немного сложнее. Для выполнения преобразования подобного рода предусмотрена специальная команда — Enum. Parse (), которая применяется следующим образом: (тип_перечисления) Enum. Parse (typeof (тип_перечисления) , строка_значения_перечисленния) ; Здесь используется еще одна операция— typeof, которая получает информацию о типе своего операнда. В случае типа orientation этим можно было бы воспользоваться следующим образом: string myString = "north"; orientation myDirection = (orientation) Enum. Parse (typeof (orientation), myString); Разумеется, не все строковые значения будут отображаться на значение orientation. В случае передачи значения, которое не отображается ни на одно из значений перечисления, будет выдаваться ошибка. Как и все остальные вещи в языке С#, такие значения являются чувствительными к регистру, поэтому ошибка будет выдаваться даже в том случае, если строка соответствует значению во всем, кроме регистра (например, в случае установки myString в North, а не в north).
128 Часть I. Язык С# Структуры Следующим видом переменных, который мы рассмотрим, являются структуры (struct). Структуры представляют собой именно то, о чем и говорит их название, т.е. структуры данных, состоящие из нескольких блоков данных, возможно, даже разного типа. Они позволяют определять свои собственные типы переменных на основании данной структуры. Например, предположим, что требуется сохранить информацию о маршруте к месту назначения из начальной точки, и что эта информация состоит из сведений о направлении и о расстоянии (в милях). Для простоты предположим, что направление соответствует позициям компаса и, соответственно, может быть представлено с помощью приведенного в предыдущем разделе перечисления orientation, и что исчисляемое в милях расстояние может быть представлено типом double. Тогда получается, что для сохранения этих сведений можно использовать две отдельных переменных: orientation myDirection; double myDistance; Ничего неправильного в применении двух таких переменных нет, но все-таки гораздо проще будет (особенно при необходимости сохранения информации о множестве маршрутов) использовать для сохранения этой информации одно место. Определение структур Структуры определяются с помощью ключевого слова struct показанным ниже образом: struct <имя_типа> { <объявления_ членов> } В разделе <объявления_членов> содержатся объявления переменных (называемых данными-членами структуры) в практически том же самом формате, что и обычно. Объявление каждого члена имеет следующий вид: <уровень_доступа> <тип> <имя>; Для предоставления вызывающему структуру коду возможности получать доступ к ее данным-членам на месте <уровень_доступа> помещается ключевое слово public. Например: struct route { public orientation direction; public double distance; } После определения типа структуры начинать пользоваться ее можно, определив переменные нового типа: route myRoute; Помимо этого, можно обращаться к данным-членам этой сложной переменной с помощью символа точки: myRoute.direction = orientation.north; myRoute.distance = 2.5; Все это иллюстрируется в следующем практическом занятии на примере использования ранее определенного перечисления orientation с показанной выше струк-
Глава 5. Дополнительные сведения о переменных 129 турой route, над которой в коде проводятся различные манипуляции, позволяющие получить представление о работе структуры. Использование структуры 1. Создайте новое консольное приложение по имени Ch05Ex03 и сохраните его в каталоге С:\BegVCSharp\Chapter05. 2. Добавьте в файл Program.cs следующий код: namespace Ch05Ex03 { enum orientation : byte { north = 1, south = 2, east = 3, west = 4 } struct route { public orientation direction; public double distance; } class Program { static void Main(string[] args) { route myRoute; int myDirection = -1; double myDistance; Console.WriteLine("l) North\n2) South\n3) East\n4) West"); do { Console.WriteLine("Select a direction:") ; // Выберите направление: myDirection = Convert. ToInt32 (Console. ReadLine ()) ; } while ((myDirection < 1) | | (myDirection > 4)) ; Console.WriteLine ("Input a distance: ") ; // Введите расстояние myDistance = Convert.ToDouble(Console.ReadLine ()) ; myRoute.direction = (orientation)myDirection; myRoute.distance = myDistance; Console.WriteLine ("myRoute specifies a direction of {0} and a " + "distance of {1}", myRoute.direction, myRoute.distance); // Вывод информации о направлении и расстоянии Console.ReadKey(); 3. Запустите приложение, а затем укажите направление вводом числа в диапазоне от 1 до 4 и расстояние. На рис. 5.8 показан результат, который должен получиться.
130 Часть I. Язык С# Рис. 5.8. Приложение Ch05Ex03 в действии Описание полученных результатов Структуры, как и перечисления, объявляются за пределами основного тела кода. В частности, в данном примере структура route объявляется прямо внутри того же пространства имен, что и перечисление orientation, использование которого она подразумевает: enum orientation : byte { north = 1, south = 2, east = 3, west = 4 } struct route { public orientation direction; public double distance; } Основное тело кода следует за структурой подобно тому, как уже показывалось в этой главе в другом примере, и предусматривает запрос входных данных у пользователя с последующим их отображением. Для вводимых пользователем данных обеспечивается выполнение простой верификации путем помещения отвечающего за выбор направления кода в цикл do, отклоняющий любые входные данные, которые не являются целым числом в диапазоне от 1 до 4 (такие значения были выбраны для того, чтобы они отображались на значения перечисления и тем самым упрощали выполнение операций присваивания). Входные данные, которые не будут распознаваться как целое число, приводят к ошибке. О том, почему это происходит, более подробно рассказывается позже в книге. Интересным моментом, на который стоит обратить внимание, является то, что при обращении к членам route, они обрабатываются в точности тем же образом, что и переменные того же типа обрабатывались бы в качестве члена. Операция присваивания выглядит так: myRoute.direction = (orientation)myDirection; myRoute.distance = myDistance; Входное значение можно и просто поместить непосредственно в myRoute. distance безо всяких негативных последствий: myRoute.distance = Convert.ToDouble(Console.ReadLine()); Такой дополнительный шаг обеспечил бы возможность выполнения дополнительной проверки правильности входных данных, но в коде такой проверки не преду-
Глава 5. Дополнительные сведения о переменных 131 смотрено. Любой доступ к членам структуры воспринимается единообразно. Можно сказать, что выражения вида structVar. memberVar в результате вычисления дают переменную типа memberVar. Массивы У всех рассмотренных до этого типов была одна общая черта: каждый из них позволял хранить одно единственное значение (или один единственный набор значений, если брать структуры). Однако бывают ситуации, в которых требуется сохранять много данных, и в таких ситуациях эти типы переменных являются не очень удобными. Иногда бывает необходимо сохранять несколько значений одного и того же типа одновременно без использования для каждого из них отдельной переменной. Например, предположим, что требуется выполнить некоторую обработку имен всех друзей. Можно объявить простые строковые переменные: string friendNamel = "Robert Barwell"; string friendName2 = "Mike Parry"; string friendName3 = "Jeremy Beacock"; Но такой подход, похоже, потребует приложения массы усилий, особенно из- за того, что для обработки каждой переменной придется писать отдельный код. Организовать проход по этому списку строк в цикле не получится. Однако существует и альтернативный подход, который предусматривает применение массива (array). Массивы — это проиндексированные списки переменных, хранящиеся в единственной переменной типа массива. Например, для хранения трех показанных выше имен можно создать массив под названием f riendNames. Получать доступ к отдельным членам этого массива можно будет указанием их индекса в квадратных скобках: f riendNames [<индекс>] Этот индекс представляет собой просто целое число. Для первого элемента в массиве этим числом будет 0, для второго — 1 и т.д. Это означает, что по элементам массива можно проходить в цикле: int i; for (i = 0; i < 3; i++) { Console.WriteLine("Name with index of {0}: {1}", i, friendNames[i]); // Вывод имени с определенным индексом } Массивы имеют один единственный базовый тип, т.е. все отдельные вхождения в массиве относятся к одному и тому же типу. Например, базовым типом приведенного здесь в качестве примера массива f riendNames является string, поскольку этот массив предназначен для хранения переменных типа string. Записи в массивах также часто называют элементами. Объявление массивов Объявляются массивы следующим образом: < базовый_ тип>[] <имя>; Здесь на месте <базовый_тип> может указываться любой тип переменных, включая такие демонстрировавшиеся ранее в главе типы, как перечисления и структуры. Массивы должны обязательно инициализироваться перед тем, как к ним можно будет получить доступ. Получать доступ или присваивать значения элементам массива, как показано ниже, нельзя:
132 Часть I. Язык С# int[] mylntArray; mylntArray[10] = 5; Инициализировать массивы можно двумя способами: указанием всего содержимого массива в литеральной форме либо указанием размера массива и применением ключевого слова new для инициализации всех его элементов. Спецификация массива с использованием литеральных значений подразумевает предоставление заключенного в фигурные скобки списка разделенных запятыми значений для элементов: int[] mylntArray = {5, 9, 10, 2, 99}; Здесь задается массив mylntArray, состоящий из пяти элементов, каждому из которых присваивается целочисленное значение. Второй подход требует применения следующего синтаксиса: int[] mylntArray = new int[5]; Здесь ключевое слово new используется для явной инициализации массива, а константное значение — для определения его размера. Такой подход приводит к присваиванию все членам массива значения по умолчанию, каковым для числовых типов является 0. Для такой инициализации можно также применять и переменные с неконстантными значениями: int[] mylntArray = new int[arraySize]; При желании оба подхода можно комбинировать: int[] mylntArray = new int [5] {5, 9, 10, 2, 99}; В случае применения такой техники размеры должны обязательно совпадать. То есть использовать следующую строку кода нельзя: int[] mylntArray = new int [10] {5, 9, 10, 2, 99}; Здесь массив определен как состоящий из 10 членов, но инициализированы только 5 членов, поэтому компиляция не удастся. Побочным эффектом такого подхода является еще и то, что если размер определяется через переменную, то эта переменная должна быть обязательно константной: const int arraySize = 5; int[] mylntArray = new int [arraySize] {5, 9, 10, 2, 99}; Если опустить слово const, код компилироваться не будет. Как и другие типы переменных, инициализировать массив в той же самой строке, в которой он объявляется, не обязательно. Например, следующий подход является вполне допустимым: int [ ] mylntArray; mylntArray = new int[5]; Теперь пришла пора испробовать какой-нибудь настоящий код. В следующем практическом занятии на приведенном в начале этого раздела примере демонстрируется создание и манипуляции массивом строк. практическое занятие Использование массива 1. Создайте новое консольное приложение по имени Ch05Ex04 и сохраните его в каталоге С: \BegVCSharp\Chapter05. 2. Добавьте в файл Program, cs следующий код:
Глава 5. Дополнительные сведения о переменных 133 static void Main(string[] args) { string[] friendNames = {"Robert Barwell", "Mike Parry", "Jeremy Beacock"}; int i; Console.WriteLine("Here are {0} of my friends:", friendNames.Length); // Вывод одного из друзей for (i =0; i < friendNames.Length; i++) { Console.WriteLine(friendNames[i]); } Console.ReadKey(); } 3. Запустите приложение. На рис. 5.9 показан результат, который должен получиться. Рис. 5.9. Приложение Ch05Ex04 в действии Описание полученных результатов В приведенном коде сначала создается массив string с тремя значениями, которые затем отображаются в цикле for. Обратите внимание, что использование friendNames .Length обеспечивает доступ к информации о количестве элементов в массиве: Console.WriteLine("Here are {0} of my friends:", friendNames.Length); Такой подход является удобным способом получения размера массива. Процесс вывода значений в массиве for можно легко сделать неправильным. Например, попробуйте изменить < на <=: for (i = 0; i <= friendNames.Length; i++) { Console.WriteLine(friendNames[i]); Компиляция этого кода приведет к получению такого диалогового окна, как показано на рис. 5.10. I IndtexOutOfRanQeGicepbon w*s unhtndtad Index was outside the bounds of the array. Troubleshooting lips: maximum index on a list is less than the list srce.j Make sure the index is not a negative number. , Make sure data column names are correct. : Get general help for this exception. Search for more Help Online... ; View Detail- Copy exception detail to the clipboard Puc. 5.10. Диалоговое окно с сообщением об ошибке выхода индекса за допустимые пределы
134 Часть I. Язык С# На этом рисунке видно, что была предпринята попытка получить доступ к f riendNames [3]. Напомним, что нумерация индексов в массиве начинается с О, а это значит, что в данном случае последним элементом является f riendNames [2]. Следовательно, получается, что элемент friendNames [3] выходит за рамки массива, а все попытки получить доступ к элементам, выходящими за пределы размера массива, приводят к невозможности выполнения кода. Однако существует еще один более гибкий способ получения доступа к членам массива и заключается он в применении циклов f oreach. Циклы £breach Цикл f oreach позволяет обращаться к каждому элементу в массиве с помощью такого простого синтаксиса: foreach ( <базовый_тип> <имя> in <массив> ) { II можно использовать <имя> для каждого элемента } Этот цикл будет осуществлять проход по всем элементам и помещать каждый из них по очереди в переменную <имя> безо всякого риска получения доступа к недопустимым элементам. Такой подход позволяет не беспокоиться о том, сколько элементов содержится в массиве, и быть уверенным, что каждый из них будет использован в цикле. В случае его применения код из предыдущего примера можно изменить следующим образом: static void Main(string[] args) { string[] friendNames = {"Robert Barwell", "Mike Parry", "Jeremy Beacock"}; Console.WriteLine("Here are {0} of my friends:", friendNames.Length) ; foreach (string friendName in friendNames) { Console.WriteLine(friendName); } Console.ReadKey(); } Вывод этого кода выглядит точно так же, как и вывод кода, который приводился в предыдущем практическом занятии. Главное отличие между применением цикла foreach и стандартного цикла for состоит в том, что foreach предоставляет к содержимому массива доступ только для чтения, что не позволяет изменять значения элементов. То есть выполнение следующей операции является недопустимым: foreach (string friendName in friendNames) { friendName = "Rupert the bear"; } Попытка сделать подобное приведет к невозможности компиляции. В случае использования простого цикла for, однако, присваивание значений элементам массива является возможным. Многомерные массивы Многомерным называется такой массив, в котором для получения доступа к элементам применяется несколько индексов. Например, предположим, что требуется нарисовать карту высот холма по замеренным позициям.
Глава 5. Дополнительные сведения о переменных 135 Позицию можно задать двумя координатами — х и у. Эти две координаты можно представить в виде индексов, дабы в массиве с именем hi 11 Height могли храниться значения высоты, соответствующие каждой паре координат. В такой ситуации может помочь многомерный массив. Массивы такого типа объявляются следующим образом: <базовый_тип> [,] <имя>; Массивы с большим количеством измерений объявляются с использованием большего количества запятых: <базовый_тип> [,,,] <имя>; Эта строка показывает, как объявить четырехмерный массив. Для присваивания значений применяется похожий синтаксис, в котором запятые служат для разделения размеров. Объявить и инициализировать двухмерный массив hi 11 Height с базовым типом double и размерностью координаты х, равной 3, а координаты у — 4, можно следующим образом: doublet,] hillHeight = new double[3,4]; В качестве альтернативного варианта для начального присваивания можно использовать литеральные значения. Эти значения должны иметь вид вложенных блоков, заключенных в фигурные скобки, и быть отделены друг от друга запятыми: doublet,] hillHeight = {{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}}; Данный массив имеет те же самые измерения, что и предыдущий, т.е. три строки и четыре столбца. Благодаря предоставлению литеральных значений, эти измерения определяются неявным образом. Для получения доступа к отдельным элементам многомерного массива указываются отделенные запятой индексы: hillHeight[2,1] Далее этими элементами можно манипулировать так же, как и всеми остальными. Приведенное выражение подразумевает доступ ко второму элементу третьего вложенного массива, который был определен ранее (т.е. к значению 4). Напомним, что отсчет начинается с 0, а первое число представляет вложенный массив. Другими Словами, первое число специфицирует пару фигурных скобок, а второе — на элемент внутри этой пары скобок. Графическим образом представить этот массив можно так, как показано на рис. 5.11. hillHeight [0,0] 1 hillHeight [1,0] 2 hillHeight [2,0] 3 hillHeight [0,1] 2 hillHeight [1,1] 3 hillHeight [2,1] 4 hillHeight [0,2] 3 hillHeight [1,2] 4 hillHeight [2,2] 5 hillHeight [0,3] 4 hillHeight [1,3] 5 hillHeight [2,3] 6 Рис. 5.11. Графическое представление массива hillHeight
136 Часть I. Язык С# Цикл f oreach предоставляет доступ ко всем элементам в многомерном массиве, точно так же как и в одномерных массивах: doublet,] hillHeight = {{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}}; foreach (double height in hillHeight) { Console.WriteLine ("{0} ", height); } Порядок, в котором элементы выводятся, совпадает с тем, что использовался для присваивания литеральных значений: hillHeight [0,0] hillHeight[0,l] hillHeight[0,2] hillHeight[0,3] hillHeight[l,0] hillHeight[l,l] hillHeight[l,2] Массивы массивов Многомерные массивы, о которых рассказывалось в предыдущем разделе, считаются прямоугольными, потому что каждая "строка" имеет одинаковый размер. Например, в последнем рассмотренном случае любой из возможных координат х могла соответствовать координата у от 0 до 3. Однако массивы еще могут быть зубчатыми, т.е. "строки" могут иметь разные размеры. Для этого каждый элемент в массиве должен являться тоже массивом. При желании можно также создавать и массивы массивов, состоящие из прочих массивов, или даже еще более сложные массивы. Однако все эти варианты возможны только при условии наличия у массивов одного и того же базового типа. Синтаксис для объявления массивов, состоящих из массивов, подразумевает указание в объявлении массива нескольких пар квадратных скобок: int[][] jaggedlntArray; К сожалению, инициализация подобных массив не является такой же простой процедурой, как инициализация многомерных массивов. Например, за приведенным выше объявлением ни в коем случае не может следовать такая строка кода: jaggedlntArray = new int[3][4]; Даже если бы это было возможно, приведенная строка не была бы особенно полезной, поскольку того же эффекта в случае использования многомерных массивов можно добиться и с гораздо меньшими усилиями. Нельзя применять и такую строку кода: jaggedlntArray = {{1, 2, 3}, {1}, {1, 2}}; Существуют всего два возможных варианта. Первый — инициализировать сначала массив, содержащий другие массивы (которые далее для ясности будут называться просто подмассивами), а затем по очереди инициализировать подмассивы: jaggedlntArray = new int[2][]; jaggedlntArray[0] = new int[3]; jaggedlntArray[1] = new int[4]; Второй вариант — использовать модифицированную версию приводившейся выше операции литерального присваивания: jaggedlntArray = new int[3] [] {new int[] {1, 2, 3}, new int[] {1}, new int[] {1, 2} };
Глава 5. Дополнительные сведения о переменных 137 Инициализировать массив можно и в той же строке, в которой он объявляется: int[][] jaggedlntArray = {newint[] {1, 2, 3}, newint[] {1}, new int[] {1, 2}}; С зубчатыми массивами можно использовать циклы f oreach, но их часто нужно делать вложенными для того, чтобы они добирались до фактических данных. Например, предположим, что имеется показанный ниже зубчатый массив, содержащий 10 других массивов, каждый из которых, в свою очередь, тоже содержит массив целых чисел, представляющих собой делители целого числа в диапазоне от 1 до 10: int[] [] divisorslTolO = {new mt[] {1}, new int[] new int[] new int[] new int[] {1, {1, {1, {1, 2], 3}, 2, 5}, 4 new int [ ] {1, 2, 3, 6}, new int[] {1, 7}, new int[] {1, 2, 4, 8}, new int[] {1, 3, 9}, new int[] {1, 2, 5, 10}}; Выполнение следующего кода завершится ошибкой: foreach (int divisor in divisorslTolO) { Console.WriteLine(divisor); } Объясняется это тем, что массив divisorslTolO содержит элементы int [ ], а не int. Поэтому вместо этого в цикле нужно проходить по каждому подмассиву, а также по самому массиву: foreach (int[] divisorsOfInt in divisorslTolO) { foreach(int divisor in divisorsOfInt) { Console.WriteLine(divisor); Как видно, синтаксис для использования зубчатых массивов может быстро становится довольно сложным. Из-за этого в большинстве случаев проще использовать прямоугольные массивы или какой-то другой более простой метод хранения. Однако все же могут встречаться ситуации, в которых без применения таких массивов не обойтись, и в таких ситуациях наличие соответствующих навыков совсем не повредит. Манипулирование строками Показанные до сих пор способы применения строк подразумевали лишь вывод строк в окне консоли, считывание строк из окна консоли и конкатенацию строк с помощью операции +. В процессе программирования более интересных приложений очень быстро станет очевидным, что манипулирование строками является одной из наиболее часто выполняемых операций. Из-за этого будет совсем не лишним потратить несколько страниц на рассмотрение некоторых наиболее распространенных приемов манипулирования строками, которые доступны в языке С#. Для начала обратим внимание на то, что переменная типа string может восприниматься как доступный только для чтения массив переменных char. Это означает, что доступ к отдельным символам можно получать с использованием приблизительно такого синтаксиса:
138 Часть I. Язык С# string myString = "A string"; char myChar = myString [1]; Однако присваивать отдельные символы подобным образом нельзя. Для получения массива char, пригодного для выполнения записи, можно применять показанный ниже код. В этом коде задействована такая команда переменной массива, как ToCharArrayO: string myString = "A string"; char[] myChars = myString.ToCharArray() ; После этого массивом char можно начинать манипулировать обычным образом. Также еще можно использовать строки в циклах f oreach, как показано ниже: foreach (char character in myString) { Console.WriteLine ("{0} ", character); } Как и в случае массивов, получать информацию о количестве элементов в строке также можно с помощью myString.Length: string myString = Console.ReadLine(); Console.WriteLine ("You typed {0} characters.", myString.Length); Другие базовые приемы манипулирования строками подразумевают использование команд в формате, подобном <строка>.ToCharArray (). К числу наиболее простых, но полезных из них относятся команды <строка>. ToLower () и <строка>. ToUpper (). Эти две команды позволяют преобразовывать строки, соответственно, в нижний и в верхний регистр. Чтобы понять, что в этом полезного, давайте предположим, что требуется обеспечить выполнение проверки на предмет наличия конкретного ответа от пользователя, например, строки yes. Преобразование вводимой пользователем строки в нижний регистр позволит также выполнять проверку на предмет наличия и таких строк, как YES, Yes, yeS и т.д. — подобный пример уже приводился в предыдущей главе. string userResponse = Console.ReadLine(); if (userResponse.ToLower () == "yes") { // Выполнение действия в ответ. } Эта команда, подобно прочим командам в настоящем разделе, на самом деле не изменяет ту строку, к которой применяется. Вместо этого комбинирование этой команды со строкой приводит к созданию новой строки, которая может далее либо сравниваться с еще какой-нибудь строкой (как это делается здесь), либо присваиваться другой переменной. В роли этой другой переменной может выступать та же самая переменная, над которой выполняются операции: userResponse = userResponse.ToLower(); Очень важно запомнить этот момент, поскольку использование просто такой строки кода: userResponse.ToLower() ; на самом деле не позволит получить никаких особых результатов. Существуют и другие вещи, которые можно делать для упрощения интерпретации вводимых пользователем данных. Что если пользователь случайно введет в начале или в конце входных данных лишний пробел? В таком случае приведенный код работать
Глава 5. Дополнительные сведения о переменных 139 не будет. Потребуется усечь введенную строку, что можно сделать с помощью команды <строка>.Тгл.т(): string userResponse = Console.ReadLine(); userResponse = userResponse.Trim() ; if (userResponse.ToLower() == "yes") { // Выполнение действия в ответ. } Применение такого подхода позволит также распознавать и такие строки, как: и YES" "Yes " Эти команды можно применять и для удаления любых других символов, указывая их в массиве char, например: char[] trimChars = {' ', 'е\ ' s'}; string userResponse = Console.ReadLine(); userResponse = userResponse.ToLower (); userResponse = userResponse.Trim(trimChars); if (userResponse == "y") { // Выполнение действия в ответ. } Этот код предусматривает устранение любых вхождений символов пробела, буквы е и буквы s в начале или конце строки. В случае отсутствия любых других символов в строке это будет приводить к распознаванию таких строк, как "Yeeeees" „ у„ и т.д. Еще можно применять команды <строка>. TrimStart () и <строка>. TrimEnd (), которые будут отсекать символы пробела, соответственно, в начале и конце строки. С ними тоже можно задавать массивы char. Существуют еще две других строковых команды, которые можно использовать для манипулирования пробелами в строках: <строка>. PadLef t () и <строка>. PadRight (). Эти команды позволяют добавлять пробелы слева и справа от строки для принудительного приведения ее к желаемой длине. Применяются они следующим образом: <строка>. PadX (<желаемая_длина>) ; Ниже приведен пример: myString = "Aligned"; myString = myString.PadLeft A0); Выполнение этого кода приведет к добавлению слева от слова Aligned в myString трех пробелов. Данные команды могут быть очень полезными при выравнивании строк в столбцах, что является особенно удобным для позиционирования строк, содержащих числа. Как и команды усечения, эти команды тоже можно использовать вторым способом, предоставляя в них символ, который должен применяться для дополнения строки. Указывать разрешено только один символ char, а не массив символов char, как в случае усечения: myString = "Aligned"; myString = myString.PadLeftA0, '-');
140 Часть I. Язык С# Выполнение этого кода приведет к добавлению в начале myString трех символов -. Существует еще очень много подобных команд для манипулирования строками, но большинство из них полезно только в специфических ситуациях. Поэтому они будут рассматриваться по мере их применения в последующих главах. Прежде чем продолжить, однако, стоить рассмотреть еще одну функциональную возможность, доступную как в Visual C# 2008 Express Edition, так и в Visual Studio 2008, действие которой уже можно было заметить в предыдущих главах и особенно в этой. В следующем практическом занятии демонстрируется функция автоматического завершения команд (auto- completion), посредством которой IDE пытается помогать разработчику, предлагая различные варианты кода, которые тот может захотеть вставить. Практическое занятие Автоматическое завершение операторов в VS 1. Создайте новое консольное приложение по имени Ch05Ex05 и сохраните его в каталоге С: \BegVCSharp\Chapter05. 2. Введите в файле Program, cs следующий код, в точности набирая указанные символы и обращая внимание на всплывающие окна, которые будут появляться по ходу: static void Main(string[] args) ( \ I 1 Щ Clone *] 1 1 ♦ CompareTo 1 1 % Contains 1 1 ♦ CopyTo *"! 1 1 ♦ EndsWith 1 I ♦ Equals 1 1 ♦ GetEnumerator 1 1 ! v GetHashCode 1 1 ♦ GetType 1 v GetTypeCode - : 1 1 * string myString = "This is a test."; char[] separator = {' '}; string[] myWords; myWords = myString. } 3. После ввода последней точки должно появиться окно, показанное на рис. 5.12. 4. Не передвигая курсор, введите s. Это приведет к измене- Рис. 5.12. Окно, потно внешнего вида отображающегося окна и появлению являющееся после вво- всплывающей подсказки, как показано на рис. 5.13 (эта да последней точки подсказка будет желтого цвета). 5. Введите следующие символы: (separator) ;. После этого всплывающее окно должно исчезнуть, а код будет выглядеть следующим образом: static void Main(string[] args) string myString = "This is a test."; char[] separator = {' '}; string [] myWords; myWords = myString.Split(separator); -* Pa die* *| \ ♦ PidRight 1 • Remove i V Replace *г*~шшшшш ; ♦ St»rtsWith 4 Substring El , * ToCh»rArr«y J '. v Tolower v Tolowerlnwanant - jStnngO string Split(stringD separator, int count, StringSplitOptions options) (+ 5 overload(s)) Returns a stnng array that contains the substrings in this string that are delimited by elements of a specified string array Parameters specify the maximum number of substrings to return and whether to return empty array elements [Exceptions System ArgumentOutOfRangeException System ArgumentException Puc. 5.13. Отображение всплывающей подсказки
Глава 5. Дополнительные сведения о переменных 141 6. Добавьте следующий код, обращая внимание на окна, которые будут появляться по мере его ввода: static void Main(string [ ] args) { string myStrmg = "This is a test."; char[] separator = {' '}; string[] myWords; myWords = myString.Split(separator); foreach (string word in myWords) { Console.WriteLine("{0}", word); } Console.ReadKey(); } 7. Запустите приложение. На рис. 5.14 показан результат, который должен получиться. Ш fHe:///C:/BeflVCShanVCh«Pt*rtM/Ch05Ex05/...' г">' W шОШ Рис. 5.14. Приложение Ch05Ex05 в действии Описание полученных результатов В приведенном коде важно обратить внимание на два таких главных момента: использование новой строковой команды и применение функции автоматического завершения кода. Новая строковая команда— <string>. Split () — преобразует строку string в массив string, разбивая на части в указанных точках. Эти точки принимают форму массива char, который в данном случае просто заполняется одним единственным элементом — символом пробела: char[] separator = {' '}; В следующем коде получаются подстроки, выходящие в результате разбиения строки после каждого пробела, т.е. массив отдельных слов: string[] myWords; myWords = myString.Split(separator); Далее все слова в этом массиве с помощью цикла foreach выводятся на консоль: foreach (string word in myWords) { Console.WriteLine ("{0}", word); } Каждое получаемое слово не содержит пробелов, ни внутри, ни в конце. Все они удаляются благодаря использованию команды Spli t (). Теперь рассмотрим то, что касается функции автоматического завершения кода. VS и VCE являются весьма интеллектуальным программным обеспечением, которое способно разбираться в большинстве касающихся кода деталей по мере его ввода. Даже когда вводится только первый символ в новой строке, IDE уже пытается помочь, предлагая различные варианты ключевых слов, имен переменных, имен типов и т.д.
142 Часть I. Язык С# Ввод в приведенном коде всего лишь трех букв str позволил IDE правильно догадаться о том, что требуется ввести слово string. Эта функция является еще более полезной при вводе имен переменных. При наборе длинных фрагментов кода имена переменных, которые требуется использовать, часто забываются. Благодаря тому, что IDE отображает по мере ввода всплывающий список всех возможных имен, имена требуемых переменных можно всегда легко отыскать, не возвращаясь и не отыскивая их в предыдущих фрагментах кода. К моменту ввода после myString символа точки IDE уже знает, что myString является строкой, понимает, что требуется указать строковую команду, и предлагает список возможных в таком случае вариантов. На этом этапе при желании можно прекратить ввод и просто выбрать в этом списке желаемую команду с помощью клавиш со стрелками вверх и вниз. По мере перехода по доступным вариантам IDE отображает описание выбранной в текущей момент команды и ее синтаксиса. В случае ввода дополнительных символов IDE автоматически перемещает выделение на самую вероятную из возможно подразумеваемых команд. После того, как IDE покажет нужную команду, можно продолжать ввод так, будто бы вводится полное имя, после чего ввод открывающей круглой скобки (() может привести прямо к тому этапу, на котором требуется указывать необходимую некоторым командам дополнительную информацию, и даже тогда IDE подскажет, в каком формате должна вводиться эта информация, отображая возможные варианты для команд, которые принимают различное количество информации. Эта функция IDE (известная как IntelliSense) чрезвычайно удобна и позволяет легко находить сведения о любых незнакомых типах. Если интересно, можете с ее помощью, например, просмотреть все команды, которые поддерживает тип string, и поэкспериментировать с ними. Иногда такая отображаемая информация может закрывать уже введенный код и тем самым раздражать, поскольку в коде могут содержаться детали, к которым необходимо обращаться при дальнейшем вводе. Нажатие клавиши <Ctrl> позволяет делать отображаемый список предлагаемых команд прозрачным и тем самым видеть код, который он скрывал. Данная чрезвычайно полезная возможность является новой и доступна только в версиях VS2008uVCE2008. Резюме Благодаря настоящей главе, вы расширили свои знания о переменных и заполнили некоторые из оставшихся пробелов. Пожалуй, наиболее важной из рассмотренных в этой главе является тема преобразования, поскольку она будет встречаться в настоящей книге повсеместно. Хорошее представление о связанных с нею концепциях на данном этапе намного облегчит процесс понимания прочих вещей позже. Помимо этого в главе рассматривались еще несколько типов переменных, с помощью которых можно хранить данные более удобным образом. В частности, здесь было рассказано о том, как перечисления могут делать код удобочитаемым за счет применения легко распознаваемых значений, как структуры могут использоваться для объединения множества связанных между собой элементов данных в одном месте и как разработчик может группировать похожие данные вместе посредством массивов. Переменные всех этих типов еще много раз будут применяться в оставшейся части книги. И, наконец, в завершение главы было рассказано о манипулировании строками, о базовых приемах и связанных с этим принципах. Для манипулирования строками доступно много отдельных строковых команд и в этой главе демонстрировались лишь
Глава 5. Дополнительные сведения о переменных 143 некоторые из них, но зато теперь вы знаете, как находить такие доступные команды в IDE-среде. Не бойтесь экспериментировать. По крайней мере, одно из следующих упражнений можно выполнить с использованием одной или более строковых команд, которые в главе не рассматривались, поэтому их нужно найти самостоятельно. Упражнения 1. Какая из следующих операций преобразования не может выполняться неявно: a)intв short б) short в int в) boolв string г) byte в float 2. Создайте на базе типа short код для перечисления color, содержащего все цвета радуги, а также черный и белый цвет. Может ли такое перечисление основываться на типе byte? 3. Измените приводившийся в предыдущей главе пример генератора множества Мандельброта так, чтобы в нем для сложных чисел использовалась следующая структура: struct imagNum { public double real, imag; } 4. Будет ли компилироваться приведенный ниже код? Обоснуйте ответ. string[] blab = new string[5] string[5] = 5th string. 5. Напишите консольное приложение, способное получать от пользователя строку и выводить ее с отображением символов в обратном порядке. 6. Напишите консольное приложение, способное получать строку и заменять в ней все вхождения слова по словом yes. 7. Напишите консольное приложение, способное заключать каждое слово в строке в двойные кавычки.
Функции Весь показанный до сих пор код имел вид одного блока, который иногда включал несколько циклов для повторения строк кода и ряд ветвлений для выполнения операторов в зависимости от соблюдения тех или иных условий. При необходимости выполнить над данными какую-то операцию, требуемый для этого код приходилось размещать именно в том месте, где он должен был работать. Такая структура кода является ограниченной. Довольно часто необходимо, чтобы некоторые задачи, вроде поиска наиболее высокого значения в массиве, например, выполнялись в нескольких точках в программе. Конечно, можно добавлять в приложение идентичные (или почти идентичные) разделы кода всякий раз, когда в этом возникает необходимость, но такой подход имеет свои недостатки. Изменение даже одной небольшой детали, касающейся общей задачи (для исправления ошибки в коде, например), в таком случае означает, что соответствующие изменения потребуется вносить во множество разделов кода, которые могут быть разбросаны по всему приложению. Пропуск одного из этих разделов может повлечь за собой серьезные последствия и сделать неработоспособным все приложение. К тому же это еще и существенно удлиняет приложение. Решить эту проблему позволяют функции. Функции в языке С# являются средствами предоставления блоков кода, которые могут выполняться в любой точке в приложении. Функции того конкретного типа, который изучается в настоящей главе, называются методами, но этот термин имеет очень специфическое значение в программировании для .NET, которое рассматривается позже в книге. Поэтому пока данный термин использоваться не будет. Например, можно создать функцию, вычисляющую максимальное значение в массиве. Далее ее можно применять из любой точки в коде и в каждом случае использовать для этого одинаковые строки кода. Поскольку предоставлять этот код нужно только один раз, любые вносимые в него изменения будут отражаться на данной операции вычисления при всяком ее применении. Другими словами, к этой функции можно относится как к содержащей многократно используемый код.
Глава 6. Функции 145 Еще одним преимуществом функций является то, что они могут делать код более читабельным, поскольку могут применяться для группирования связанных между собой фрагментов кода. Благодаря этому тело приложения сокращается за счет разделения внутренних действий. Такой подход подобен способу которым в IDE можно сворачивать разделы кода с помощью структурного представления, и делает структуру приложения более логичной. Функции еще также могут применяться для создания универсального кода, что позволяет им выполнять одни и те же операции над варьирующимися данными. Предоставлять необходимую для работы информацию функциям можно в виде параметров, а получать от них результаты — в виде возвращаемых значений. В предыдущем примере в роли параметра мог бы выступать подлежащий поиску массив, а в роли возвращаемого значения — максимальная величина в этом массиве. Это означает, что. одну и ту же функцию можно было бы использовать для работы каждый раз с другим массивом. Имя и параметры функции (но не ее возвращаемый тип) вместе называются сигнатурой функции. В частности, в настоящей главе рассматриваются следующие основные темы. □ Определение и использование простых функций, которые не принимают и не возвращают данных. □ Передача данных в и из функций. □ Область видимости переменных, отражающая то, как данные в приложении С# ограничиваются определенными разделами кода, что становится особенно важным, когда код разбивается на множество функций. □ Подробное рассмотрение такой важной функции в С#-приложениях, как Main (). Здесь вы узнаете, как с помощью встроенного поведения этой функции можно пользоваться аргументами командной строки, которые позволяют передавать информацию в приложения во время их работы. □ Еще одно свойство рассмотренного в предыдущей главе типа— структуры, заключающееся в том, что функции можно предоставлять в виде членов структур. Завершается настоящая глава рассмотрением двух более сложных тем — перегрузки функций и делегатов. Перегрузкой функций называется прием, позволяющий предоставлять несколько функций с одинаковым именем, но разными параметрами, а делегатом — тип переменной, позволяющий использовать функции косвенным образом. Делегат может применяться для вызова любой функции, возвращаемый тип и параметры которой соответствуют тем, что определены в нем, а это предоставляет возможность делать во время выполнения выбор между несколькими разными функциями. Определение и использование функций В этом разделе показано, как добавлять функции в приложения и затем использовать (вызывать) их из кода. Вначале демонстрируются простые функции, не предполагающие обмена никакими данными с вызывающим кодом, а затем рассматриваются более сложные функции и способы работы с ними. Приведенное ниже практическое занятие служит отправной точкой.
146 Часть I. Язык С# практическое заиятие[ Определение и использование базовой функции 1. Создайте новое консольное приложение по имени Ch06Ex01 и сохраните его в каталоге С: \BegVCSharp\Chapter06. 2. Добавьте в файл Program, cs следующий код: class Program { static void Write () { Console.WriteLine("Text output from function.") ; // Текстовые выходные данные из функции } static void Main(string[] args) { Write () ; Console.ReadKey(); } } 3. Запустите приложение. На рис. 6.1 показан результат, который должен получиться. Рис. 6.1. Приложение Ch06Ex01 в действии Описание полученных результатов В четырех следующих строках кода определяется функция с именем Write (): static void Write() { Console.WriteLine("Text output from function."); } Код этой функции просто выводит в окно консоли некоторый текст, но поведение на данный момент не является важным, поскольку пока интерес представляет лишь механизм определения и использования функций. Определение функции здесь состоит из следующих элементов. □ Два ключевых слова: static и void. □ Имя функции, за которым следуют круглые скобки — Write (). □ Заключенный в фигурные скобки блок кода, который подлежит выполнению. Имена функций обычно пишутся в формате PascalCasing. Код, определяющий функцию Write (), очень похож на другой код в приложении: static void Main(string[] args)
Глава 6. Функции 147 Это объясняется тем, что весь код, который доводилось писать до сих пор (кроме определений типов), был частью определенной функции, а именно — Main (), которая является для консольного приложения так называемой точкой входа. При выполнении любого приложения С# первым делом вызывается та содержащаяся в нем функция, которая выступает в роли точки входа, а при завершении выполнения этой функции завершается и работа всего приложения. Весь исполняемый код на С# должен обязательно иметь такую точку входа. Единственным отличием между функциями Main () и Write () (кроме содержащихся в них строк) является наличие внутри круглых скобок после имени функции Main некоторого кода. С помощью этого кода задаются параметры, о чем более подробно будет рассказываться чуть позже. Как уже упоминалось ранее, и Main (), и Write () определяются с ключевыми словами static и void. Ключевое слово static связано с концепциями объектно-ориентированного программирования и потому более подробно будет описываться позже в книге. Пока необходимо запомнить лишь то, что оно должно обязательно присутствовать во всех функциях, которые предлагается создавать в приложениях, описываемых в настоящем разделе данной книги. Ключевое слово void, однако, является более простым для объяснения. Оно служит для обозначения того, что функция не возвращает никакого значения. Чуть позже в этой главе обязательно будет показан тот код, который необходимо использовать в ситуации, если функция имеет возвращаемое значение. Далее идет код, вызывающий функцию, который выглядит следующим образом: Write (); Здесь просто вводится имя функции, а за ним — круглые скобки без содержимого. При достижении программой во время выполнения этой точки, будет запускаться код, содержащийся внутри функции Write (). Использование круглых скобок как в определении, так и в вызове функции, является обязательным. Если хотите, можете попробовать удалить их, и код перестанет компилироваться. Возвращаемые значения Наиболее простым способом для обмена данными с функцией является использование возвращаемого значения. Функции, имеющие возвращаемые значения, при оценке выдают именно эти значения, точно так же, как переменные, когда используются в выражениях, при оценке выдают значения, которые содержат. Как и переменные, возвращаемые значения имеют тип. Например, можно создать функцию GetString () с возвращаемым значением типа string. Использовать такую функцию в коде можно следующим образом: string myString; myString = GetString (); Можно также создать функцию GetVal () с возвращаемым значением типа double. Такая функция может участвовать в математическом выражении: double myVal; double multiplier = 5.3; myVal = GetVal() * multiplier; Обеспечение функции возвращаемым значением требует внесения двух следующих изменений. □ Указание в объявлении функции типа возвращаемого значения вместо ключевого слова void. □ Использование ключевого слова return для завершения выполнения функции и передачи возвращаемого значения в вызывающий код.
148 Часть I. Язык С# В случае функции консольного приложения рассматриваемого здесь типа в коде это будет выглядеть так: static <возвращаемый_тип> <имя_функции> {) { return <возвращаемое_значение> ; } Единственным ограничением здесь является то, что значение в <возвращаемое_ значение> должно обязательно либо быть значением типа <возвращаемый_тип>, либо неявно преобразовываться в значение такого типа. Однако в качестве <возвращаемый_ тип> может указываться какой угодно тип, включая и продемонстрированные более сложные типы. Например, все может выглядеть очень просто: static double GetVaK) { return 3.2; } Тем не менее, возвращаемые значения обычно являются результатом какой-то осуществляемой функцией обработки; показанного выше можно было бы легко добиться и с помощью переменной const. При достижении оператора return управление выполнением программы сразу же возвращается вызывающему коду. Никакие строки кода после этого оператора больше не выполняются, хотя это вовсе не означает, что операторы return могут размещаться только в последней строке тела функции. Оператор return можно использовать и ранее в коде, например, после выполнения какой-то логики ветвления. Размещение return в цикле for, блоке if или любой другой структуре будет приводить к немедленному выходу из структуры и завершению выполнения функции, как показано ниже: static double GetVaK) { double checkVal; // CheckVal присваивается значение посредством // определенной логики (которая здесь не показана) . if (checkVal < 5) return 4.7; return 3.2; } Здесь возвращаться может одно из двух значений, в зависимости от значения checkVal. Единственным ограничением в данном случае является то, что оператор return должен обрабатываться перед достижением закрывающей фигурной скобки } в функции. Следующий код недопустим: static double GetValO { double checkVal; // CheckVal присваивается значение посредством определенной логики. if (checkVal < 5) return 4.7; } Здесь получается, что в случае, если значение checkVal >= 5, не будет выполнено никакого оператора return, что является недопустимым. Все пути обработки должны обязательно приводить к оператору return. В большинстве случаев компилятор будет распознавать эту проблему, и возвращать ошибку типа not all code paths return a value (не все пути кода возвращают значение).
Глава 6. Функции 149 И, наконец, последнее: оператор return может применяться в функциях, которые объявляются с использованием ключевого слова void (т.е. не имеют возвращаемого значения). Такие функции будут просто завершаться. При использовании оператора return подобным образом указание возвращаемого значения между ключевым словом return и следующим за ним символом точки с запятой считается ошибкой. Параметры Когда функция должна принимать параметры, должно быть указано следующее: □ список принимаемых функцией параметров вместе с их типами в определении этой функции; □ соответствующий список параметров в каждом вызове этой функции. Это подразумевает использование показанного ниже кода, в котором может присутствовать любое количество параметров, при этом для каждого из них должен указываться тип и имя: static <возвращаемый_тип> <имя_функции> ( <тип_параметра> <имя_параметра>, ...) { return <возвращаемое_значение>; } Параметры отделяются друг от друга запятыми, и каждый из них доступен в коде функции через переменную. Например, простая функция может брать два параметра double и возвращать результат их умножения: static double Product(double paraml, double param2) { return paraml * param2; } Более сложный пример приведен в следующем практическом занятии. 5—"-4 Обмен данными с функцией (часть 1) 1. Создайте новое консольное приложение по имени Ch06Ex02 и сохраните его в каталоге C:\BegVCSharp\Chapter06. 2. Добавьте в файл Program.cs следующий код: class Program { static int MaxValue (int [ ] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray[i]; } return maxVal;
150 Часть I. Язык С# static void Main(string [ ] args) { int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; int maxVal = MaxValue(myArray) ; Console.WriteLine("The maximum value in myArray is {0}", maxVal); // Вывод максимального значения в массиве myArray Console.ReadKey (); 3. Запустите приложение. На рис. 6.2 показан результат, который должен получиться. ■ «*^//C:/B^|VCShwp«:r»|)t*f1)e«:h0eEx02/.. I 1га1вв Рис. 6.2. Приложение Ch06Ex02 в действии Описание полученных результатов В приведенном коде содержится функция, которая делает то, что должна была делать функция, приведенная в качестве примера в начале главы, а именно— принимает массив целых чисел в виде параметра и возвращает максимальное число в нем. Определение этой функции выглядит так: static int MaxValue (int [ ] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray [i]; } return maxVal; } Здесь видно, что эта функция называется MaxValue () и имеет один определенный параметр — массив типа int под названием intArray, а также возвращаемый тип int. Процесс вычисления максимального значения выглядит очень просто. Сначала локальная переменная типа int по имени maxVal инициализируется первым значением из массива, после чего это значение сравнивается с каждым из остальных элементов в массиве. Если в каком-то из элементов содержится более высокое значение, чем в maxVal, тогда оно делается текущим значением maxVal. В результате по завершении цикла maxVal содержит максимальное значение из имеющихся в массиве и возвращается с помощью оператора return. В коде Main () объявляется и инициализируется простой массив целых чисел (int), который будет использоваться с функцией MaxValue (): mt[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; Вызов MaxValue () применяется для присваивания значения int-переменной maxVal: int maxVal = MaxValue(myArray); Далее это значения выводится на экран с помощью Console .WriteLine (): Console.WriteLine ("The maximum value in myArray is {0}", maxVal);
Глава 6. Функции 151 Соответствие параметров При вызове функции параметры должны указываться в точности, как в определении функции, т.е. соответствующего типа, в соответствующем количестве и соответствующем порядке. Например, функцию static void MyFunction(string myString, double myDouble) { } нельзя вызывать так: MyFunctionB.6, "Hello"); Здесь в качестве первого параметра передается значение double, а в качестве второго — значение string, что не соответствует порядку, в котором эти параметры были указаны в определении функции. Эту функцию также нельзя вызывать и следующим образом: MyFunction("Hello"); Здесь передается только один параметр string, а согласно определению функции, параметров должно быть два. При попытке использовать любой из двух приведенных выше вызовов функции компилятор будет сообщать об ошибке, вынуждая соблюдать соответствие сигнатурам применяемых функций. Как уже говорилось, под сигнатурой подразумевается имя и параметры функции. Что касается приведенной ранее в качестве примера функции MaxValue (), то она может использоваться только для получения максимального значения int в массиве значений int. В случае изменения кода в Main () следующим образом: static void Main(string[] args) { doublet] myArray = {1.3, 8.9, 3.3, 6.5, 2.7, 5.3}; double maxVal = MaxValue(myArray); Console.WriteLine("The maximum value in myArray is {0}", maxVal); Console.ReadKey (); } он не будет компилироваться из-за того, что указан параметр не того типа. Чуть позже в разделе "Перегрузка функций" будет описан один полезный прием, обеспечивающий обходной путь для решения этой проблемы. Массивы параметров Язык С# позволяет указывать один (и только один) специальный параметр для функции. Этот параметр называется массивом параметров-, он должен задаваться последним в определении функции. Массивы параметров позволяют вызывать функции с использованием различного количества параметров и определяются с помощью ключевого слова params. Массивы параметров могут служить удобным способом для упрощения кода, поскольку их не нужно передавать из вызывающего кода. Вместо этого в них можно просто помещать несколько параметров одинаково типа и затем обращаться к ним из функции. Код, который необходим для определения функции, использующей массив параметров, выглядит так:
152 Часть I. Язык С# static <возвращаемый_тип> <имя_фуикции> (<тип_параметра1> <имя_параметра1> , ... , params <тип>[] <имя>) return <возвращаемый_тип>; } Код, с помощью которого можно вызывать эту функцию, выглядит следующим образом: <имя_функции> ( <параметр1>, ..., <значение1>, <значение2>, ...) Здесь <значение2>, <значение2> и т.д. представляют собой значения типа <тип>, которые используются для инициализации массива <имя>. Количество параметров, которые можно указывать здесь, практически бесконечно; единственным ограничением является то, что все они обязательно должны быть параметрами типа <тип>. Можно вообще не указывать никаких параметров. Последнее свойство делает массивы параметров особенно полезными для предоставления функциям дополнительной информации, которая используется во время обработки. Например, предположим, что есть функция по имени GetWordO , которая принимает строку (значение string) в качестве первого параметра и возвращает первое слово в этой строке: string firstWord = GetWord("This is a sentence."); В данном случае firstWord будет присвоена строка This. Добавив в эту функцию GetWord () параметр params, можно сделать так, чтобы она позволяла при желании возвращать какое-то другое слово за счет указания его индекса: string firstWord = GetWord("This is a sentence.", 2); При условии, что отсчет слов начинается с 1 (для первого слова), в данном случае это приведет к присваиванию firstWord строки is. Помимо этого, с помощью params также можно ограничить количество возвращаемых символов, указывая его в третьем параметре: string firstWord = GetWord("This is a sentence.", 4, 3) ; В данном случае firstWord будет присвоена строка sen. Теперь давайте рассмотрим полноценный пример. В следующем практическом занятии демонстрируется определение и использование функции с параметром params. тшщтттшщщтшяшштшшштштшшшшштшшщшштшшттштт ^^ Обмен данными с функцией (часть 2) 1. Создайте новое консольное приложение по имени СпОбЕхОЗ и сохраните его в каталоге C:\BegVCSharp\Chapter06. 2. Добавьте файл Program.cs следующий код: class Program { static int SumVals(params int[] vals) { int sum = 0; f о reach (int val in vals) { sum += val; } return sum; }
Глава 6. Функции 153 static void Main(string [ ] args) { int sum = SumVals(l, 5, 2, 9, 8) ; Console.WriteLine("Summed Values = {0}", sum) ; // вывод суммы значений Console.ReadKey(); } } 3. Запустите приложение. На рис. 6.3 показан результат, который должен получиться. Рис. 6.3. Приложение Ch06Ex03 в действии Описание полученных результатов В этом примере функция SumVals () определяется с использованием ключевого слова params так, чтобы она могла принимать любое количество параметров типа int (и больше никаких других типов): static int SumVals(params int[] vals) { Код в этой функции просто проходит по значениям в массиве vals и складывает их вместе, возвращая результат. В Main () осуществляется вызов этой функции с пятью целочисленными параметрами: int sum = SumVals A, 5, 2, 9, 8); Обратите внимание, что эту функцию можно было бы точно так же легко вызвать и без параметров или с одним, двумя или даже сотней целочисленных параметров — предела количеству передаваемых параметров не существует. Параметры-ссылки и параметры-значения Во всех демонстрировавшихся до сих пор функциях использовались параметры- значения (value parameters). To есть при каждом применении параметров используемой внутри функции переменной передавалось значение. Никакие изменения, вносимые в эту переменную в функции, не оказывали влияние на параметр, передаваемый в вызове функции. Например, возьмем показанную ниже функцию, которая удваивает и отображает значение переданного ей параметра на экране: static void ShowDouble(int val) { val *= 2; Console.WriteLine("удвоенное значение val = {0}", val); } Здесь видно, что в этой функции производится удваивание параметра val. Следовательно, в случае показанного ниже вызова этой функции:
154 Часть I. Язык С# int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); ShowDouble(myNumber); Console.WriteLine("myNumber = {0}", myNumber); текстовый вывод в окне консоли будет выглядеть так: myNumber = 5 удвоенное значение val = 10 myNumber = 5 Вызов ShowDouble () с myNumber в качестве параметра никак не влияет на значение myNumber в Main (), хотя параметр val, которому оно присваивается, умножается на 2. Все это замечательно, но если все-таки нужно, чтобы значение myNumber изменялось, возникает проблема. При желании ее можно решить применением функции, возвращающей новое значение для myNumber, как показано ниже: static int DoubleNum(int val) { val *= 2; return val; } Для вызова этой функции можно использовать такой код: int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); myNumber = DoubleNum(myNumber); Console.WriteLine("myNumber = {0}", myNumber); Однако такой код труден для восприятия и не справляется с изменением значений множества переменных, передаваемых в виде параметров (поскольку функции возвращают единственное значение). Поэтому передавать параметр необходимо по ссылке (reference). Благодаря этому функция будет работать с той же переменной, которая указывается в вызове функции, а не просто переменной, которая имеет точно такое же значение. В результате любые вносимые в эту переменную изменения будут отражаться и в переменной, переданной в качестве параметра. Все, что для этого требуется — применить для параметра ключевое слово ref: static void ShowDouble (ref int val) { val *= 2; Console.WriteLine("удвоенное значение val = {0}", val); } Затем использовать его опять в вызове функции (обязательно!): int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); ShowDouble (ref myNumber) ; Console.WriteLine("myNumber = {0}", myNumber); Тогда текстовый вывод в окне консоли будет выглядеть следующим образом: myNumber = 5 удвоенное значение val = 10 myNumber =10 На этот раз выполнение функции ShowDouble () повлияло на значение myNumber. Важно обратить внимание на два ограничения, которые следует соблюдать в случае использования переменной в качестве параметра ref. Во-первых, функция может
Глава 6. Функции 155 приводить к изменению значения параметра-ссылки, поэтому в вызове функции должна обязательно использоваться только неконстантная переменная. Следовательно, такой код является недопустимым: const int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); ShowDouble(ref myNumber); Console.WriteLine("myNumber = {0}", myNumber); Во-вторых, используемая переменная должна обязательно инициализироваться. Язык С# не позволяет предполагать, что параметр ref будет инициализироваться в той функции, которая его использует. То есть следующий код тоже является недопустимым: int myNumber; ShowDouble(ref myNumber); Console.WriteLine("myNumber = {0}", myNumber); Выходные параметры Помимо передачи значений по ссылке можно еще также указывать, что данный параметр является выходным параметром, используя ключевое слово out, которое применяется тем же способом, что и ключевое слово ref (т.е. в виде модификатора перед параметром как в определении, так и в вызове функции). В сущности, он обеспечивает точно такое же поведение, как и параметр-ссылка, в том, что касается возврата значения параметра в конце выполнения функции использовавшейся в вызове функции переменной. Однако существуют два важных отличия. □ В качестве параметра ref применять неинициализированную переменную нельзя, а в качестве параметра out — можно. □ Параметр out должен трактоваться как неприсвоенное значение функцией, которая его использует. Это означает, что хотя использовать неинициализированную переменную в качестве параметра out в вызывающем коде допускается, хранящееся в этой переменной значение при выполнении функции будет утрачиваться. Для примера рассмотрим функцию MaxValue (), которая уже демонстрировалась ранее и возвращает максимальное значение в массиве. Изменим ее так, чтобы она позволяла получать индекс того элемента, в котором находится максимальное значение. Чтобы не усложнять пример, пусть это будет индекс только первого вхождения этого значения при наличии множества элементов с таким же максимальным значением. Чтобы добиться такого поведения, достаточно добавить параметр out, изменив функцию, как показано ниже: static int MaxValue (int [] intArray, out int maxlndex) { int maxVal = intArray[0]; maxlndex = 0; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) { maxVal = intArray[i]; maxlndex = i; } } return maxVal; }
156 Часть I. Язык С# Использовать эту функцию можно так: int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, О, 2} ; int maxIndex; Console.WriteLine("Максимальное значение myArray равно {0}", MaxValue(myArray, out maxlndex)); Console.WriteLine("Первое вхождение этого значения встречается в элементе {0}", maxlndex + 1) ; Это приведет к получению следующего вывода: Максимальное значение myArray равно 9 Первое вхождение этого значения встречается в элементе 7 Обратите внимание на обязательное использование в вызове функции ключевых слов out и ref. К возвращенному здесь значению maxlndex при отображении на экране была добавлена единица. Это сделано для того, чтобы преобразовать индекс в более привычную форму, когда отсчет начинается с 1, а нес 0. Область видимости переменных По мере прочтения предыдущего раздела наверняка возник вопрос о том, зачем необходим обмен данными с функциями. Все дело в том, что переменные в С# доступны только из локализованных областей кода. Говорят, что у каждой взятой переменной имеется так называемая область видимости, или контекст (scope), в пределах которой к ней можно получать доступ. Область видимости переменных является важной темой, объяснить которую легче всего на примере. В следующем практическом занятии демонстрируется ситуация с определением переменной в одной области видимости и попыткой получения к ней доступа из другой области. практическое занятие! Определение и использование базовой функции 1. Внесите в файл Program, cs приложения Ch06Ex01 следующие изменения: class Program { static void Write () { Console.WriteLine("myString = {0}", myString); } static void Main(string[] args) { string myString = "String defined in Main() "; // Строка, определенная в функции Main() Write (); Console.ReadKey() ; } } 2. Скомпилируйте этот код и обратите внимание на сообщение об ошибке и предупреждение: The name 'myString' does not exist in the current context The variable 'myString' is assigned but its value is never used Имя 'myString' не существует в текущем контексте Переменная 'myString' присвоена, но ее значение никогда не используется
Глава 6. Функции 157 Описание полученных результатов Что же пошло не так? Дело в том, что переменная myString, определенная в основном теле приложения (в функции Main ()), не доступна внутри функции Write (). Объясняется такая недоступность тем, что у всех переменных имеется область видимости, в пределах которой они являются действительными. Область видимости охватывает блок кода, в котором переменная определяется, а также любые блоки, непосредственно вложенные в него. Блоки кода в функциях идут отдельно от блоков кода, из которых они вызываются. Внутри функции Write () имя myString нигде не определено, а раз так, то получается, что переменная myString, определенная в Main (), находится за пределами области видимости, т.е. может использоваться только в пределах функции Main (). Тем не менее, в функции Write () может присутствовать своя, совершенно отдельная переменная по имени myString. Попробуйте изменить предыдущий код, как показано ниже: class Program { static void Write () { string myString = "String defined in Write () " ; // Строка, определенная в функции Write() Console.WriteLine("Now in Write() ") ; // Теперь в функции Write () Console.WriteLine("myString = {0}", myString); } static void Main(string[] args) { string myString = "String defined in MainO"; Write () ; Console.WriteLineС\nNow in Main()"); Console.WriteLine("myString = {0}", myString); Console.ReadKey(); } } Этот код успешно скомпилируется и приведет к получению вывода, показанного на рис. 6.4. Ниже перечислены действия, выполняемые этим кодом. □ Сначала функция Main () определяет и инициализирует строковую переменную по имени myString. □ Затем функция Main () передает управление функции Write (). □ Функция Write () определяет и инициализирует строковую переменную myString, которая отличается от той, что была определена в Main (). □ Далее функция Write () выводит на консоль строку, содержащую значение той переменной myString, которая была определена в ней. □ После этого функция Write () передает управление обратно функции Main (). □ Функция Main () выводит на консоль строку, содержащую значение той переменной myString, которая была определена в ней. Переменные, область видимости которых распространяется подобным образом только на одну единственную функцию, называются локальными переменными. Однако также допускается использовать и глобальные переменные, область видимости которых распространяется на несколько функций.
158 Часть I. Язык С# Попробуйте изменить код следующим образом: class Program { static string myString; static void Write () { string myString = "String defined in Write()"; Console.WriteLine("Now in Write ()"); Console.WriteLine("Local myString = {0}", myString); // Локальная переменная Console.WriteLine ("Global myString = {0}", Program.myString) ; // Глобальная переменная } static void Main(string[] args) { string myString = "String defined in MainO"; Program.myString = "Global string"; Write () ; Console. WriteLine ("\nNow in MainO") ; Console.WriteLine ("Local myString = @)", myString) ; Console.WriteLine ("Global myString = {0}", Program.myString) ; Console.ReadKey() ; } } Результат, который получится в этом случае, показан на рис. 6.5 Здесь была добавлена еще одна переменная с именем myString, на этот раз чуть выше в иерархии имен в коде. Определение этой переменной выглядит следующим образом: static string myString; Обратите внимание на то, что здесь опять присутствует ключевое слово static. Вкратце это объясняется тем, что в консольном приложении такого типа для глобальных переменных подобного вида нужно обязательно использовать либо ключевое слово static, либо ключевое слово const. При желании изменить значение глобальной переменной, следует применять именно ключевое слово static, поскольку const не допускает изменения значения переменной. Чтобы различить данную глобальную переменную и локальные переменные с таким же именем в функциях Main () и Write (), необходимо использовать для нее полностью квалифицированное имя, как было описано в главе 3. Рис. 6.4. Вывод после изменения кода Рис. 6.5. Использование локальных и глобальных переменных
Глава 6. Функции 159 Здесь для ссылки на глобальную переменную используется имя Program.myString. Подобное необходимо делать только в том случае, когда есть и глобальные, и локальные переменные с одинаковыми именами; если бы локальной переменной myString не было, для обозначения глобальной переменной можно было бы применять и просто myString, а не Program.myString. При наличии локальной переменной с таким же именем, как и у глобальной, глобальная переменная считается скрытой. Значение глобальной переменной устанавливается в функции Main () с помощью такой строки кода: Program.myString = "Global string"; Доступ к ней осуществляется в функции Write () следующим образом: Console.WriteLine("Global myString = {0}", Program.myString); Может возникнуть вопрос, почему нельзя просто использовать этот прием для обмена данными с функциями вместо показанного ранее приема с передачей параметров? На самом деле бывают ситуации, когда такой подход действительно является предпочтительным способом для обмена данными, но часто случается, что это не так. Выбор того, стоит ли применять глобальные переменные, зависит от предполагаемого использования задействованной функции. Проблемой глобальных переменных является то, что они обычно не подходят для "универсальных" функций, способных работать с любыми передаваемыми им данными, а не только с теми, что содержатся в конкретной глобальной переменной. Об этом более подробно будет рассказываться позже в главе. Область видимости переменных в других структурах Прежде чем продолжить, нужно обязательно обратить внимание на то, что один из описанных в предыдущем разделе моментов влияет не только на область видимости переменных между функциями. Было сказано, что области видимости переменных охватывают как те блоки кода, в которых они определяются, так и любые непосредственно вложенные в них блоки. Это также касается и других блоков, вроде тех, что содержатся в структурах типа ветвлений и циклов. Рассмотрим следующий код: int i; for (i = 0; i < 10; i++) { string text = "Line " + Convert.ToString(i); Console.WriteLine("{0}", text); } Console.WriteLine("Last text output in loop: {0}", text); // Вывод в цикле последних введенных текстовых данных Здесь строковая переменная text является локальной по отношению к циклу for. Скомпилировать этот код не получится, поскольку вызов функции Console. WriteLine (), происходящий за пределами данного цикла, приведет к попытке использования переменной text, что будет уже за рамками ее области видимости, поскольку эта операция происходит за пределами цикла. Давайте попробуем изменить этот код следующим образом: int i; string text; for (i = 0; i < 10; i++) { text = "Line " + Convert.ToString(i); Console.WriteLine("{0}", text); } Console.WriteLine("Last text output in loop: {0}", text);
160 Часть I. Язык С# В этом случае скомпилировать код тоже не получится, поскольку переменные перед использованием должны обязательно объявляться и инициализироваться, а переменная text в цикле for только инициализируется. Из-за этого при выходе из цикла присвоенное ей значение теряется. Однако можно внести в этот код такое изменение: int lustring text = ""; for A = 0; i < 10; i++) { text = "Line " + Convert.ToString(i); Console.WriteLine("{0}", text); } Console.WriteLine("Last text output in loop: {0}", text); На этот раз переменная text инициализируется за пределами цикла и имеется доступ к ее значению. Результат выполнения этого простого кода показан на рис. 6.6. Рис. 6.6. Результат выполнения измененного кода Здесь последнее значение, присваиваемое text в цикле, доступно и за пределами цикла. Очевидно, что понимание данной темы требует приложения несколько больших усилий. На первый взгляд не совсем ясно, почему, с учетом предыдущего примера, у переменной text после цикла не остается в качестве значения та пустая строка, которая была присвоена ей перед началом цикла в коде. Причина такого поведения связана с выделением памяти для переменной text, как, по сути, это делается для любой другой переменной. Одно лишь объявление переменной простого типа ни к каким особым действиям не приводит. Только при присваивании переменным значений для их хранения в памяти выделяется место. Когда эта операция по выделению памяти происходит внутри цикла, значение определяется как локальное значение и тогда за пределами цикла оно покидает свою область видимости. Даже хотя сама переменная не ограничивается циклом, то значение, которое в ней содержится, ограничивается им. Присваивание значения переменной за пределами цикла, однако, делает его локальным по отношению к главному коду и при этом все равно оставляет действительным внутри цикла. Это означает, что переменная не выходит за рамки области видимости до тех пор, пока не завершится выполнения основного блока кода, благодаря чему к содержащемуся в ней значению можно получать доступ и за пределами цикла. К счастью для программистов, компилятор С# обнаруживает проблемы, связанные с областью видимости переменных, а реагирование на генерируемые им сообщения об ошибках, действительно позволяет лучше разобраться в этой теме. И, наконец, конечно же, нужно сказать о том, какой подход является рекомендуемым. В целом считается, что лучше объявлять и инициализировать все переменные перед теми блоками кода, в которых они будут использоваться. Исключением являются лишь переменные, применяемые внутри цикла, каковые удобнее объявлять в виде части представляющего цикл блока:
Глава 6. Функции 161 for (int i = 0; l < 10; i++) Здесь i ограничивается блоком реализующего цикл кода, но в это нет ничего страшного, поскольку доступ к этой переменной-счетчику из внешнего кода требуется крайне редко. Параметры и возвращаемые значения вместо глобальных данных В этом разделе более подробно анализируются преимущества и недостатки обмена данными с функциями через глобальные данные и через параметры и возвращаемые значения. Чтобы вспомнить, о чем идет речь, взгляните на следующий код: class Program { static void ShowDouble (ref int val) { val *= 2; Console.WriteLine("удвоенное значение val = {0}", val) ; } static void Main(string [ ] args) { int val = 5; Console.WriteLine("val = {0}" , val); ShowDouble (ref val) ; Console.WriteLine("val = {0}", val); Этот код немного отличается от продемонстрированного в главе ранее, на примере использования переменной myNumber в функции Main (). Он иллюстрирует тот факт, что переменные вполне могут иметь идентичные имена и при этом совершенно не мешать друг другу. Это делает его более схожим со вторым приводимым здесь примером кода, что дает возможность сфокусироваться на конкретных отличиях и не обращать внимания на имена переменных. Теперь сравните его с таким кодом: class Program { static int val; static void ShowDouble () { val *= 2; Console.WriteLine ("удвоенное значение val = {0}"f val) ; } static void Main(string [ ] args) { val = 5; Console.WriteLine("val = {0}", val); ShowDouble (); Console .WriteLine ("val = {0}" , val) ;
162 Часть I. Язык С# Результаты вызова обеих показанных здесь функций ShowDouble будут выглядеть идентично. Никаких жестких правил касательно того, что следует применять именно тот подход, а не другой, не существует. Оба они абсолютно допустимы, но, возможно, следующие рекомендации окажутся полезными. Прежде всего, как уже упоминалось, когда данная тема затрагивалась впервые, версия ShowDouble (), в которой используется глобальное значение, требует обязательного применения глобальной переменной val. Это немного ограничивает разносторонность функции и означает, что при желании сохранять результаты придется постоянно копировать значение этой глобальной переменной в другие переменные. Помимо этого, при таком подходе не исключена вероятность изменения глобальных данных в каком-нибудь месте в приложении, что может привести к непредсказуемым результатам (например, изменению значений без выявления проблемы до тех пор, пока не станет слишком поздно). Однако такая потеря универсальности зачастую может превращаться в преимущество. Нередко функцию требуется использовать только для какой-то одной цели, а применение хранилища глобальных данных снижает вероятность допущения в вызове функции той или иной ошибки, например, передачи не той переменной. Конечно, также можно и возразить, что подобная простота на самом деле усложняет восприятие кода. Явное указание параметров позволяет сразу же видеть, что изменяется. Например, один взгляд на вызов типа myFunction (vail, out val2) дает возможность понять, что vail и val2 являются теми важными переменными, на которые следует обратить внимание, и что по завершении выполнения функции переменной val2 будет присваиваться новое значение. Если бы функция не принимала никаких параметров, сделать подобные предположения о том, какие данные будут обрабатываться, было бы не возможно. Напоследок запомните, что применение глобальных данных не всегда представляется возможным. Позже в этой книге еще будет демонстрироваться код, хранящийся в разных файлах и/или относящийся к разным пространствам имен, которые взаимодействуют друг с другом посредством функций. В подобных случаях код часто разбивается до такой степени, что никакого очевидного варианта для места размещения глобального хранилища просто нет. Таким образом, получается, что для обмена данными можно свободно применять любой из описанных подходов. В целом рекомендуется использовать параметры, а не глобальные данные. Но, разумеется, встречаются случаи, когда подход с применением глобальных данных может оказаться более подходящим и его использование точно не будет ошибкой. Функция Main () Теперь, когда было рассмотрено большинство из простых приемов, применяемых для создания и использования функций, пришла пора более подробно рассмотреть функцию Main (). Ранее уже упоминалось о том, что функция Main () является точкой входа в любом С#-приложении и что ее выполнение охватывает выполнение всего приложения. То есть запуск приложения приводит к началу выполнения функции Main (), а завершение его работы — к завершению ее выполнения. Функция Main () может возвращать либо void, либо значение int, а также необязательно включать параметр string [ ] args, что дает возможность использовать ее в любой из следующих версий:
Глава 6. Функции 163 static void Main() static void Main(string[] args) static int Main () static int Main(string[] args) В третьей и четвертой версии она возвращает значение int, которое может использоваться для уведомления о том, как завершается работа приложения, и зачастую применяется как признак ошибки (разумеется, это не является обязательным). Как правило, возврат значения 0 свидетельствует о нормальном завершении работы приложения (т.е. приложение закончило выполнение своих задач и может быть безопасно завершено). Необязательный параметр args в функции Main () дает возможность передавать в приложение информацию извне во время выполнения. Эта информация задается в виде параметров командной строки. Вам уже наверняка доводилось встречаться с параметрами командной строки. При запуске приложения из командной строки информацию, вроде того, какой файл требуется загрузить на время работы приложения, часто можно задавать напрямую. Например, возьмем доступное в Windows приложение Notepad. Это приложение можно запустить путем ввода Notepad в окне командной строки или в окне, которое появляется при выборе в меню Start (Пуск) пункта Run (Выполнить). Вместо Notepad в этих окнах можно также ввести и что-то вроде Notepad "myfile.txt". Тогда приложение Notepad при запуске либо сразу же загрузит файл по имени myfile.txt, либо предложит создать файл с таким именем, если он не существует. В данном случае "myf ile. txt" является аргументом командной строки. Именно такое поведение и позволяет обеспечивать в консольных приложениях параметр args. При выполнении консольного приложения все заданные параметры командной строки помещаются в массив args. После этого их можно использовать в приложении. В следующем практическом занятии демонстрируются соответствующий пример. Аргументы командной строки можно задавать в любом количестве, каждый из них все равно будет выводиться в окне консоли. практическое занятие Аргументы командной строки 1. Создайте новое консольное приложение по имени Ch06Ex04 и сохраните его в каталоге С: \BegVCSharp\Chapter06. 2. Добавьте в файл Program, cs следующий код: class Program { static void Main(string [] args) { Console.WriteLine("{0} command line arguments were specified:", args.Length); // Вывод аргументов командной строки foreach (string arg in args) Console.WriteLine(arg); Console.ReadKey(); } } 3. Откройте окно со страницами свойств проекта (щелкнув правой кнопкой мыши на имени проекта Ch06Ex04 в окне Solution Explorer (Проводник решений) и выбрав в контекстном меню пункт Properties (Свойства)).
164 Часть I. Язык С# 4. Отобразите страницу Debug (Отладка) и добавьте в поле Command Line Arguments (Аргументы командной строки) любые необходимые аргументы командной строки, например, так, как показано на рис. 6.7. Build Events Debug Resources Settings Reference P* Signing Security Publish Start Options Commend line «rguments: 256 myf.ie.txt -, longer »rgumenf Working directory: [.. j V: En«ble the Visual Studio hosting process Pm. 6.7. Добавление аргументов командной строки 5. Запустите приложение. На рис. 6.8 показан результат, который должен получиться. мЗННН |<1 I ИЛ Рис. 6.8. Приложение Ch06Ex04 в действии Описание полученных результатов Приведенный здесь код является очень простым: Console.WriteLine ("{0 } command line arguments were specified:", args.Length); foreach (string arg in args) Console.WriteLine(arg); Параметр args просто используется тем же образом, каким применялся бы любой другой строковый массив. Никаких сложных действий над аргументами не выполняется; все они просто выводятся на экран. В данном случае вы предоставляли эти аргументы через свойства проекта в IDE. Такой подход является удобным способом использовать одинаковые аргументы командной строки при каждом запуске приложения из IDE вместо того, чтобы каждый раз заново вводить их в командной строке. Точно такого же результата можно добиться и открыв окно командной строки в том же каталоге, что и выходные данные проекта (а именно— С: \BegCSharp\Chapter6\ Ch06Ex04\Ch06Ex04\bin\Debug), и набрав следующую команду: Ch06Ex04 256 myFile.txt "a longer argument" Все аргументы отделяются друг от друга пробелами. Предоставить аргумент, содержащий пробелы, можно, заключив его в двойные кавычки, которые не позволят воспринимать его как несколько аргументов.
Глава 6. Функции 165 Использование функций в структурах В предыдущей главе описывались различные типы структур, которые позволяют хранить множество элементов данных в одном месте. На самом деле структуры способны на гораздо большее. Например, они могут содержать не только данные, но и функции. Это их свойство на первый взгляд может показаться немного странным, но в действительности является очень полезным. В качестве простого примера рассмотрим следующую структуру: struct customerName { public string firstName, lastName; } При наличии переменных типа customerName и желании вывести в окно консоли полное имя, это имя придется собирать из отдельных частей. Для переменной типа CustomerName по имени myCustomer можно воспользоваться, например, таким синтаксисом: customerName myCustomer; myCustomer.firstName = "John"; myCustomer.lastName = "Franklin"; Console.WriteLine("{0} {1}", myCustomer.firstName, myCustomer.lastName) ; Добавив в структуру соответствующие функции, код можно упростить и централизовать решение общих задач. В частности, добавить подходящую функцию в структуру можно следующим образом: struct customerName { public string firstName, lastName; public string Name () { return firstName + " " + lastName; } } Эта функция выглядит во многом так же, как и любая другая показанная в этой главе функция; единственное отличием состоит в том, что в ней не применяется модификатор static. Причины будут объясняться позже в книге; пока что вполне достаточно знать то, что для функций в структурах использовать данное ключевое слово не требуется. Применить эту функцию можно следующим образом: customerName myCustomer; myCustomer.firstName = "John"; myCustomer.lastName = "Franklin"; Console .WriteLine (myCustomer.Name ()) ; Такой синтаксис и гораздо проще, и понятнее предыдущего. Здесь важно обратить внимание на то, что у функции Name () имеется прямой доступ к таким членам структуры, как firstName и lastName. В рамках структуры customerName они могут считаться глобальными. Перегрузка функций Ранее в этой главе уже показывалось, как соблюдать соответствие с сигнатурой функции при ее вызове. Это означает, что для выполнения операций над разными типами переменных необходимо иметь отдельные функции.
166 Часть I. Язык С# Механизм перегрузки функций (function overloading) позволяет создавать множество функций, имеющих одинаковое имя, но работающих с разными типами параметров. Например, ранее приводился код, в котором содержалась функция с именем MaxValue (): class Program { static int MaxValue (int [] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray[i]; } return maxVal; } static void Main(string [ ] args) { int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; int maxVal = MaxValue(myArray); Console.WriteLine("The maximum value in myArray is {0}", maxVal); // Вывод максимального значения в массиве myArray Console.ReadKey(); } } Эта функция могла использоваться только с массивами значений int. Для работы с другими типами параметров можно было бы либо предоставить соответствующие функции с другими именами, т.е., например, изменить имя на IntArrayMaxValue () и предоставить функции с именами вроде DoubleArrayMaxValue () для обработки других типов, либо добавить в код следующую функцию: static double MaxValue(double [ ] doubleArray) { double maxVal = doubleArray[0]; for (int i = 1; i < doubleArray.Length; i++) { if (doubleArray[i] > maxVal) maxVal = doubleArray[i]; } return maxVal; } Единственное отличие здесь связано с применением значений double. Имя функции — MaxValue () — выглядит точно так же, но вот ее сигнатура (что критически важно) выглядит по-другому. Объясняется это тем, что в сигнатуру функции, как уже рассказывалось ранее, входит как имя функции, так и ее параметры. Определение двух функций с одинаковыми сигнатурами было бы ошибкой, но благодаря тому, что эти две функции имеют разные сигнатуры, никакой ошибки здесь нет. Возвращаемый тип функции не является частью ее сигнатуры, поэтому определять две функции, отличающиеся только типом возвращаемого значения, нельзя; их сигнатуры считаются идентичными. После добавления приведенного выше кода у функции MaxValue () появятся две версии, одна из которых будет принимать в качестве входных данных массив значе-
Глава 6. Функции 167 ний типа int и возвращать максимальное значение в нем как число типа int, а вторая— принимать в качестве входных данных массив значений типа double и возвращать максимальное значения в нем в виде числа типа double. Прелесть такого кода состоит в том, что он не требует указывать явным образом, какая именно из этих двух версий должна использоваться. Достаточно просто предоставить параметр массива и подходящая версия этой функции будет выбрана автоматически в зависимости от того, к какому типу относится параметр. Обратите внимание на еще одно свойство механизма IntelliSense в VS и VCE: при наличии в приложении двух таких, как были показаны выше, версий функции и вводе ее имени, IDE будет отображать все доступные перегрузки для данной функции. Например, в случае ввода такой строки: double result = MaxValue ( IDE отобразит информацию об обеих версиях функции MaxValue (), между которыми можно будет переключаться клавишами со стрелками вверх и вниз (рис. 6.9). Im^l of 2% double Program MaWalue (doublet] double Array) | \щ2 of 2щ int Program MaxValue flnt[] int Array) | Рис. 6.9. Отображение информации о доступных перегруженных версиях функции При перегрузке функций учитываются все аспекты их сигнатур. Например, может существовать две разных версии функции, принимающие параметры, соответственно, по значению и по ссылке: static void ShowDouble (ref int val) { } static void ShowDouble (int val) { } Решение по поводу того, какая версия должна использоваться, будет приниматься на основании того, содержится в вызове функции ключевое слово ref или нет. Например, следующая строка кода приведет к вызову той версии, которая принимает параметры по ссылке: ShowDouble(ref val); Показанная ниже строка кода обеспечит вызов версии, которая принимает параметры по значению: ShowDouble(val); В качестве альтернативы можно сделать так, чтобы версии функции отличались друг от друга количеством параметров и т.д. Делегаты Делегат (delegate) — это тип, который позволяет хранить ссылки на функции. Возможно, это звучит довольно замысловато, но на самом деле механизм делегатов выглядит удивительно просто. Самая важная задача делегатов будет демонстрироваться чуть позже в этой книге при рассмотрении событий и их обработки, но полезно
168 Часть I. Язык С# рассказать здесь о них хотя бы вкратце. Объявляются делегаты во многом так же, как функции, но только безо всякого тела функции и с ключевым словом delegate. В объявлении любого делегата указывается возвращаемый тип и список параметров. После определения делегата можно объявлять переменную с типом этого делегата. Далее эту переменную можно инициализировать как ссылку на любую функцию, которая имеет точно такой же возвращаемый тип и список параметров, как и у делегата. После этого функцию можно вызывать с использованием переменной делегата так, будто бы это и есть сама функция. Наличие переменной, ссылающейся на функцию, также делает возможным выполнение и других операций, которые в противном случае были бы невозможными. Например, это позволяет передавать переменную делегата функции в качестве параметра, после чего делегат может использоваться для вызова функции, на которую он ссылается, во время выполнения. В следующем практическом занятии демонстрируется применение делегата для получения доступа к одной из двух возможных функций. практическое занятие Применение делегата для вызова функции 1. Создайте новое консольное приложение по имени Ch06Ex05 и сохраните его в каталоге С:\BegVCSharp\Chapter06. 2. Добавьте в файл Program.cs следующий код: class Program { delegate double ProcessDelegate (double paraml, double param2) ; static double Multiply (double paraml, double param2) return paraml * param2; static double Divide (double paraml, double param2) return paraml / param2; ;tatic void Main(string[] args) ProcessDelegate process; Console.WriteLine("Enter 2 numbers separated with a comma:") ; // Введите 2 числа, отделив их друг от друга запятой string input = Console.ReadLine(); int commaPos = input.IndexOf(','); double paraml = Convert.ToDouble(input.Substring@, commaPos)); double param2 = Convert.ToDouble(input.Substring(commaPos + 1, input.Length - commaPos - 1)) ; Console. WriteLine ("Enter M to multiply or D to divide:") ; // Введите М, если хотите выполнить операцию умножения, // или D, если операцию деления input = Console.ReadLine(); if (input = "M") process = new ProcessDelegate(Multiply); else process = new ProcessDelegate(Divide); Console.WriteLine("Result: {0}", process(paraml, param2)); // Вывод результата Console.ReadKey();
Глава 6. Функции 169 3. Запустите приложение. На рис. 6.10 показан результат, который должен получиться. Рис. 6.10. Приложение Ch06Ex05 в действии Описание полученных результатов В приведенном коде определяется делегат (по имени Process Delegate), возвращаемый тип и параметры которого соответствуют тем, что имеются у двух функций (Multiply () и Divide ()). Определение этого делегата выглядит следующим образом: delegate double ProcessDelegate(double paraml, double param2); Ключевое слово delegate указывает, что данное определение предназначено для делегата, а не для функции (поскольку это определение расположено в том же месте, в котором могло бы находиться определение функции). Далее в определении указывается возвращаемый тип double и два параметра double. Использованные имена были выбраны произвольно; типам и параметрам делегатов можно давать любые имена. В частности, в данном коде для делегата было выбрано имя ProcessDelegate, а для двух параметров типа double — соответственно, paraml и param2. Код в Main () начинается с объявления переменной с использованием нового типа делегата: static void Main(string [ ] args) { ProcessDelegate process; Далее идет довольно стандартный код на С#, который сначала запрашивает два разделенных запятой числа и затем помещает их в две переменные double: Console. WriteLine ("Enter 2 numbers separated with a comma:"); string input = Console.ReadLine (); int commaPos = input.IndexOf(','); double paraml = Convert.ToDouble(input.Substring@, commaPos)); double param2 = Convert.ToDouble(input.Substring(commaPos + 1, input.Length - commaPos - 1)); В демонстрационных целях в этот код не была включена операция проверки правильности вводимых пользователем данных. В реальном коде обязательно это было обязательно предусмотрено. Далее пользователю предлагается перемножить или поделить эти числа: Console.WriteLine("Enter M to multiply or D to divide:"); input = Console.ReadLine (); После этого, в зависимости от выбора, инициализируется переменная делегата process: if (input == "М") process = new ProcessDelegate(Multiply); else process = new ProcessDelegate(Divide);
170 Часть I. Язык С# Для присваивания переменной делегата ссылки на функцию применяется несколько необычный синтаксис. Во многом так же, как и при присваивании значений массивам, при создании нового делегата требуется использовать ключевое слово new. После new указывается тип делегата и предоставляется параметр, ссылающийся на функцию, которая должна использоваться, каковой в данном случае является Multiply () или Divide (). Этот параметр не соответствует ни параметрам типа делегата, ни параметрам целевой функции; этот синтаксис применяется исключительно для делегатов. На его месте указывается просто имя применяемой функции, безо всяких круглых скобок. И, наконец, напоследок выполняется вызов выбранной функции с использованием делегата. Здесь применяется тот же самый синтаксис, независимо от того, на какую из функций ссылается делегат: Console.WriteLine("Result: {0}", process(paraml, param2)); Console.ReadKey(); } Переменная делегата обрабатывается так же, как если бы это было имя функции. В отличие от функции, однако, с ней можно также выполнять и дополнительные операции, например, передавать ее функции через параметр, как показано в следующем примере: static void ExecuteFunction(ProcessDelegate process) { process B.2, 3.3); } Это означает, что поведением функций можно управлять передачей им делегатов, во многом подобно выбору наиболее подходящей для использования "оснастки". Например, может существовать функция, сортирующая массивы строк в алфавитном порядке. Для сортировки списков могут применяться разнообразные алгоритмы с разной производительностью, что зависит от характеристик сортируемого списка. За счет применения делегатов можно выбирать используемый алгоритм, передавая функции сортировки делегат алгоритма сортировки. У делегатов имеется еще много подобных сфер применения, но, как упоминалось ранее, наиболее важной из них является обработка событий, о которой более подробно речь поцдет в главе 13. Резюме В этой главе был предоставлен довольно подробный обзор способов применения функций в коде С#. Многие из дополнительных возможностей, которые предлагают функции (вроде делегатов, например), являются более абстрактными и требуют анализа с точки зрения объектно-ориентированного программирования, который будет проводиться в главе 8. Далее перечислены ключевые моменты, с которыми вы ознакомились в этой главе. □ Определение и использование функций в консольных приложениях. □ Обмен данными с функциями через возвращаемые значения и параметры. □ Передача массивов параметров функциям. □ Передача по ссылке и по значению. □ Указание параметров для дополнительных возвращаемых значений. □ Концепция области видимости переменных, благодаря которой переменные могут скрываться от тех разделов кода, в которых они не нужны.
Глава 6. Функции 171 □ Детали применения функции Main () и параметров командной строки в ней. □ Использование функций в структурах. □ Перегрузка функций, посредством которой одной и той же функции можно предоставлять разные параметры и тем самым получать дополнительную функциональность. □ Делегаты и как с их помощью обеспечивать динамический выбор функций во время выполнения. Понимание того, как пользоваться функциями, играет центральную роль во всех видах программирования. В последующих главах, начиная, с главы 8, когда пойдет изучение приемов объектно-ориентированного программирования, будет демонстрироваться более формальная структура функций и то, каким образом их можно применять к классам. После этого наверняка станет ясно, что возможность абстрагировать код в пригодные для многократного использования блоки кода является одной из самых полезных особенностей программирования на языке С#. Упражнения 1. Две приведенных ниже функции содержат ошибки. Назовите их. static bool Write () { Console.WriteLine("Text output from function."); } static void myFunction(string label, params int[] args, bool showLabel) { if (showLabel) Console.WriteLine(label); foreach (int i in args) Console.WriteLine("{0}", i); } 2. Напишите приложение, использующее два аргумента командной строки для помещения значений, соответственно, в строку и в целочисленную переменную, а затем отображающее эти значения на экране. 3. Создайте делегат и используйте его для выполнения роли функции Console. ReadLine () при запрашивании у пользователя входных данных. 4. Измените приведенную ниже структуру так, чтобы она включала функцию, возвращающую общую цену заказа: struct order { public string itemName; public int unitCount; public double unitCost; } 5. Добавьте в структуру order еще одну функцию, возвращающую форматированную строку, как показано ниже (с заменой всех выделенных курсивом вхождений в угловых скобках соответствующими значениями): Order Information: <количество_единиц> <наименование_элемента> items at $<стоимость единицы> each, total cost $<общая стоимость>
Отладка и обработка ошибок До сих пор в книге рассматривались базовые детали простого программирования на С#. Прежде чем переходить к исследованию приемов объектно-ориентированного программирования в следующей части книги, необходимо ознакомиться со способами выполнения отладки и обработки ошибок в коде С#. Ошибки в коде — это такая вещь, которая случается всегда. Каким бы хорошим ни был программист, проблемы всегда возникают, и потому хороший программист должен осознавать это и быть готовым к их разрешению. Конечно, некоторые проблемы являются незначительными и никак не влияют на работу приложения, как, например, ошибка в названии кнопки, но вопиющие ошибки тоже возможны и они могут приводить к полной остановке приложений (из-за чего обычно называются фатальными ошибками). К числу фатальных ошибок относятся как простые ошибки в коде, которые препятствуют компиляции (синтаксическиеошибки), так и более серьезные ошибки, которые происходят только во время выполнения. Некоторые ошибки являются неявными. Например, приложение может не справляться с добавлением записи в базу данных по причине отсутствия требуемого поля или добавлять запись с неправильными данными из-за других ограничивающих обстоятельств. Такие ошибки, при которых каким-либо образом страдает логика приложения, называются семантическими или логическими ошибками. Зачастую программист узнает о неявных ошибках лишь тогда, когда получает от пользователя жалобу о том, что в приложении что-то работает не так. Тогда ему нужно пройтись по своему коду, выяснить, в чем дело, и устранить проблему так, чтобы все работало должным образом. В таких ситуациях предлагаемые в VS и VCE возможности для отладки оказывают просто фантастическую помощь. В первых разделах настоящей главы будут описаны некоторые из этих возможностей и то, как их применять для решения наиболее распространенных проблем.
Глава 7. Отладка и обработка ошибок 173 Затем будут рассмотрены приемы для обработки ошибок, доступные в С#. Они позволяют принимать меры предосторожности на случаи, в которых вероятно возникновение ошибок, и писать код, достаточно устойчивый для того, чтобы справляться с ошибками, которые в противном случае могли бы стать фатальными. Эти приемы являются частью языка С#, а не механизма отладки, но в IDE предусмотрены инструменты для оказания помощи программисту и здесь тоже. В частности, в настоящей главе рассматриваются следующие основные темы. □ Методы отладки, доступные в Visual Studio. □ Приемы обработки ошибок, доступные в языке С#. Отладка в VS и VCE Ранее упоминалось о том, что приложения можно запускать двумя способами: с включением и без включения отладки. По умолчанию при запуске приложения из VS или VCE оно запускается с включением отладки. Это происходит, например, при нажатии клавиши <F5> или выполнении щелчка на кнопке с изображением зеленой стрелки и подписью Play (Воспроизвести) в панели инструментов. Запускать приложение без отладки можно путем выбора в меню Debug (Отладка) пункта Start Without Debugging (Запустить без отладки). VS и VCE позволяют создавать приложения в двух конфигурациях: отладочной (Debug), используемой по умолчанию, и рабочей (Release). (На самом деле можно определять и дополнительные конфигурации, но эта методика здесь не рассматривается.) Переключаться между этими двумя конфигурациями можно с помощью доступного в стандартной панели инструментов раскрывающегося списка Solution Configurations (Конфигурации решения). В VCE этот раскрывающийся список по умолчанию отключен. Для проработки материала настоящей главы его необходимо активизировать. Чтобы сделать это, сначала нужно выбрать в меню Tools (Сервис) пункт Options (Параметры), а потом в диалоговом окне Options (Параметры) удостовериться в том, что флажок Show All Settings (Показывать все параметры) отмечен, перейти в категории Projects and Solutions (Проекты и решения) в подкатегорию General (Общие) и отметить флажок Show Advanced Build Configurations (Показывать расширенные конфигурации сборки). При сборке приложения в отладочной конфигурации и его запуске в режиме Debug, происходит нечто более чем просто выполнение кода. В отладочных сборках сохраняется символьная информация приложения, чтобы IDE-среда точно знала, что происходит при выполнении каждой строки кода. Под символьной информацией подразумевается отслеживание, например, имен переменных, которые используются в некомпилированном коде, чтобы их можно было сопоставлять со значениями в скомпилированном коде, где не будет содержаться никакой читабельной человеком информации. Эта информация хранится в . pdb-файлах, которые, возможно, уже приходилось встречать в каталогах Debug, и позволяет выполнять множество полезных операций, часть из которых перечислена ниже. □ Выводить отладочную информацию в IDE. □ Просматривать (и редактировать) значения переменных в области видимости во время выполнения приложения. □ Приостанавливать и возобновлять выполнение программы. □ Автоматически останавливать выполнение программы в определенных точках кода. □ Выполнять программы по одной строке за раз.
174 Часть I. Язык С# □ Следить за изменениями в содержимом переменных во время выполнения приложения. □ Изменять содержимое переменных во время выполнения. □ Выполнять тестовые вызовы функций. В рабочей конфигурации код приложения оптимизируется, и выполнять все эти операции невозможно. Однако рабочие сборки работают быстрее и по окончании разработки приложения пользователям, как правило, предоставляются именно они, потому что они не требуют включения той символьной информации, которая помещается в отладочные сборки. В этом разделе описаны приемы отладки, которыми можно пользоваться для выявления и исправления областей кода, которые работают не так, как ожидается, что, в общем, и называется собственно процессом отладки. Эти приемы сгруппированы в два раздела, в соответствии с их применением. В целом процесс отладки производится либо путем прерывания выполнения программы, либо путем оставления пометок для осуществления анализа позже. Согласно терминологии VS и VCE, это означает, что приложение либо работает, либо находится в режим останова, т.е. его обычное выполнение приостановлено. Первыми мы рассмотрим приемы выполнения отладки в непрерывном (обычном) режиме. Выполнение отладки в непрерывном (обычном) режиме Одной из функций, которая использовалась повсюду в книге, была Console. WriteLine (), выводящая текст на консоль. При разработке приложений ею очень удобно пользоваться для получения дополнительной информации по выполняемым операциям: Console.WriteLine("Функция MyFuncO вызвана."); MyFunc("Какие-то действия."); Console.WriteLine("Функция MyFuncO завершена."); Этот фрагмент кода показывает, как можно получать дополнительную информацию касательно функции по имени MyFunc (). Все это хорошо, но приводит к тому, что вывод в окне консоли становится немного захламленным, а при разработке приложений других типов, например, Windows Forms, консоль для вывода такой информации вообще не будет доступной. Поэтому существует альтернативный вариант — выводить такой текст в отдельном месте, а именно — в предлагаемом в IDE окне Output (Вывод). В главе 2, где описывалось окно Error List (Список ошибок), уже упоминалось о том, что в этом же месте могут также отображаться и другие окна. Одним из них является окно Output, которое может быть очень полезным для отладки; оно отображается выбором в меню View (Вид) пункта Output (Вывод). Это окно предоставляет информацию, касающуюся компиляции и выполнения кода, включая ошибки, которые были обнаружены во время компиляции. Еще его можно использовать, как показано на рис. 7.1, для отображения специальной диагностической информации, осуществляя вывод прямо в него. Show output Ьош Debug ^^,||ЛЦЩЦИ^,^Ии1 hjErrorlut J] Output =================. Рис. 7.1. Окно Output
Глава 7. Отладка и обработка ошибок 175 В этом окне содержится раскрывающееся меню, из которого можно выбирать разные режимы, такие как Build (Сборка) и Debug (Отладка). В этих режимах отображается информация, касающаяся, соответственно, только компиляции или только выполнения кода. Под "выполнением вывода в окно Output" в настоящем разделе подразумевается "вывод в представление режима Debug окна Output". В качестве альтернативы можно создавать журнальный файл и делать так, чтобы при выполнении приложения в нем фиксировалась вся важная информация. Приемы для обеспечения такого поведения выглядят во многом так же, как и приемы записи в окно Output, но требуют понимания способов получения доступа к файловой системе из С#-приложений. Поэтому мы пока отложим рассмотрение этой темы, поскольку много чего можно делать и без погружения в премудрости технологий доступа к файлам. Вывод отладочной информации Вывод текста в окно Output во время выполнения очень прост. Вместо вызовов Console. WriteLine () нужно использовать вызовы таких команд, которые позволяют выводить текст в желаемое место: □ Debug.WriteLine() □ Trace.WriteLine() Эти команды функционируют практически одинаково с одним важным отличием: первая работает только в отладочных сборках, а вторая — и в рабочих сборках тоже. На самом деле команда Debug.WriteLine () не будет даже компилироваться в рабочую сборку; она будет просто исчезать, что, несомненно, имеет свои преимущества (в скомпилированном коде будет на одну команду меньше). В сущности, из одного исходного файла можно создавать две версии приложения. В отладочной версии будут отображаться всевозможные дополнительные диагностические сведения, а в рабочей этих накладных расходов не будет, и пользователям не будут отображаться сообщения, которые в противном случае могли бы их раздражать. Эти функции работают не совсем так, как Console .WriteLine (). Они принимают только один строковый параметр для выводимого сообщения, а не позволяют вставлять значения переменных с помощью синтаксиса {X}. Это означает, что для вставки значений переменных в строки должен применяться альтернативный прием — например, операция конкатенации +. Еще (при желании) можно предоставлять второй строковый параметр, отвечающий за отображение категории, к которой относится выводимый текст. Такой подход позволяет сразу же видеть, какие выходные сообщения отображаются в окне Output, что может оказаться удобным в случае вывода похожих сообщений из разных мест приложения. В общем, вывод этих функций выглядит следующим образом: <категория>: <сообщение> Например, приведенный ниже оператор, в котором MyFunc выступает в роли необязательного параметра, представляющего категорию: Debug.WriteLine("Добавление 1 к i", "MyFunc"); привел бы к получению такого вывода: MyFunc: Добавление 1 к i В следующем практическом занятии демонстрируется пример вывода отладочной информации, как было описано выше.
176 Часть I. Язык С# шттшшттштшшшшшттт лиш.^дд , . „t i „i,i .мпш, , i, л!,# i Практическое занятие! ВЫВОД ТвКСТЭ В ОКНО Output 1. Создайте новое консольное приложение по имени Ch07Ex01 и сохраните его в каталоге С: \BegVCSharp\Chapter07. 2. Измените его код следующим образом: using System; using System.Collections .Generic- using System.Linq; using System.Text; using System.Diagnostics; namespace Ch07Ex01 { class Program { static void Main(string [ ] args) { int[] testArray = {4, 7, 4, 2, 7, 3, 7, 8, 3, 9, 1, 9}; int[] maxVallndices; int maxVal = Maxima (testArray, out maxVallndices) ; Console. WriteLine ("Maximum value {0} found at element indices:", // Вывод максимального значения и его индексов maxVal); foreach (int index in maxVallndices) { Console.WriteLine(index); } Console.ReadKey(); } static int Maxima(int[] integers, out int[] indices) { Debug. WriteLine ("Maximum value search started.") ; // Начало поиска максимального значения indices = new int[l]; int maxVal = integers [0] ; indices[0] = 0; int count = 1; Debug.WriteLine(string.Format( "Maximum value initialized to {0}, at element index 0.", maxVal)) ; // Инициализация максимального значения элементом с индексом 0 for (int i = 1; i < integers. Length; i++) { Debug.WriteLine(string.Format( "Now looking at element at index {0}.", i)); // Просмотр очередного элемента if (integers[i] > maxVal) { maxVal = integers[i]; count = 1; indices = new int[l] ; indices[0] = i; Debug.WriteLine(string.Format( "New maximum found. New value is {0}, at element index {1}.", // Найдено новое максимальное значение maxVal, i)) ; }
Глава 7. Отладка и обработка ошибок 177 else { if (integers [i] = maxVal) { count++; int[] oldlndices = indices; indices = new int[count]; oldlndices.CopyTo(indices, 0); indices [count - 1] = i; Debug.WriteLine(string.Format( "Duplicate maximum found at element index {0}. ", i)) ; // Найден дубликат максимального значения } ) } Trace.WriteLine(string.Format( "Maximum value {0} found, with {1} occurrences.", maxVal, count)); // Вывод максимального значения и количества его вхождений Debug. WriteLine ("Maximum value search completed.") ; // Поиск максимального значения завершен return maxVal; } } } 3. Запустите приложение в режиме отладки. На рис. 7.2 показан результат, который должен получиться. I Ш fi»*:///C:/B#gVCSharjVChapUftO/Cr»07ExO1/... L^iMJBfie I [^l 1 Jj| Рис. 7.2. Приложение Ch 0 7Ex 01 в действии 4. Завершите работу этого приложения и проверьте содержимое окна Output (Вывод) в режиме Debug (Отладка). Ниже приведена усеченная версия вывода: Maximum value search started. Maximum value initialized to 4, at element index 0. Now looking at element at index 1. New maximum found. New value is 7, at element index 1. Now looking at element at index 2. Now looking at element at index 3. Now looking at element at index 4. Duplicate maximum found at element index 4. Now looking at element at index 5. Now looking at element at index 6. Duplicate maximum found at element index 6. Now looking at element at index 7. New maximum found. New value is 8, at element index 7. Now looking at element at index 8. Now looking at element at index 9. New maximum found. New value is 9, at element index 9. Now looking at element at index 10. Now looking at element at index 11.
178 Часть I. Язык С# Duplicate maximum found at element index 11. Maximum value 9 found, with 2 occurrences. Maximum value search completed. The thread '<No Name>' @xcc8) has exited with code 0 @x0). The thread '<No Name>' (Oxccc) has exited with code 0 @x0). The program '[3648] Ch07Ex01.vshost.exe: Managed' has exited with code 0 @x0). Поток '<Без имени>' @хсс8) завершен с кодом 0 @x0) . Поток '<Без имени>' (Охссс) завершен с кодом 0 @x0). Программа '[3648] ChOlExOl.vshost.exe: Managed' завершена с кодом 0 @x0). 5. Переключитесь в режим Release, воспользовавшись раскрывающимся меню в стандартной панели инструментов, как показано на рис. 7.3. Debug Debug Configuration Manager.. Рис. 73. Переключение в режим Release 6. Запустите программу снова, на этот раз в режиме Release, и опять загляните в окно Output, когда ее выполнение завершится. Ниже приведена усеченная версия вывода, который будет там содержаться: Maximum value 9 found, with 2 occurrences. The thread '<No Name>' @xe2c) has exited with code 0 @x0). The thread '<No Name>' @xd2c) has exited with code 0 @x0). The program '[1736] Ch07Ex01.vshost.exe: Managed' has exited with code 0 @x0). Поток '<Без имени>' (Охссв) завершен с кодом 0 @x0). Поток '<Без имени>' (Охссс) завершен с кодом 0 @x0) . Программа '[3648] Ch07Ex01.vshost.exe: Managed1 завершена с кодом 0 @x0). Описание полученных результатов Данное приложение является расширенной версией показанного в главе 6, и обладает функцией для вычисления максимального значения в массиве целых чисел. Вдобавок оно возвращает массив индексов, которые обнаруженные максимальные значения имеют в массиве, чтобы вызывающий код мог манипулировать этими элементами. В начале кода присутствует дополнительная директива: using System.Diagnostics; Она упрощает доступ к описанным выше функциям, поскольку они содержатся в пространстве имен System. Diagnostics. Без этой директивы using код вроде: Debug.WriteLine("Bananas"); требовал бы дополнительной квалификации: System.Diagnostics.Debug.WriteLine("Bananas") ; Эта директива using позволяет коду оставаться простым и сокращает количество требуемых слов. В коде внутри Main () инициализируется тестовый массив целых чисел по имени testArray, а также объявляется еще один целочисленный массив по имени maxValIndices для хранения индексов, которые будет возвращать отвечающая за вычисление функция Maxima О , и затем производится вызов этой функции. После возврата из функции в коде просто выводятся результаты.
Глава 7. Отладка и обработка ошибок 179 Функция Maxima () выглядит несколько более сложно, но никакого особого кода, который еще не был показан ранее, в ней нет. Поиск по массиву выполняется практически тем же самым образом, что и в функции MaxVal (), которая демонстрировалась в главе 6, но только с фиксированием индексов максимальных значений. Особенно важно обратить внимание (помимо строк, отвечающих за вывод отладочной информации) на функцию, которая используется для отслеживания индексов. Вместо того чтобы возвращать массив с размером, достаточным для хранения всех индексов в исходном массиве (т.е. с таким же размером, как у исходного массива), функция Maxima () возвращает массив с размером, достаточным для хранения только обнаруженных индексов. Делает она это за счет постоянного повторного создания массивов разного размера в ходе процесса поиска; это необходимо из-за того, что изменять размер массивов после создания нельзя. Поиск начинается с предположения о том, что первый элемент в исходном массиве (который локально именуется integers) является максимальным значением и что в массиве есть только одно максимальное значение. Это позволяет установить значения для параметра maxVal (который представляет возвращаемое значение функции и обнаруженное максимальное значение) и для массива out-параметров indices, который отвечает за хранение индексов обнаруженных максимальных значений. Поэтому maxVal присваивается значение первого элемента в integers, a indices — одно значение 0, которое является индексом первого элемента в массиве. Еще осуществляется сохранение количества обнаруженных максимальных значений в переменной count, что позволяет отслеживать состояние массива indices. Основное тело функции составляет цикл, в котором производится проход по всем значениям в массиве integers, кроме первого, поскольку оно уже было обработано. Каждое значение сравнивается с текущим значением maxVal и игнорируется, если значение maxVal оказывается больше. Если же инспектируемое в текущий момент значение массива оказывается больше maxVal, тогда значения maxVal и indices изменяются соответствующим этому значению образом. Если же оно равно maxVal, тогда происходит увеличение значения переменной count и для indices создается новый массив. Этот новый массив содержит на один элемент больше прежнего массива indices и хранит новый обнаруженный индекс. Код для обеспечения последней функциональной возможности выглядит следующим образом: if (integers [i] == maxVal) { count++; int[] oldlndices = indices; indices = new int[count]; oldlndices.CopyTo(indices, 0); indices[count - 1] = i; Debug.WriteLine(string.Format("Duplicate maximum found at element index {0}.", i)); } Он работает за счет сохранения резервной копии старого массива indices в массиве oldlndices, который представляет собой массив целочисленного типа и является локальным по отношения к данному блоку if. Обратите внимание на то, что значения в oldlndices копируются в новый массив indices с помощью функции <массив>. СоруТо (). Эта функция берет целевой массив и индекс, в который подлежит скопировать первый элемент, и вставляет все значения в этот целевой массив. Повсюду в коде осуществляется вывод различных фрагментов текста с помощью функций Debug.WriteLine () и Trace .WriteLine (). Для вложения значений пере-
180 Часть I. Язык С# менных в строки так же, как и в случае использования Console. WriteLine (), в этих функциях применяется string.Format (). Такой подход является немного эффективнее операции конкатенации +. При запуске приложения в режиме Debug отображается перечень всех шагов, которые выполняются для получения результата. В режиме Release отображается только результат вычисления, поскольку в рабочих сборках никаких вызовов функции Debug. WriteLine () не производится. Помимо этих функций WriteLine () существует еще несколько, о которых следует знать. Во-первых, доступны эквиваленты функции Console.Write (): □ Debug.Write () □ Trace.Write () Обе эти функции подразумевают использование точно такого же синтаксиса, как и функции WriteLine () (т.е. принимают один или два параметра с выводимым сообщением и, при желании, категорией), но отличаются тем, что не добавляют символов конца строки. Во-вторых, существуют следующие команды: □ Debug.WriteLinelf () □ Trace.WriteLinelf () □ Debug.WritelfO □ Trace.WritelfO Каждая из них имеет те же самые параметры, что и ее не содержащий If аналог, плюс один дополнительный обязательным параметр, который предшествует всем остальным в списке. Этот параметр принимает значение типа Boolean (или вычисляющееся в такое значение выражение) и приводит к тому, что функция выводит текст только в том случае, если это значение равно true. Эти функции можно применять для обеспечения вывода текста в окно Output на базе каких-то условий. Например, при необходимости, чтобы отладочная информация выводилась только при определенных обстоятельствах, можно использовать в коде ряд операторов Debug. WriteLinelf (), требующих соблюдения определенного условия. Тогда в случае несоблюдения этого условия, операторы не будут отображать отладочных сведений и окно Output, соответственно, не будет захламляться излишней информацией. Точки трассировки Точка трассировки (tracepoint) представляет собой средство, доступное только в VS и отсутствующее в VCE. Поэтому те, кто использует VCE, при желании могут пропустить этот раздел. Альтернативой выводу информации в окно Output является применение точек трассировки. Они являются средством VS, а не языка С#, но служат для той же самой цели, что и функция Debug.WriteLine (). По сути, они позволяют выводить отладочную информацию без внесения изменений в код. Увидеть, как работают точки трассировки, можно, подставив их на место отладочных команд, которые использовались в предыдущем примере. (См. файл Ch07Ex01TracePoints в исходном коде для этой главы.) Ниже перечислены шаги для добавления точки трассировки. 1. Поместите курсор на строку, где требуется вставить точку трассировки. Точка трассировки будет обрабатываться перед выполнением этой строки кода.
Глава 7. Отладка и обработка ошибок 181 2. Щелкните правой кнопкой мыши на строке кода и выберите в контекстном меню, которое появится после этого, пункт Breakpoints Insert Tracepoint (Точка останова■=>Вставить точку трассировки). 3. Введите подлежащую выводу строку в текстовом поле Print a Message (Вывести сообщение) диалогового окна When Breakpoint Is Hit (При достижении точки останова), которое откроется далее. Если на экран необходимо вывести и значение переменной, укажите имя переменной в фигурных скобках. 4. Щелкните на кнопке ОК. 5. Слева от строки кода, содержащей точку трассировку, появится ромбовидный символ красного цвета, и сама строка станет отображаться с красной подсветкой. Уже по названию диалогового окна, которое предназначено для добавления точек трассировки, и пунктов меню, которые требуется выбирать для них, становится понятно, что точки трассировки являются разновидностью точек останова (и могут при необходимости, как и точки останова, приводить к приостановке процесса выполнения приложения). О точках останова, которые обычно применяются для выполнения более сложных процедур отладки, речь пойдет позже в этой главе. На рис. 7.4 показана точка трассировки для строки 31 в примере Ch07Ex01TracePoints, в коде которого после удаления существующих операторов Debug.WriteLine () применяется нумерация строк. Wn#f1 ВГМкрОМК ш Hit Specify what to do when the breakpoint И hit J Print a menage Mwmum vilue initialized to JmaxVall at element index 0. You can include the value of a variable or other expression in the message by placing it in curty braces, such as The value of x is fxj." To Insert a curty brace, use ЛГ- To Insert a backslash, use Л\-. The following special keywords will be replaced with their current values: SADDRESS - Current Instruction, $CAUER - Previous Function Name, $CAU STAC К . Call Stack, $FUNCTION - Current Function Name. $PID - Process Id, $PNAME - Process Name $TTO - Thread Id, STNAME - Thread Name И Run a macro: J Continue execution Puc. 7.4. Пример добавления точки трассировки Как видно на рис. 7.4, точки трассировки позволяют вставлять и другую полезную информацию, касающуюся их месторасположения и контекста. Поэкспериментируйте с этими значениями, а особенно с $FUNCTION и $CALLER, чтобы увидеть, какую дополнительную информацию можно собирать с их помощью. Кроме того, на рисунке видно, что точки трассировки могут выполнять макросы, которые являются расширенным средством и здесь не рассматриваются. Существует еще одно окно, которое можно использовать для быстрого просмотра имеющихся в приложении точек трассировки. Оно открывается выбором в меню Debug (Отладка) пункта Windows1^Breakpoints (Окна1^Точки останова) и представляет собой общее окно для отображения точек останова (разновидностью которых, как уже упоминалось ранее, и являются точки трассировки). Его можно настраивать так, чтобы в нем отображалось больше сведений, касающихся точек трассировки, добав-
182 Часть I. Язык С# ляя из доступного в нем раскрывающегося меню Columns (Столбцы) столбца When Hit (При достижении). На рис. 7.5 показано это окно с добавленным столбцом When Hit и всеми вставленными в Ch07Ex01TracePoints точками трассировки. aaaxVaU ■ integers: 1]; count » 1; indicts ■ ne» ir.t(l]; indices[0] • 1; it (Integer*[i] ixVs.1) lnt[] oldlndires - indices; indices • ne» int[count]; oldlndlces.CopyTolindices, 0); indices[count -!]•!» 1) occurrences." -*—. ixVaU, count)); ■■-■"'"- -■■ ■■' ■ New- X >•• tS % Сейма™- Name Condition Hit Count #♦1 <f+ Program о line I character IS (no condition! break i V+ Program ci. lint 3) character 13 (no condition! break i ?/■+ Program.a. im« 3» character 13 |no conditjoni break i /^ Program cj. lint 4» character H |no condition! break i •"ф Program ci lint S4 character 10 |no I [ 3й«И*^ Bet ■> point» ■отмиш"^^ Whtn МП to (raivea «t element mdu I nage Menmum value Print menage Now looking at tltmtnt at index (I) Pnnt menage New maximum found New value n (maxvall at tltmtnt in Print menage Duplicate maximum found at tltmtnt indt» (I) Pnnt menage Maximum value ittrch completed Рис. 7.5. Окно Breakpoints с добавленным столбцом When Hit Выполнение этого приложения в режиме Debug приведет к получению тех же результатов, что и ранее. Удалять или временно отключать точки трассировки можно либо щелкая на них правой кнопкой мыши в окне кода, либо снимая с них отметки в окне Breakpoints. На то, что точка трассировки включена, в окне Breakpoints указывает наличие напротив нее установленной отметки; отключенные точки трассировки не имеют таких отметок, и отображаются в окне кода в виде контурного, а не сплошного ромба. Сравнение вывода диагностической информации и точек трассировки Теперь, когда были описаны две разных методики для вывода практически одинаковой информации, давайте рассмотрим преимущества и недостатки каждой из них. Во-первых, у точек трассировки нет никаких эквивалентных команд Trace (); т.е. выводить информацию с использованием точек трассировки в рабочих сборках нельзя. Объясняется это тем, что точки трассировки не включаются в состав приложения. Они обрабатываются Visual Studio и потому не присутствуют в скомпилированной версии приложения. Видеть их в работе можно только во время выполнения приложения в отладчике VS. Главный недостаток точек трассировки является также их главным преимуществом и состоит в том, что они хранятся в VS. Это позволяет быстро и легко добавлять их в приложения, когда они необходимы, а также очень просто удалять. Для удаления точки трассировки достаточно щелкнуть на указывающем на ее месторасположение красном ромбе.
Глава 7. Отладка и обработка ошибок 183 Одним из преимуществ точек трассировки, однако, является дополнительная информация, которую они позволяют легко добавлять, вроде упоминавшихся в предыдущем разделе значений $ FUNCTION. Хотя эта информация и доступна для кода, который пишется с использованием команд Debug и Trace, получать ее подобным образом сложнее. В целом рекомендации по применению этих двух методик вывода отладочной информации выглядят следующим образом. □ Вывод диагностической информации. Эту методику лучше применять в случаях, когда нужно, чтобы выводимые отладочные данные всегда выводились из приложения, особенно когда выводимая строка является сложной и содержит несколько переменных или много информации. Вдобавок, команды Trace зачастую оказываются единственно возможным вариантом при необходимости в выводе информации во время выполнения приложения в режиме Release. □ Точки трассировки. Эту методику лучше применять при отладке приложения для быстрого вывода важной информации, которая может помочь в исправлении семантических ошибок. Есть еще одно очевидное отличие, которое состоит в том, что точки трассировки доступны только в VS, в то время как методикой вывода диагностической информации можно пользоваться как в VS, так и в VCE. Выполнение отладки в режиме останова Остальные приемы отладки, описываемые в настоящей главе, работают в режиме останова (Break). В этот режим можно переходить несколькими способами, и все они приводят к приостановке выполнения программы тем или иным образом. Переход в режим останова Простейшим способом для перехода в режим останова является щелчок на кнопке паузы в IDE во время выполнения приложения. Эта кнопка находится в панели инструментов Debug (Отладка), которую нужно специально добавлять ко всем остальным панелям инструментов, которые отображаются в VS по умолчанию. Делается это щелчком правой кнопкой мыши в области панелей инструментов и выбором в контекстном меню пункта Debug (Отладка), как показано на рис. 7.6. На рис. 7.7 показана сама панель Debug, которая появляется после этого. Первые четыре кнопки в этой панели инструментов позволяют вручную управлять остановом. На рис. 7.7 три из них не активны, поскольку не работают с программой, которая еще не запущена. Та, что активна, называется Start и идентична кнопке, предлагаемой в стандартной панели инструментов. Остальные кнопки будут описаны далее по мере необходимости. Build Data Design Database Diagram Help "* layout Query Designer Puc. 7.6. Добавление панели инструментов Debug Кнопка Start (Пуск) ► и я а | ♦ «■ £i *з | Hex | m Puc. 7.7. Внешний вид панели Debug При запуске приложения эта панель инструментов приобретает внешний вид, показанный на рис. 7.8.
184 Часть I. Язык С# Кнопка Pause (Пауза) 4 Кнопка Stop (Останов) Кнопка Restart (Перезапуск) i ► U Я И J V -I 1*3 *J I Hex I 1 Рис. 7.8. Внешний вид панели Debug после запуска приложения После запуска приложения три следующих за Start кнопки, которые были неактивными, становятся доступны и позволяют делать следующее: □ приостанавливать приложение и переходить в режим останова (режим Break); □ полностью останавливать приложение (без перехода в режим Break, т.е. просто выходить из приложения). □ запускать приложение заново. Пауза приложения является, пожалуй, простейшим способом для перехода в режим Break, но не позволяет выбирать точное место останова. Остановка наверняка будет происходить в наиболее пригодном для паузы месте в приложении, например, в месте запрашивания ввода данных от пользователя. Возможно, будет также удаваться переходить в режим Break и во время выполнения длительной операции или длинного цикла, но выбор точной точки остановки все равно будет происходить довольно- таки произвольно. В общем, гораздо лучше вместо этого использовать специальные точки останова. Точки останова Точка останова (breakpoint) представляет собой отметку в исходном коде, которая приводит к автоматическому переходу в режим Break. Точки останова доступны и в VS, и VCE, но в VS обладают большей гибкостью. Их можно настраивать для того, чтобы они позволяли приложению делать следующее. □ Переходить в режим Break при достижении точки останова. □ (Только в VS.) Переходить в режим Break при достижении точки останова только в случае, если булевское выражение равно true. □ (Только в VS.) Переходить в режим Break при достижении точки останова определенное количество раз. □ (Только в VS.) Переходить в режим Break при достижении точки останова в случае, если с момента последнего достижения точки останова значение переменной изменилось. □ (Только в VS.) Выводить текст в окно Output или выполнять макрос (о котором упоминалось ранее в разделе "Точки трассировки"). Выполнение всех этих действий возможно только в отладочных сборках. При компиляции рабочей сборки все точки останова игнорируются. Для добавления точек останова существует несколько способов. Простые точки останова, предусматривающие перевод приложения в режим Break при достижении соответствующей строки, добавляются щелчком левой кнопкой мыши в окрашенной в серый области слева от необходимой строки кода, а затем либо выполнением щелчка правой кнопкой мыши на этой строке и выбором в контекстном меню пункта Breakpoints Insert Breakpoint (Точка останова1^Вставить точку останова), либо выбором в стандартном меню Debug (Отладка) пункта Toggle Breakpoint (Включение/отключение точки останова), или же нажатием клавиши <F9>. На рис. 7.9 показан вари-
Глава 7. Отладка и обработка ошибок 185 ант со щелчком правой кнопкой мыши и выбором подходящего пункта в контекстном меню VS (в VCE это меню будет выглядеть немного по другому, а в частности, не будет включать пункта Insert Tracepoint (Вставить точку трассировки)). а i Л .■ '1 А -^ Л Refactor ► Organize Uslngs ► Create Unit Tests- Insert Snippet- Surround With... Go To Definition Find All References Breakpoint ► Run To Cursor Cut Copy Paste Outlining ► ф Insert Breakpoint Insert Trace^felnt Puc. 7.9. Добавление точки останова После этого точка останова отображается в виде красного круга рядом с указанной строкой кода, которая подсвечивается, как показано на рис. 7.10. 1- 13 14 IS к 17 18 19 20 21 22 3, 9, 1, 9 }; static void Haln(strlng[] argrs) int[] testArray - { 4, 7, 4, 2, 7, 3, 7, int[] naxValIndices; int maxVal - Maxima(testArray, out maxValIndices); Console.WriteLine("Maximum value (CM found at element indices: maxVal); foreach (int index in maxValIndices) < ) Console.ReadKey(); Puc. 7.10. Отображение точки останова Материал, излагаемый в остальной части настоящего раздела, касается только VS, но не VCE. Поэтому те, кто использует VCE, могут пропустить всю остальную часть этого раздела и перейти сразу же к следующему разделу, который называется "Другие способы для перехода в режим останова ". В VS еще можно просматривать информацию о точках останова с помощью окна Breakpoints (о том, как оно отображается, уже рассказывалось ранее в этой главе). Это окно можно применять также и для отключения точек останова (снимая отметки, отображающиеся слева от их описания; отключенные точки останова будут иметь вид незакрашенных красных кружков), для удаления точек останова и для редактирования их свойств. Столбцы Condition (Условие) и Hit Count (Количество прохождений), отображающиеся в этом окне по умолчанию, являются лишь двумя из всех доступных столбцов, но зато самыми полезными. Редактировать содержащиеся в них свойства можно путем выполнения щелчка правой кнопкой мыши на требуемой точке (как в этом окне, так и непосредственно в окне кода) и выбора в контекстном меню, соответственно, либо пункта Condition (Условие), либо пункта Hit Count (Количество прохождений).
186 Часть I. Язык С# Выбор пункта Condition приводит к отображению диалогового окна Breakpoint Condition (Условие точки останова), показанного на рис. 7.11. When the breakpoint location is reached, the expression is evaluated and 1 ! the breakpoint is hit only If the expression is true or has changed. I [?i Condition: 1 maxVal > 4 1 4) b true 1 Has changed I { (Ж J| Canal I \тттштттшяттшшшттшшвшшЁШяя^шш^^^Еят!^ Рис. 7.1 1. Диалоговое окно Breakpoint Condition В этом диалоговом окне можно вводить любое булевское выражение, в котором задействованы любые переменные, лежащие в области видимости данной точки останова. На рис. 7.11 показано выражение, согласно которому точка останова должна срабатывать при ее достижении только в том случае, если значение maxVal больше 4. Дополнительно можно включить проверку этого выражения на предмет изменения, и точка останова будет активизироваться только в таком случае. Выбор пункта Hit Count приводит к отображению диалогового окна Breakpoint Hit Count (Количество прохождений точки останова), как показано на рис. 7.12. Breakpoint Hit Count A breakpoint is hit when the breakpoint location is reached and the condition is satisfied. The hit count Is the number of times the breakpoint has been hit. When the breakpoint is hit when the hit count Is equal to ») 1 Current hit count: 0 break Puc. 7.12. Диалоговое окно Breakpoint Hit Count В этом диалоговом окне можно задать количество проходов точки останова, после которого она должна срабатывать. В раскрывающемся списке предлагаются следующие варианты: □ Break always (Переход в режим останова должен происходить всегда); □ Break when the hit count is equal to (Переход в режим останова должен происходить только тогда, когда количество прохождений равно); □ Break when the hit count is a multiple of (Переход в режим останова должен происходить только тогда, когда количество проходов кратно); □ Break when the hit count is greater than or equal to (Переход в режим останова должен происходить только тогда, когда количество проходов больше или равно). Выбираемый в этом списке вариант в сочетании со значением, которое указывается в расположенном напротив списка текстовом поле, определяют поведение точки останова. Указывать количество проходов удобно в длинных циклах, в которых может потребоваться, чтобы переход в режим останова происходил, скажем, после первых 5000 итераций. Если бы такой возможности не было, переход в режим останова и перезапуск 5000 раз превратился бы в проблему.
Глава 7. Отладка и обработка ошибок 187 Точка останова с установленными дополнительными свойствами (вроде условия или количества прохождений) отображается несколько иначе. Вместо простого красного кружка сконфигурированная точка останова отображается в виде красного кружка, содержащего внутри выделенный белым цветом знак +. Это полезно, поскольку позволяет сразу видеть, какие точки останова будут сразу же приводить к переходу в режим Break, а какие— только при определенных обстоятельствах. Другие способы для перехода в режим останова Существуют еще два способа для перехода в режим Break. Первый подразумевает переход при выдаче необрабатываемого исключения, и будет описан позже в этой главе во время рассмотрения приемов обработки ошибок. Второй способ подразумевает переход в режим останова при генерации утверждения (assertion). Утверждениями называются такие инструкции, которые могут прерывать выполнение приложения с выдачей определенного пользователем сообщения. Они часто применяются во время разработки приложений для тестирования того, все ли работает гладко. Например, может потребоваться, чтобы в какой-то точке в приложении значение определенной переменной было меньше 10. С помощью утверждения можно удостовериться, что так оно и если, и прервать программу в противном случае. При генерации утверждения на выбор предлагается три действия: Abort (Прервать), которое завершает работу приложения; Retry (Повторить попытку), которое приводит к переходу в режим Break; и Ignore (Игнорировать), которое позволяет приложению продолжать работу обычным образом. Как и рассмотренные ранее функции для вывода отладочной информации, функция генерации утверждения имеет две версии: □ Debug.Assert() □ Trace.Assert() Отладочная версия компилируется только в отладочных сборках. Эти функции принимают три параметра. Первый представляет собой параметр Boolean, установка в false которого приводит к срабатыванию функции генерации утверждения. Второй и третий являются строковыми параметрами и отвечают за вывод информации, соответственно, во всплывающее диалоговое окно и окно Output. Для предыдущего примера вызов функции должен был бы выглядеть так: Debug.Assert(myVar < 10, "myVar is 10 or greater.", // Значение myVar равно или больше 10 "Assertion occurred in Main()."); Утверждения часто полезны на ранних стадиях адаптации приложения пользователем. Разработчик может включать в распространяемую среди пользователей рабочую версию своего приложения вызовы Trace.Assert () для наблюдения за происходящим. В случае генерации утверждения пользователь будет получать соответствующее уведомление. Информация об этом затем может пересылаться разработчику, а тот с ее помощью сможет определять, что было не так, даже если ему и не известно, как именно это случилось. Например, в первом строковом параметре разработчик может предоставлять краткое описание ошибки, а втором — инструкции касательно того, что делать дальше: Trace.Assert(myVar < 10, "Variable out of bounds.", // Переменная вышла за допустимые пределы "Please contact vendor with the error code KCW001."); // Свяжитесь с поставщиком и сообщите о коде ошибки KCW001
188 Часть I. Язык С# Если эта функция генерации утверждения сработает, пользователь увидит на экране диалоговое окно, показанное на рис. 7.13. I Aesartion Fatted: Abort-Qutt. Retry-Oebug. Ignort-Continue еЖ-Ш I f\ Variable out of bounds. Vi7 Please contact vendor with the error code KCW001. at Program.Main(StrlnQ(l args) C:«eevX:SrwpChaptef07As*ertlonDemoAs«er1»onOemoPTOora m.cs<14) at AppOomain. jiExecuteA*sembly(As*ernbly assembly. Strlng(] are») at AppOomaStExecuteAisernbMString assemblyf He, Evidence astemblySecurlty, Strlngf! args) at HostProc.FUjnUsereAtsembly() at ThreadHelper.Thf eadStart_Context(Ob|ect state) at Execu*ionContext.Run(ExecutlonContext executtonContext, ContextCaeback caaback, Object state) at ThreadHelper.ThreedStartO Abort Itetty Ignore Рис. 7.13. Диалоговое окно, отображаемое при срабатывании функции генерации утверждения Безусловно, данное окно не является особо дружественным по отношению к пользователю, поскольку в нем содержится масса информации, которая может сбивать с толку, но, получив такой снимок экрана, разработчику сможет быстро отыскать проблему. Теперь пришла пора узнать, что можно делать после остановки приложения и перехода в режим Break. Вообще переход в этот режим применяется для поиска ошибки в коде (или для получения уверенности в том, что все работает так, как надо). В режиме Break можно использовать самые разнообразные приемы, позволяющие анализировать код и точное состояние приложения в той точке, где оно было остановлено. Мониторинг содержимого переменных Мониторинг содержимого переменных является лишь одним примером того, как VS и VCE оказывают значительную помощь в отладке. Простейший способ для проверки значения переменной предусматривает наведение курсора мыши на ее имя в исходном коде, когда приложения находится в режиме Break. Это приводит к отображению всплывающей подсказки желтого цвета, содержащей информацию о переменной и ее текущем значении включительно. Подобным образом можно также выделять целые выражения и получать информацию об их результатах. Для сложных значений вроде массивов можно даже разворачивать записи в подсказке и просматривать отдельные детали элементов. Вы наверняка заметили, что при запуске приложения компоновка некоторых окон в IDE изменятся. По умолчанию во время выполнения, как правило, происходят следующие изменения (это поведение может немного отличаться, что зависит от каждой конкретной установки). □ Окно Properties исчезает вместе с некоторыми другими окнами, возможно, включая даже окно Solution Explorer. □ Окно Error List заменятся двумя новыми окнами, которые отображаются вдоль нижнего края окна IDE. □ В этих новых окнах отображается несколько новых вкладок.
Глава 7. Отладка и обработка ошибок 189 Новая компоновка показана на рис. 7.14. Данное представление может выглядеть не совсем так, как оно выглядит у вас на экране, т.е. некоторые вкладки и окна могут немного отличаться, но функциональные возможности, которые доступны в этих окнах и рассматриваются далее, будут в точности совпадать, а само представление, в принципе, можно настраивать любым желаемым образом как путем выбора в меню View (Вид) и Debug (Отладка) касающихся окон опций (в режиме Break), так и перетаскиванием окон по экрану для изменения их месторасположения. ч|СМ7Еж«1 ' ШС[] test Array ■ I 4, 7, 4. 2, 7, 3, 7, В. 3, 9, 1, 9 I; |Ж(] мхVeilndiceэ; in* axuVal - Haxiaa(test*xtay, "Jt eaxValIndices) ; ■atlMUaCluiaM «tin 101 toucd at element lad. (oreacb (in-, index in sjaxVal Indices) Con»ole.«nt«Line( index) ; static lni ltilM(in'(l integers, 0« U№|] Indices) , • X Output ■■■■»■■ ■■■■■■■"■■ ■naji Jl IHiHyaaeayy lv-2Wmri. ■ ■ : л output у,ТаУЛВГДЫ| iIBiШ<'' ' i n 1 ? со) io am A*c. 7.74. Внешний вид окна IDE при запуске приложения Одно из новых окон, которое появляется в нижнем левом углу, особенно полезно для отладки. Оно позволяет наблюдать за значениями переменных в приложении в режиме Break. Доступные в нем вкладки в VS и VCE выглядят немного по-разному. □ Вкладка Autos (Автоматические). Доступна только в VS и отображает все переменные, которые используются в текущем и предшествующих ему операторах (<Ctrl+D>, <A>). □ Вкладка Locals (Локальные). Отображает все переменные, которые находятся в области видимости (<Ctrl+D>, <L>). □ Вкладка Watch N (Слежение N). Отображает выбранные переменные и выражения (где под N подразумевается одна из тех четырех версий, которые доступны в Debug^Windows1^Watch). Все эти вкладки работают более или менее одинаково, предлагая различные дополнительные средства в соответствии с функцией, которой они служат. В общем, каждая вкладка содержит список переменных вместе с информацией об имени, значении и типе каждой из них. Для более сложных переменных, наподобие массивов, предусмотрены символы разворачивания и сворачивания дерева (+ и -), которые отображаются слева от их имени и позволяют получать доступ к древовидному представлению их содержимого.
190 Часть I. Язык С# Например, на рис. 7.15 показана вкладка Locals, полученная после размещения в примере кода точки останова. На вкладке видно развернутое представление одной из переменных типа массива — maxValIndices. Name ♦ args ф index ■ ф testArray #[0] ♦ [1] у maxVal —] Locals ^JWateti Value <string@]} 9 9 11 9 TypeП stnng[] mt «a ■■■■■■■ ШШ int mt mt Рш;. 7.75. Вкладка Locals с развернутым представлением переменной maxVal Indices Здесь также можно редактировать содержимое переменных. Это позволяет обходить любые другие операции присваивания значений переменным, которые могли происходить ранее в коде. Делается это просто вводом нового значения в столбце Value (Значение) для переменной, которую необходимо редактировать. Подобным подходом можно пользоваться, например, в сценариях, в которых в противном случае потребовалось бы вносить изменения в код. Окно Watch (или окна Watch, которых в VS может быть до четырех) позволяет наблюдать за конкретными переменными или за выражениями, содержащими конкретные переменные. Для его использования введите в столбце Name (Имя) имя требуемой переменной или выражения и просмотрите результат. Обратите внимание, что не все переменные в приложении находятся в области видимости постоянно, что соответствующим образом помечаются в окне Watch. Например, на рис. 7.16 показано окно Watch с несколькими демонстрационными переменными и выражениями внутри. [W** Т5П Name Value Type -I ~# maxVal * count 18 ** # ncfcesU] 11 mt О testArray The name testArray' does not exist m the current context ф [gfiocim jfgWatch | Puc. 7.16. Пример окна Watch Массив testArray является локальным по отношению к Main (), поэтому его значение здесь не отображается. Взамен показано сообщение, информирующее о том, что данная переменная в текущем контексте не существует. Добавлять переменные в окно Watch можно и с помощью их перетаскивания из исходного кода. Одно хорошее преимущество разного представления переменных, к которым получается доступ в этом окне, состоит в том, что это позволяет видеть переменные, у которых после прохождения точек останова изменились значения. Любое новое значение отображается выделенным красным цветом, а не черным, что значительно упрощает выявление изменившихся значений. Как уже упоминалось ранее, для добавления дополнительных окон Watch в VS в режиме Break можно использовать опции, которые доступны в меню Debug^Windows1^ Watch^Watch N и позволяют включать и отключать четыре возможных версии этого окна. В каждом из этих окон может содержаться отдельный набор наблюдаемых пе-
Глава 7. Отладка и обработка ошибок 191 ременных и выражение, что дает возможность группировать связанные между собой переменные и тем самым облегчать доступ к ним. Помимо этих окон в VS еще имеется окно QuickWatch, которое предоставляет детальную информацию о любой выбираемой переменной в исходном коде. Открыть его можно, щелкнув правой кнопкой мыши на необходимой переменной и выбрав в контекстном меню пункт QuickWatch. В большинстве случаев, однако, вполне достаточно стандартных окон Watch. Обратите внимание, что выбранные для наблюдения переменные и выражения сохраняются между сеансами выполнения приложения. То есть в случае завершения и последующего запуска приложения добавлять наблюдаемые переменные и выражения повторно не нужно — IDE запоминает то, какие переменные и выражения просматривались в последний раз. Пошаговое выполнение кода Пока что было рассказано лишь о том, как узнавать, что происходит в приложениях после их перевода в режим Break. Теперь пришла пора посмотреть, как использовать IDE для пошагового выполнения кода в режиме Break и просмотра результатов, к которым приводит выполнение каждой строки кода. Данный прием очень полезен при отладке приложений. После перехода в режим Break слева от кода появляется курсор, который обычно первоначально отображается внутри кружка красного цвета, представляющего точку останова, если именно она применялась для перехода в этот режим, и указывает на строку, которая должна выполняться следующей (рис. 7.17). TTW static void Hain(string[] агдз) ( |lnt[] testJLrray - ( 4, 7, 4, 2, 7, 3, 7, 8, 3, 9, 1, 9 ) ;| int[] maxValIndices; int maxVal - Maxima(testArray, out roaxValIndices); Console.VriteLine("naximum value tO) found at element indices: maxVal); foreach (xnt index in roaxvalIndices) < Console.¥riteLine(index); ) Tonsole.ReadKeyO ; Puc. 7.17. Курсор в режиме Break Он показывает, какой точки достигло выполнение кода, когда произошел переход в режим Break. Начиная с этой точки, выполнение кода можно продолжать построчно. Для этого используются кнопки панели инструментов Debug, показанные на рис. 7.18. Кнопка Step Over (Пропустить) Кнопка Step Into (Перейти к) Кнопка Step Out (Выйти) I ► п ш а | ♦ «■ Cji *i. нвх з • '..,.■—... .».«-,.. i. i. „.■■-......, .....*.«.». .■-...И1^«»« '■-■■■ -" - Рис. 7.18. Кнопки в панели инструментов Debug, которыми можно пользоваться для построчного выполнения кода На этом рисунке видно, что управление выполнением программы в режиме Break в панели Debug осуществляется с помощью шестой, седьмой и восьмой пиктограмм. В частности, они позволяют делать следующее.
192 Часть I. Язык С# □ Кнопка Step Into позволяет выполнять данный оператор и переходить к следующему оператору, который подлежит выполнению. □ Кнопка Step Over похожа на кнопку Step Into, но предусматривает пропуск вложенных блоков кода, кода функций включительно. □ Кнопка Step Out позволяет переходить сразу к концу блока кода и возобновлять режим Break со следующего за ним оператора. Для просмотра каждой выполняемой приложением операции лучше использовать кнопку Step Into, поскольку она предполагает последовательный проход по всем инструкциям, что включает проход даже по тому коду, который содержится внутри функций, вроде показанной в предыдущем примере Maxima (). Щелчок на этой кнопке при достижении курсором строки 15, в которой содержится вызов функции Maxima (), приведет к переходу курсора на первую строку кода внутри Maxima (). С другой стороны, щелчок на кнопке Step Over при достижении строки 15 приведет к переходу курсора прямо на строку 16, безо всякого прохождения кода внутри Maxima () (хотя этот код все равно выполняется). При случайном переходе к функции, код которой не представляет никакого интереса, можно воспользоваться кнопкой Step Out для возврата в код, где эта функция вызывалась. По мере прохождения кода значения переменных наверняка будут меняться. Легко удостовериться в этом позволят описанные выше окна мониторинга. В коде, содержащем семантические ошибки, такой прием может оказаться самым полезным из всех. Он позволяет переходить в коде сразу же к тому месту, которое является ожидаемым кандидатом на возникновение проблем, и получать все те ошибки, которые будут генерироваться при выполнении программы обычным образом, а также, по ходу, следить за данными и видеть, что конкретно послужило причиной. Позже в этой главе этот прием будет применяться для выяснения происходящего в примере приложения. Окна Immediate и Command Окно Command (доступное только в VS) и окно Immediate (доступное через выбор в меню Debug пункта Windows) позволяют выполнять команды во время работы приложения. В частности, окно Command позволяет вручную выполнять различные операции VS (т.е. операции, которые предлагаются в VS внутри меню и панелей инструментов), а окно Immediate — выполнять дополнительный код, помимо исходного кода, который уже выполняется, а также вычислять выражения. В VS оба эти окна являются, по сути, связанными между собой (в предыдущих версиях VS они трактовались как одно и то же окно). Между ними можно даже переключаться путем ввода соответствующих команд, а именно — команды immed для перехода из окна Command в окно Immediate, и команды >cmd для осуществления обратного перехода. В настоящем разделе основное внимание уделяется окну Immediate, поскольку окно Command является по-настоящему полезным только для выполнения сложных операций и к тому же доступно лишь в VS, в то время как Immediate доступно и в VS, и в VCE. Проще всего это окно использовать для вычисления выражений, что немного напоминает одноразовый способ применения окон Watch. Делается это вводом выражения и нажатием клавиши <Enter>. После этого в окне появляется запрошенная информация. На рис. 7.19 показан пример. Immediate Window t«stAiray[3] * 10 20 J} Output ^Call Start ^Immediate Window ~ Ф X • Puc. 7.19. Пример вычисления выражения в окне Immediate
Глава 7. Отладка и обработка ошибок 193 В этом окне также можно изменять содержимое переменных, как показано на рис. 7.20. I Immediate Window t*stArr»yC| * 10 20 t*stArr»y[3] +■ 7 I9 J Output ^jjCill Stack ^Immediate Window Puc. 7.20. Пример изменения (одержимого переменной в окне Immediate В большинстве случаев требуемый эффект гораздо легче получать с использованием описанных выше окон мониторинга переменных, но прием с применением этого окна все равно удобен для выполнения более тонкой настройки значений и прекрасно подходит для тестирования выражений, результаты которых позже вряд ли будут представлять какой-либо интерес. Окно Call Stack Последнее окно, которое осталось рассмотреть, называется Call Stack (Стек вызовов) и показывает, каким образом было достигнуто текущее местонахождение. Попросту говоря, это означает, что оно отображает текущую функцию вместе с функцией, которая ее вызвала, потом функцией, которая вызвала эту функцию и т.д. (т.е. список вложенных вызовов функций). Точные точки, в которых делаются вызовы этих функций, тоже фиксируются. В случае предыдущего примера при переходе в режим Break во время нахождения внутри функции Maxima () или перемещении внутрь этой функции, благодаря пошаговому выполнению кода, в окне Call Stack появится информация, показанная на рис. 7.21. | Call Stack Name MbftVaJiiUJtiiaJkWi^illailA'WWgWHCTJJ^FJW^ UiriYWJl^Vftfn'lffiiy t ifflii Till UrflTr ичгмшгтпштлп-чгг ** Ch07Ex01 .exe!Ch07Ex01 .Program.Man(stmgl] args - {string^]}) Une 15 + Oxb bytes Г") Output ^jCall Stack ^Immediate Window i »lx Lang * c# Puc. 7.21. Пример информации, отображаемой в окне Call Stack Выполнение двойного щелчка на записи позволит перейти к соответствующему месту в коде и отследить путь, которым выполнение кода достигло текущей точки. Данное окно особенно полезно при первом обнаружении ошибок, поскольку позволяет видеть, что произошло непосредственно перед возникновением той или иной ошибки. В случае возникновения ошибок в наиболее часто используемых функциях, это позволяет определять их источник. Иногда в этом окне отображается очень запутанная информация. Например, ошибки могут возникать и за пределами приложения, из-за неправильного применения внешних функций. В таких случаях в этом окне может содержаться длинный список записей, в котором только одна или две могут выглядеть знакомо. Просматривать внешние ссылки (при возникновении в том необходимости) можно, выполнив в этом окне щелчок правой кнопкой мыши и выбрав в контекстном меню пункт Show External Code (Показать внешний код). ~ д х
194 Часть I. Язык С# Обработка ошибок В первой части настоящей главы рассказывалось о том, как можно отыскивать и исправлять ошибки во время разработки приложения, чтобы они не возникали снова в рабочей версии кода. Иногда, однако, бывает известно, что ошибки все равно могут возникать и стопроцентного способа предотвратить их появление не существует. В таких ситуациях может быть предпочтительнее допускать вероятность возникновения проблем и писать код, являющийся достаточно надежным для того, чтобы справляться с этими ошибками аккуратным образом, без прерывания процесса выполнения. Все существующие для этого приемы вместе называются обработкой ошибок. В настоящем разделе речь пойдет об исключениях и том, как их обрабатывать. Исключением (exception) называется такая ошибка, которая генерируется в основном коде или в какой-то вызываемой им функции во время выполнения. Определение ошибки здесь немного более размыто, чем было до этого, поскольку исключения могут генерироваться вручную, внутри функций и т.д. Например, исключение внутри функции может генерироваться в случае, если один из ее строковых параметров не начинается с буквы "а". Строго говоря, оно в таком случае не является ошибкой за рамками контекста функции, но будет восприниматься как таковое вызывающим эту функцию кодом. Вам уже доводилось встречаться с исключениями несколько раз в этой книге. Пожалуй, простейшим примером является попытка обратиться к элементу массива за его пределами: int[] myArray = {1, 2, 3, 4); int myElem = myArray[4]; Этот код приведет к отображению приведенного ниже сообщения об исключении и завершению приложения: Index was outside the bounds of the array. Индекс выходит за пределы массива. В этой книге уже показывались примеры окна, которое отображаться в подобных случаях. Оно содержит информацию о вызвавшем проблему коде, ссылки для доступа к соответствующим материалам справочной системы .NET, а также ссылку View Detail (Просмотреть детали), позволяющую получать дополнительные сведения о возникшем исключении. Исключения определяются в пространствах имен и в большинстве случаев имеют имена, которые делают очевидным их предназначение. В приведенном примере сгенерированное исключение имеет имя System. IndexOutOfRangeException, в котором есть смысл, поскольку предоставленный индекс выходит за пределы диапазона (out of range), допустимого в myArray. Появление сообщения об исключении и завершение работы приложения происходит только в тех случаях, когда исключение никак не обрабатывается. В следующем разделе будет показано, что нужно делать для обработки исключения. Ключевые слова try.. .catch.. .finally В состав языка С# входит синтаксис для структурной обработки исключений (Structured Exception Handling— SHE). С помощью трех ключевых слов— try, catch и finally— код помечается, как способный обрабатывать исключения, и снабжается инструкциями касательно того, что именно ему делать при возникновении исключения. Все ключевые слова имеют ассоциируемый блок кода и должны обязательно использоваться в строго следующем друг за другом порядке. Базовая структура выглядит, как показано ниже:
Глава 7. Отладка и обработка ошибок 195 catch (<тип_исключения> е) finally Еще, однако, допускается создавать блоки try и finally без блока catch или блок try с несколькими блоками catch. При наличии одного и более блоков catch блок finally является необязательным, а в противном случае, при отсутствии блоков catch, соответственно, наоборот — обязательным. Способы применения всех этих блоков описаны ниже. □ try. Содержит код, который может генерировать (throw) исключения. □ catch. Содержит код, подлежащий выполнению при выдаче исключений. Блоки catch могут настраиваться так, чтобы они реагировали только на исключения определенных типов (например, исключения типа System. IndexOutOfRange Exception), путем использования параметра <тип_исключения>, отсюда и происходит возможность предоставлять множество блоков catch. Также допускается и полностью опускать этот параметр и тем самым получать универсальный блок catch, способный реагировать на исключения всех типов. □ finally. Содержит код, который выполняется всегда, либо после блока try, если не возникает никаких исключений, либо после блока catch, если обрабатывается какое-то исключение, или же непосредственно перед тем, как какое- нибудь необработанное исключение приведет к завершению приложения (тот факт, что данный блок обрабатывается в такой момент и служит причиной его существования; иначе код с таким же успехом можно было бы помещать и после этого блока). Ниже приведена последовательность событий, происходящих после возникновения исключения в коде внутри блока try. □ Выполнение блока try останавливается в точке, где возникло исключение. □ Если существует какой-нибудь блок catch, тогда выполняется проверка на предмет соответствия этого блока типу сгенерированного исключения. Если никакого блока catch не существует, тогда выполняется блок finally (который, если нет ни одного блока catch, должен присутствовать обязательно). □ Если блок catch существует, но не является подходящим, выполняется проверка на предмет наличия других подходящих блоков catch. □ Если обнаружить совпадающий с типом исключения блок catch удается, выполняется содержащийся в нем код, после чего выполняется блок finally, если он присутствует. □ Если не удается обнаружить ни одного совпадающего с типом исключения блока catch, выполняется блок кода finally, если таковой присутствует. В следующем практическом занятии демонстрируется пример работы с исключениями — генерация исключений и несколько способов их обработки.
196 Часть I. Язык С# акшческое занятие Обработка исключений 1. Создайте новое консольное приложение по имени Ch07Ex02 и сохраните его в каталоге С: \BegVCSharp\Chapter07. 2. Измените его код следующим образом (показанные здесь комментарии с номерами строк помогут сопоставить код с приведенным далее описанием): class Program { static string [] eTypes = {"none", "simple", "index", "nested index"}; static void Main(string[] args) { foreach (string eType in eTypes) { try { Console.WriteLine("Main() try block reached."); // Строка 23 // Достигнут блок try в Main () Console.WriteLine("ThrowException (\"{0}\") called.", eType);// Строка 24 // Вызвано ThrowException ThrowException(eType); Console.WriteLine("Main() try block continues."); // Строка 26 // Выполнение блока try в Mam() продолжается } catch (System.IndexOutOfRangeException e) // Строка 28 { Console.WriteLine ("Main () System.IndexOutOfRangeException catch" + " block reached. Message:\n\"{0}\"", e.Message); } catch // Строка 34 { Console.WriteLine("Main() general catch block reached."); // Достигнут общий блок catch в Main() } finally { Console.WriteLine("Main() finally block reached."); // Достигнут основной блок catch в Main() } Console.WriteLine() ; } Console.ReadKey(); } static void ThrowException (string exceptionType) { Console.WriteLine("ThrowException (\"{0}\") reached.", exceptionType); // Строка 4 9 // Достигнута функция ThrowException switch (exceptionType) { case "none" : Console.WriteLine ("Not throwing an exception."); // Исключение не генерируется break; // Строка 54
Глава 7. Отладка и обработка ошибок 197 case "simple" : Console.WriteLine("Throwing System.Exception."); // Генерируется исключение System.Exception throw (new System.Exception ()); // Строка 57 case "index" : Console.WriteLine("Throwing System.IndexOutOfRangeException . "); // Генерируется исключение System.IndexOutOfRangeException eTypes[4] = "error"; // Строка 60 break; case "nested index" : try // Строка 63 { Console.WriteLine("ThrowException(V'nested index\") " + "try block reached."); // Достигнут блок try в ThrowException Console.WriteLine("ThrowException(\"index\") called."); // Вызвана функция ThrowException ThrowException("index"); // Строка 68 } catch // Строка 70 { Console.WriteLine("ThrowException(\"nested index\") general" + " catch block reached."); // Достигнут основной блок catch в ThrowException } finally { Console.WriteLine ("ThrowException (V'nested indexV) finally" + " block reached."); // Достигнут блок finally в ThrowException } break; } } } 3. Запустите приложение. На рис. 7.22 показан результат, который должен получиться. Рис. 7.22. Приложение Ch07Ex02 в действии
198 Часть I. Язык С# Описание полученных результатов У этого приложения в Main () имеется блок try, в котором вызывается функция по имени ThrowException (). Эта функция может генерировать различные исключения, в зависимости от того, с каким из параметров она вызывается: □ ThrowException ("none"): не выдает исключения; □ ThrowException ("simple"): генерирует общее исключение; □ ThrowException ("index"): генерирует исключение System. IndexOutOfRange Exception; □ ThrowException ("nested index"): содержит свой собственный блок try, внутри которого находится код, вызывающий ThrowException ("index") для генерации исключения типа System. IndexOutOfRangeException. Каждый из этих string-параметров хранится в глобальном массиве eTypes, который в цикле проходится в Main () для выполнения вызова ThrowException () по одному разу с каждым возможным параметром. Во время этого цикла на консоль выводятся различные сообщения для информирования о происходящем. Этот код предоставляет замечательную возможность испробовать описанные ранее в главе приемы пошагового выполнения кода. Выполняя данный код по одной строке за раз, можно в точности понять, как он функционирует. Чтобы сделать это, добавьте новую точку останова (со стандартными свойствами) в строке кода под номером 23, которая выглядит следующим образом: Console.WriteLine("Main() try block reached."); Здесь для работы с кодом используются номера строк, в том виде, в каком они представлены в загружаемом коде. Если функция нумерации строк отключена, включите ее, выбрав в меню Tools пункт Options либо выбрав Text Editors C# и перейдя в раздел параметров General). Комментарии были включены в код с целью упрощения. Запустите приложение в режиме Debug. Практически сразу же программа перейдет в режим Break, а курсор будет находиться в строке 23. Перейдя на вкладку Locals окна мониторинга переменных, вы должны увидеть, что еТуре в настоящий момент имеет значение "попе". Воспользуйтесь кнопкой Step Into для выполнения строк 23 и 24 и удостоверьтесь в том, что первая строка текста была действительно выведена на консоль. Далее снова воспользуйтесь кнопкой Step Into для перехода к функции ThrowExpcetion () в строке 25. После перехода к функции ThrowException () (в строке 49), содержимое окна Locals изменится. еТуре и args покидают область видимости (потому что являются локальными по отношению к Main ()) и вместо них появится локальный аргумент exceptionType тоже, конечно же, со значением "попе". Продолжайте щелкать на кнопке Step Into и тогда достигнете оператора switch, который проверит значение exceptionType и выполнит код, выводящий на экран строку Not throwing an exception. При выполнении оператора break (в строке 54) произойдет выход из кода функции и возобновится обработка кода в Main () в строке 26. Поскольку никакого исключения сгенерировано не было, далее продолжится выполнения кода в блоке try. Затем обработка доходит до блока finally. Щелкните на кнопке Step Into еще несколько раз, чтобы пройти блок finally и первую итерацию цикла foreach. Когда вы в следующий раз дойдете до строки 25, функция ThrowExcetion () будет вызвана с использованием уже другого параметра, а именно — simple. Продолжайте использовать кнопку Step Into для выполнения и, в конце концов, достигнете строки 57: throw (new System.Exception ());
Глава 7. Отладка и обработка ошибок 199 В этой строке используется ключевое слово throw, которое предназначено для генерации исключения. Его нужно просто предоставлять вместе с инициализируемым с помощью new исключением в качестве параметра и тогда оно будет выдавать соответствующее исключение. В данном примере вместе с ними предоставляется еще одно исключение из пространства имен System, а именно— System.Exception. Никакого оператора break; в данном случае не требуется: структуры block-throw вполне достаточно для завершения выполнения блока. При обработке данного оператора с помощью кнопки Step Into вы окажетесь в общем блоке catch, который начинается в строке 34. Никаких совпадений с предыдущим блоком, который начинался в строке 28, обнаружено не было, поэтому вместо него и обрабатывается данный блок. Пошаговое выполнение этого кода позволит пройти этот блок, пройти блок finally и вернуться снова к выполнению еще одной итерации цикла в строке 25, предусматривающей вызов ThrowException ()c новым параметром, каковым на этот раз будет "index". Теперь ThrowException () сгенерирует исключение в строке 60: eTypes[4] = "error"; Массив eTypes является глобальным, поэтому проблема здесь состоит не в невозможности получения доступа к этому массиву, а в попытке получить доступ к пятому элементу в нем (вспомните, что отсчет элементов начинается с 0), что приводит к генерации исключения System. IndexOutOfRangeException. На этот раз в Main () имеется подходящий блок catch и поэтому дальнейший проход по коду приведет именно к этому блоку, который начинается в строке 28. Вызов Console. WriteLine () в данном блоке предусматривает вывод на экран хранящегося в исключении сообщения с использованием е .Message (исключение доступно через параметр этого блока catch). Опять-таки, пошаговое выполнение приводит сначала к прохождению блока finally (а не второго блока catch, поскольку исключение уже обработано), а затем — еще одной итерации цикла снова с вызовом ThrowException () в строке 25. При достижении структуры switch в ThrowException () на этот раз произойдет вход в новый блок try, начинающийся в строке 63. А при достижении строки 48 будет выполнен вложенный вызов ThrowException (), на этот раз с параметром "index". Можете воспользоваться кнопкой Step Over для пропуска подлежащих выполнению строк кода, потому вы уже их проходили. Как и ранее, данный вызов приведет к генерации исключения System. IndexOutOfRangeException, но на этот раз оно будет обрабатываться во вложенной структуре try. . .catch. . . finally, а именно— той, что находится внутри ThrowException (). В этой структуре нет никакого явного совпадения для исключения такого типа, поэтому его обработкой займется код в общем блоке catch (который начинается в строке 70). Как и при обработке предыдущих исключений, далее вы пройдете этот блок catch и ассоциируемый с ним блок finally и достигнете кода вызова, но с одним важным отличием: хотя исключение и было сгенерировано, на этот раз оно было еще и обработано— кодом в ThrowException (). А это значит, что никакого исключения для обработки в Main () не останется и поэтому далее вы перейдете непосредственно к блоку finally, после чего произойдет завершение приложения. Просмотр и конфигурирование исключений В .NET Framework содержится множество исключений различного типа, любое из которых разработчик может генерировать и обрабатывать в своем коде или даже генерировать из своего кода так, чтобы оно могло перехватываться в более сложных
200 Часть I. Язык С# приложениях. В IDE предусмотрено специальное диалоговое окно для просмотра и редактирования доступных исключений, отобразить которое можно, выбрав в меню Debug (Отладка) пункт Exceptions (Исключения) (или нажав <Ctrl+D>, <E>). Это диалоговое окно показано на рис. 7.23 (в VS отображающиеся в списке элементы могут выглядеть по-другому). 1 Exceptions Break when an exception is: j Name : мШИШШШШШНЯМЯ ♦ M«n«ged Debugging Assistants Jhrown User-unhandled r^\ 1 «ill Cancel 1 П*. 1 1 f;ra Next 1 1 RMttAII I 1 1 **- I Delete 1 ■——J Рис. 7.23. Диалоговое окно Exceptions Исключения в этом окне отображаются по категориям и пространствам имен библиотеки .NET. Увидеть исключения, доступные в пространстве имен System, например, можно, развернув сначала узел Common Language Runtime Exceptions (Исключения общеязыковой исполняющей среды), а затем узел System (Система). В списке, который появится после этого, будет присутствовать и уже продемонстрированное ранее в главе исключение System. IndexOutOfRangeException. Каждое исключение можно конфигурировать с использованием флажков, которые расположены рядом. Первый флажок — Thrown (Генерируется) — позволяет делать так, чтобы прерывание и переход в режим отладки происходил даже в случае возникновения обрабатываемых исключений, а второй — так, чтобы необрабатываемые исключения игнорировались со всеми вытекающими из этого последствиями. В большинстве случаев игнорирование необрабатываемых исключений все равно приводит к переходу в режим Break, так что второй флажок применяется лишь в особых случаях. Обычно предлагаемые по умолчанию настройки вполне приемлемы. Примечания по обработке исключений Перед выполнением операций по перехвату более общих исключений нужно всегда предоставлять блоки catch для более специфических исключений. В случае неправильного оформления этого момента приложение компилироваться не будет. Еще обратите внимание на то, что генерировать исключения можно и внутри самих блоков catch, либо тем способом, который применялся в предыдущем примере, либо просто с использованием следующего выражения: throw; Это выражение приводит к повторной выдаче обрабатываемого блоком catch исключения. В случае выдачи исключения подобным образом оно будет обрабатываться не текущим блоком try. . .catch. . .finally, а родительским кодом (хотя блок finally во вложенной структуре выполняться все равно будет).
Глава 7. Отладка и обработка ошибок 201 Например, если изменить блок try. . .catch. . .finally в ThrowException () из предыдущего примера, как показано ниже: try { Console. WnteLine ("ThrowException (V'nested indexV) " + "try block reached."); Console. WriteLme ("ThrowException (V'indexV) called. ") ; ThrowException("index"); } catch { Console.WriteLine ("ThrowException (\"nested indexV) general" + " catch block reached."); throw; } finally { Console.WriteLine("ThrowException (V'nested index\") finally" + " block reached."); } тогда поток выполнения сначала будет переходить к показанному здесь блоку finally, и только затем к соответствующему блоку catch в Main (). Отображаемый в результате в окне консоли вывод станет выглядеть по-другому, а именно — так, как показано на рис. 7.24. Рис. 7.24. Вывод в окне консоли после изменения блока try. . .catch. . .finally На этом экранном снимке видны дополнительные строки, которые будет выводить функция Main (), поскольку теперь исключение System. IndexOutOfRangeException перехватывается в ней. Резюме В этой главе было рассказано о приемах отладки приложений. Таких приемов существует довольно много, и большинство из них подходит для использования с проектами любого типа, а не только с консольными приложениями. В частности, в настоящей главе были освещены следующие вопросы. □ Способы использования Debug.WriteLine () и Trace .WriteLine () для вывода текста в окно Output. □ Применение точек трассировки для вывода текста в окно Output.
202 Часть I. Язык С# □ Переход и работа в режиме Break с использованием разнообразных точек останова. □ Применение окон с отладочной информацией в VS. □ Пошаговое выполнение кода. □ Обработка исключений с помощью блоков try. . . catch. . . finally. Теперь вам известно обо всем, что может потребоваться для создания простых консольных приложений, равно как и для их отладки. В следующей главе речь пойдет о мощной технологии объектно-ориентированного программирования. Упражнения 1. "Предпочтительнее использовать Trace .WriteLine (), а не Debug.WriteLine (), поскольку последняя версия работает только в отладочных сборках". Согласны ли вы с данным утверждением, и если да, то почему? 2. Напишите код для простого приложения с циклом, предусматривающим генерацию ошибки после выполнения 5000 итераций. Используйте точку останова для перехода в режим Break непосредственно перед возникновением ошибке на 5000-й итерации. (Примечание: проще всего добиться генерации ошибки, попытавшись получить доступ к несуществующему элементу массива, например, к myArray [1000] в массиве, который содержит сотню элементов). 3. "Блоки кода finally выполняются только в том случае, если не выполняется блок catch". Так это или нет? 4. С учетом наличия перечисления по имени orientation, определение которого приведено в показанном ниже коде, напишите приложение, использующее технологию структурной обработки исключений (SEH) для безопасного приведения переменной типа byte к orientation. (Примечание: с помощью ключевого слова checked можно обеспечить принудительную генерацию исключений, пример чего и показан здесь. Обязательно используйте этот код в приложении.) enum orientation : byte { north = 1, south = 2, east = 3, west = 4 } myDirection = checked((orientation)myByte);
Введение в объектно- ориентированное программирование К этому моменту в настоящей книге был изложен весь базовый материал по синтаксису языка С# и программированию на нем, а также показано, как выполнять отладку приложений и создавать пригодные для использования консольные приложения. Однако для получения доступа к по-настоящему мощным возможностям С# и .NET Framework необходимо обратиться к объектно-ориентированному программированию (ООП). На самом деле, как станет понятно уже совсем скоро, эти приемы уже демонстрировались в этой книге, но для простоты внимание на этом просто не акцентировалось. В настоящей главе описание кода временно откладывается, а все внимание уделяется рассмотрению принципов, лежащих в основе объектно-ориентированного программирования. Это снова подразумевает возврат к языку С#, поскольку между ним и ООП существует тесная связь. Все понятия, приводимые в этой главе, будут более подробно рассматриваться в последующих главах на конкретных примерах, поэтому не стоит переживать, если в каком-то материале не получится разобраться сразу. В первую очередь здесь затрагиваются основы ООП и, конечно же, даются ответы на самые фундаментальные вопросы вроде того, что вообще собой представляет объект. Терминология ООП поначалу может показаться несколько запутанной, поэтому предоставляется много объяснений, которые, помимо всего прочего, также позволяют увидеть и то, что для применения ООП необходимо взглянуть на программирование по-другому. Помимо общих принципов ООП в главе затрагивается и область, которая требует глубокого понимания ООП — приложения Windows Forms. Приложения этого типа (работающие в среде Windows и имеющие элементы управления вроде меню, кнопок и т.п.) предоставляют огромную область для описания и позволяют эффективно проиллюстрировать основные моменты ООП в среде Windows Forms.
204 Часть I. Язык С# В частности, в настоящей главе рассматриваются следующие основные темы. □ Что собой представляет ООП. □ Какие существует приемы ООП. □ Каким образом приложения Windows Forms основываются на ООП. Концепции объектно-ориентированного программирования, описанные в данной главе, по сути, рассчитаны на среду .NET, из-за чего некоторые из приводимых здесь приемов могут не работать в других средах, поддерживающих ООП. Во время программирования на С# необходимо применять ориентированные на .NET приемы ООП, поэтому именно на них и делается акцент. Что собой представляет объектно- ориентированное программирование Объектно-ориентированное программирование является относительно новым подходом к созданию компьютерных приложений, который стремится устранить многие из проблем, существующих в традиционных методиках программирования. Тот вид программирования, с которым доводилось пока иметь дело в этой книге, называется функциональным (или процедурным) программированием и часто приводит к созданию так называемых монолитных приложений, все функциональные возможности в которых содержатся в нескольких модулях кода (а зачастую и вообще в одном). Приемы ООП обычно подразумевают использование гораздо большего количества модулей кода, каждый из которых предоставляет специфическую функциональность и может быть изолирован или существовать совершенно независимо от всех остальных. Такое модульное программирование обеспечивает гораздо большее разнообразие и возможности для многократного использования кода. Чтобы еще лучше показать, о чем идет речь, представим, что высокопроизводительное приложение на компьютере является высококлассным гоночным автомобилем. В случае создания с помощью традиционных приемов программирования этот спортивный автомобиль будет представлять собой одно целое. При желании улучшить что-нибудь в этом автомобиле его придется заменять целиком, т.е. отправлять обратно изготовителю и позволять их профессиональному механику модернизировать его, или вообще покупать новый автомобиль. В случае же применения ООП достаточно будет, например, купить у изготовителя новый мотор и самостоятельно заменить его, следуя инструкциям изготовителя и не погружаясь глубоко в изучение деталей по проведению ремонтных работ. В более традиционном приложении поток выполнения зачастую является простым и линейным. Приложения загружаются в память, начинают выполняться в точке А, завершают свою работу в точке Б и затем выгружаются из памяти. Во время этого могут использоваться и другие разнообразные сущности, вроде файлов на носителе данных или возможности видеокарты, но основная часть обработки происходит в одном месте. Код обычно занимается манипулированием данных, используя для этого различные математические и логические средства. Методы манипулирования обычно выглядят довольно просто и подразумевают использование базовых типов, вроде целочисленных или булевских, для построения более сложных представлений данных. В случае применения ООП подобная линейность практически не встречается. Хотя результаты достигаются те же, путь к ним зачастую выглядит совершенно по- другому. В ООП основной акцент делается на структуру и значение данных, а также на взаимодействие этих данных с другими данными. Это обычно выражается в боль-
Глава 8. Введение в объектно-ориентированное программирование 205 шем количестве усилий на этапах проектирования приложения, но обеспечивает его таким полезным свойством, как способность к расширению. После принятия соглашения касательно представления конкретных типов данных, это соглашение может применяться в последующих версиях приложения и даже в совершенно новых приложениях. Сам факт существования подобного соглашения может значительно сокращать время разработки. Этим можно объяснить то, почему работает пример с гоночным автомобилем. В данном примере таким соглашением является способ структуризации кода "мотора", который позволяет легко подставлять новый код (нового мотора), а не возвращать его полностью обратно изготовителю. Помимо этого он еще также позволяет использовать мотор после создания и для других целей, например, помещать в другой автомобиль или применять на яхте. Помимо соглашения по представлению данных, приемы ООП часто упрощают дело, обеспечивая соглашения по структуре и применению более абстрактных объектов. Например, соглашение может приниматься не только по формату данных, который должен использоваться для отправки выходных данных на устройство вроде принтера, но также и по методам обмена данными с этим устройством, т.е. инструкциями, которые оно может понимать, и т.д. В случае примера с гоночным автомобилем такое соглашение могло бы охватывать способ подключения мотора к топливному баку, систему передачи и т.п. Как видно из названия этой технологии, достигается это все за счет объектов. Что собой представляет объект Объект является своего рода "строительным блоком" в ООП-приложении. Этот строительный блок инкапсулирует часть приложения, каковой может быть процесс, порция данных или какой-то более абстрактный объект. В самом простом смысле объект может быть очень похож на тип структуры, вроде тех, что демонстрировались ранее в книге и содержали члены типа переменных и функций. Содержащиеся переменные могут представлять в объекте хранимые данные, а содержащиеся функции — обеспечивать доступ к возможностям этого объекта. Чуть более сложные объекты могут не обслуживать никаких данных, а вместо этого представлять процесс и содержать только функции. Например, таким объектом может быть объект, представляющий принтер, с функциями, позволяющими работать с принтером (распечатывать на нем документ, тестовую страницу и т.д.). Объекты в языке С# создаются из типов, точно так же, как уже рассмотренные переменные. Тип объекта называется в ООП особым образом, а именно — классом. Определения классов могут использовать для создания объектов, под которыми понимаются реальные именованные экземпляры класса. Понятия "экземпляр класса" и "объект" означают здесь одно и то же, но термины "класс" и "объект", на что следует обратить внимание, обозначают совершенно разные вещи. Термины "класс" и "объект " часто путают, поэтому очень важно разобраться в том, чем они отличаются. Помочь в этом может применение к ним приводившейся ранее аналогии с гоночным автомобилем. Классом может считаться шаблон автомобиля или, возможно, план по изготовлению автомобиля, а объектом — сам автомобиль, поскольку он является реализацией этих планов (их экземпляром). В настоящей главе для работы с классами и объектами используется синтаксис языка UML (Unified Modeling Language — унифицированный язык моделирования). Этот язык был специально разработан для моделирования приложений, начиная от объектов, которые должны входит в их состав, и операций, которые они должны выполнять, и заканчивая предполагаемыми способами их использования. Здесь применяют-
206 Часть I. Язык С# ся и объясняются только базовые возможности языка UML. Более сложные аспекты не затрагиваются, потому что изучение UML является специализированной темой, которой посвящены целые книги. В состав VS входит средство для просмотра классов, которое в своей области тоже является очень мощным инструментом. На рис. 8.1 для примера показано UML-представление класса принтера по имени Printer. Имя класса отображается в самом верхнем разделе данного прямоугольника (о двух нижних разделах будет рассказываться позже). На рис. 8.2 показано UML-представление экземпляра данного класса Printer по имени myPrinter. Printer I myPrinter: Printer Рис. 8.1. UML-представление класса Prin ter Рис. 8.2. UML-представление экземпляра класса Printer по имени myPrinter Здесь первым в верхнем разделе отображается имя экземпляра, а за ним — имя его класса. Эти имена отделены друг от друга двоеточием. Свойства и поля Свойства и поля обеспечивают доступ к содержащимся в объекте данным. Эти данные как раз и являются тем, что отличает отдельные объекты друг от друга, поскольку в свойствах и полях разных объектов одного и того же класса могут храниться разные значения. Различные фрагменты содержащихся в объекте данных все вместе образуют состояние этого объекта. Например, предположим, что имеется класс объектов, представляющий чашку кофе и потому имеющий имя CupOfCoffee. При создании экземпляра этого класса (т.е. создании объекта этого класса), для него нужно обязательно обеспечить состояние, чтобы он нес какой-то смысл. Для этого можно использовать свойства и поля с целью предоставления использующему данный объект коду возможности определять сорт используемого кофе, то, содержится ли в кофе молоко и/или сахар, является ли кофе растворимым и т.д. Тогда каждый конкретный объект CupOfCofee будет иметь определенное состояние, например: "Чашка колумбийского кофе с молоком и двумя ложками сахара". Как поля, так и свойства добавляются путем ввода, поэтому информация может в них сохраняться в виде значений string, int и т.д. Свойства, однако, отличаются от полей тем, что они не предоставляют непосредственного доступа к данным. Объекты могут ограждать пользователей от мельчайших деталей своих данных, которые не нуждаются в представлении "один к одному" существующих свойств. Скажем, в случае поля для сохранения информации о количестве ложек сахара в экземпляре CupOfCof f ее пользователи смогут помещать в него любые желаемые значения и подвергаться лишь тем ограничениям, которые имеются у использованного для хранения этой информации типа. То есть, например, в случае использования для хранения этих данных типа int пользователи смогут помещать в это поле любое значение в диапазоне от -2 147 483 648 до 2 147 483 647, как было показано в главе 3. Очевидно, что не все значения в этом диапазоне будут иметь смысл, особенно отрицательные, да
Глава 8. Введение в объектно-ориентированное программирование 207 и некоторые большие положительные тоже могут требовать чашки необычайно больших размеров. В случае же применения свойства для сохранения этой информации данное значение можно легко ограничить, скажем, только числами от 0 до 2. В общем, для доступа к состоянию лучше предоставлять свойства, а не поля, поскольку они позволяют управлять большим количеством аспектов. Этот выбор никак не влияет на код, применяющий экземпляры объектов, потому что синтаксис для использования свойств и полей выглядит одинаково. Тип доступа (для чтения или для записи) к свойствам тоже может четко определяться объектом. Некоторые свойства могут быть доступными только для чтения, т.е. позволять осуществлять просмотр, но не вносить изменения (по крайней мере, напрямую). Такой прием часто оказывается удобным для выполнения одновременного считывания нескольких фрагментов состояния. Например, класс CupOfCoffee может иметь доступное только для чтения свойство по имени Description, возвращающее строку с информацией о состоянии экземпляра этого класса (вроде той, что приводилась выше). Разумеется, те же самые сведения могут собираться и за счет опроса нескольких свойств, но наличие такого свойства может сэкономить время и усилия. Аналогичное можно проделывать и с доступными только для записи свойствами. Помимо доступа для чтения и записи, в отношении свойств и полей могут еще задаваться разные права доступа, называемые доступностью. Доступность определяет то, какой код может получать доступ к данным членам, т.е. являются они доступным для всего кода (общедоступными), только для кода внутри класса (приватными) или следуют более сложной схеме (о которой более подробно будет рассказываться позже в этой главе). Одним из наиболее распространенных приемов считается делать поля приватными и открывать к ним доступ через общедоступные свойства. При таком подходе код внутри класса имеет прямой доступ к хранящимся в поле данным, а общедоступное свойство ограждает внешних пользователей от этих данных и не позволяет им помещать туда недействительное содержимое. Об общедоступных членах говорят, что они представляются классом. Для большей наглядности можно провести аналогию с рассматривавшейся ранее областью видимости переменных. О приватных полях и свойствах, например, можно сказать, что они являются локальными для объекта, который ими владеет, а об общедоступных — что их область видимости также охватывает и внешний по отношению к объекту код. В UML-представлении класса имена свойств и полей отображаются во втором разделе, как показано на рис. 8.3. Здесь показано UML-представление класса CupOf Coffee, имеющего пять определенных членов (ими могут быть как свойства, так и поля, поскольку в UML между свойствами и полями нет никакой разницы). В каждой из представленных записей содержится описанная ниже информация. □ Доступность. Для общедоступных членов используется символ +, а для приватных — символ -. В целом, однако, приватные члены на приводимых в настоя- CupOfCoffee +BeanType : string -Hnstant: bool +Milk : bool +Sugar: byte ♦Description : string щей главе диаграммах не показываются, поскольку г, 0 э ,~ ^. , г „ 7 Рис. 8.3. Отображениеимен такая информация является внутренней для класса. ~ \. ТТЛЛТ . -> 1 свойств и полей в UML-npeo- Никакая информация касательно доступа для чте- ^ г ^ / -"• ставлении класса ния и записи не предоставляется. □ Имя члена. □ Тип члена. Для разделения имен членов и типов применяется двоеточие.
208 Часть I. Язык С# Методы Метод— это термин, которым принято называть представляемую объектом функцию. Методы могут вызываться так же, как и любые другие функции, и тем же самым образом использовать возвращаемые значения и параметры (о функциях более подробно говорилось в главе 6). Методы применяются для обеспечения доступа к функциональным возможностям объекта. Подобно полям и свойствам, они могут быть общедоступными или приватными и тем самым ограничивать доступ для внешнего кода необходимым образом. Они нередко подразумевают использование состояния объекта в своих операциях и имеют доступ к приватным членам, вроде приватных полей, если в том есть необходимость. Например, класс CupOfCoffee может иметь метод AddSugar (), предоставляющий более удобочитаемый синтаксис для увеличения количества сахара, чем установка соответствующего свойства Sugar. В UML-представлении методы внутри прямоугольников классов отображаются в третьем разделе, как показано на рис. 8.4. CupOfCoffee +BeanType : string ч-lnstant: bool +Milk : bool -i-Sugar: byte +Description : string +AddSugar(in amount: byte) byte Рис. 8.4. Отображение методов в UML- представлении класса CupOfCoffee Синтаксис методов в принципе похож на синтаксис полей и свойств, но только в нем еще отображается возвращаемый тип и параметры методов. Каждый параметр отображается в UML с одним из следующих идентификаторов: in, out или inout. Эти идентификаторы служат для обозначения направления потока данных, причем out и inout, грубо говоря, соответствуют в языке С# применению таких ключевых слов, как out и ref, о которых рассказывалось в главе 6, a in — поведению, которое применяется в языке С# по умолчанию тогда, когда не используются ни out, ни ref. Объектом является все, что угодно На этом этапе пришла пора внести ясность: объекты, свойства и методы уже встречались в этой книге не раз. На самом деле в языке С# и .NET Framework объектом является все, что угодно. Функция Main () в консольном приложении является методом класса. Каждый из рассмотренных ранее типов переменных является классом. Каждая из продемонстрированных команд, наподобие <строка>. Length, <строка>.То11ррег () и т.д., представляет собой свойство или метод. (Символ точки здесь, по сути, отделяет имя экземпляра объекта от имени свойства или метода.) Объекты действительно встречаются повсюду, и синтаксис их использования зачастую выглядит очень просто. До этого момента рассматривались лишь кое-какие фундаментальные аспекты С#, и все приводимые примеры были достаточно простыми. С этого момента начнется более детальное изучение объектов. Очень важно запомнить, что описываемые здесь концепции имеют далеко идущие последствия, способные коснуться даже той простой маленькой переменной int, с которой было так легко иметь дело.
Глава 8. Введение в объектно-ориентированное программирование 209 Жизненный цикл объекта Каждый объект имеет четко определенный жизненный цикл. Помимо обычного состояния, под которым подразумевается нахождение объекта в использовании, этот жизненный цикл включает два следующих важных этапа. □ Этап конструирования. При создании экземпляра объекта в первый раз он должен инициализироваться. Этот самый процесс инициализации и называется конструированием и выполняется он функцией-конструктором. □ Этап деструкции. При уничтожении объекта часто должны выполняться какие- то операции по очистке, вроде операции освобождения памяти. За их выполнение отвечает функция-деструктор. Конструкторы Базовая инициализация объекта осуществляется автоматически. Например, заботиться о поиске того места в памяти, в котором сможет уместиться новый объект, не нужно. Однако иногда бывает необходимо, чтобы на стадии инициализации объекта выполнялись какие-то дополнительные задачи, например, инициализация хранимых объектом данных. Для осуществления подобного применяется функция-конструктор. Все объекты имеют конструктор по умолчанию, который представляет собой не принимающий параметров метод с таким же именем, как у самого класса. Определение класса может также включать несколько методов-конструкторов, принимающих параметры и отличных от конструктора по умолчанию. Такие конструкторы позволяют создавать экземпляр объекта различными способами, например, предоставляя начальные значения для хранимых в объекте данных. В языке С# конструкторы вызываются с использованием ключевого слова new. Например, создать экземпляр объекта CupOfCoffee с применением конструктора по умолчанию можно было бы следующим образом: CupOfCoffee myCup = new CupOfCoffee (); Экземпляры объектов могут также создаваться и с помощью конструкторов не по умолчанию. Например, у класса CupOf Coffee мог бы существовать конструктор не по умолчанию, принимающий параметр для установки во время создания экземпляра типа кофейных зерен: CupOfCoffee myCup = new CupOfCoffee("Blue Mountain"); Конструкторы, подобно полям, свойствам и методам, могут быть общедоступными или приватными. Код, являющийся внешним по отношению к классу, не может создавать экземпляр объекта с помощью приватного конструктора; он должен обязательно использовать общедоступный конструктор. Это позволяет, например, заставить пользователей классов применять конструктор не по умолчанию (сделав конструктор по умолчанию приватным). Некоторые классы не имеют общедоступных конструкторов, что делает невозможным создание их экземпляров внешним кодом (такие классы называются не создаваемыми). Однако, как вскоре будет показано, это не означает их бесполезность. Деструкторы Деструкторы используются в .NET Framework для выполнения очистки после удаления объектов. В целом предоставлять код для метода-деструктора не требуется; это делается автоматически. Однако можно предоставлять специфические инструкции, если необходимо, чтобы перед удалением экземпляра объекта выполнялись какие-нибудь важные операции.
210 Часть I. Язык С# Когда переменная выходит за рамки области видимости, например, она может быть и не доступной из кода, но при этом все равно по-прежнему существовать где-то в памяти. Только после выполнения исполняющей средой .NET ее операции по сборке мусора экземпляр уничтожается полностью. Не стоит рассчитывать па освобождение деструктором ресурсов, которые используются экземпляром объекта, поскольку это может произойти спустя много времени после того, как объект перестанет быть нужным. Если данные ресурсы являются критичными, это чревато появлением проблем. Однако существует одно решение, которое рассматривается позже в разделе "Освобождаемые объекты ". Статические члены классов и члены экземпляров классов Помимо членов, наподобие свойств, методов и полей, принадлежащих конкретным экземплярам объектов, могут также создаваться и статические (еще называемые совместно используемыми, особенно в Visual Basic) члены, которые тоже могут представлять собой методы, свойства и поля. Статические члены используются совместно разными экземплярами класса, из-за чего могут считаться глобальными для объектов конкретного класса. Статические свойства и поля позволяют получать доступ к данным, которые не зависят ни от каких экземпляров объектов, а статические методы — выполнять команды, связанные с типом класса, но не привязанные ни к каким конкретным экземплярам объектов. При использовании статических членов, на самом деле, даже не нужно создавать экземпляр объекта. Например, методы Console. WriteLine () и Convert. ToString (), которые уже демонстрировались ранее в книге, являются статическими. Создавать экземпляры классов Console и Convert для них ни в коем случае не нужно (на самом деле при попытке сделать это, вы обнаружите, что это вообще невозможно, поскольку конструкторы этих классов не являются общедоступными, как описывалось ранее). Существует еще много подобных ситуаций, в которых статические свойства и методы можно применять с пользой. Например, статическое свойство можно использовать для отслеживания количества созданных экземпляров класса. В UML-синтаксисе статические члены классов изображаются с подчеркиванием, как показано на рис. 8.5. MyClass + lnstanceProperty : int +StaticProperty : int -HnstanceMethodQ : void +StaticMethod(): void Рис. 8.5. Статические члены в UML-представлении Статические конструкторы В случае применения статических членов в классе может возникать желание инициализировать их заранее. Конечно, можно предоставлять статический член с начальным значением в виде части объявления, но иногда требуется, чтобы осуществлялась более сложная инициализация или, возможно, выполнялись какие-то другие операции перед присваиванием значений или переходом к выполнению статических методов.
Глава 8. Введение в объектно-ориентированное программирование 211 Для выполнения подобных задач в процессе инициализации предусмотрены статические конструкторы. У класса может быть только один статический конструктор, у которого не должно быть никаких модификаторов доступа и не может быть никаких параметров. Статический конструктор никогда не может вызываться напрямую; вместо этого он может вызываться в одном из следующих случаев: □ при создании экземпляра того класса, в котором содержится данный статический конструктор; □ при получении доступа к какому-то статическому члену класса, в котором содержится данный статический конструктор. В обоих этих случаях сначала вызывается статический конструктор и только потом создается экземпляр класса или получается доступ к статическим членам. Сколько бы экземпляров класса не создавалось, его статический конструктор будет вызываться только один раз. Чтобы отличать статические конструкторы от тех, что описывались ранее в этой главе, все нестатические конструкторы еще также называют конструкторами экземпляров. Статические классы Довольно часто может возникать желание использовать классы, имеющие только статические члены, которые не могут применяться для создания экземпляров объектов (вроде класса Console). Кратчайшим путем для этого, вместо преобразования всех конструкторов класса в приватные, является применение статического класса. Статический класс может содержать только статические члены и не может иметь конструкторов экземпляра, поскольку по своей природе не допускает создания экземпляров. Статические классы могут, однако, обладать статическим конструктором, как описывалось в предыдущем разделе. Тем, кто является полным новичком в ООП, не помешает передохнуть перед изучением остального материала этой главы. Очень важно полностью разобраться со всеми базовыми понятиями и только затем приступать к рассмотрению более сложных аспектов. Приемы объектно-ориентированного программирования Теперь, когда изучены основы и известно, что собой представляют объекты и как они работают, пришла пора перейти к рассмотрению других функциональных возможностей объектов. В настоящем разделе речь пойдет о следующем: □ интерфейсы; □ наследование; □ полиморфизм; □ отношения между объектами; □ перегрузка операций; □ события; □ ссылочные типы и типы-значения.
212 Часть I. Язык С# Интерфейсы Интерфейс (interface) — это коллекция общедоступных методов и свойств, которые объединяются вместе для инкапсуляции конкретной функциональности. После определения интерфейса его можно реализовать в классе. Это означает, что в таком случае класс будет поддерживать все свойства и члены, специфицируемые данным интерфейсом. Важно обратить внимание на то, что интерфейсы не могут существовать сами по себе. Создавать "экземпляр" интерфейса, как это можно делать с классом, нельзя. Помимо этого, интерфейсы не могут содержать никакого кода, реализующего их члены, они могут лишь определять их, а реализоваться те должны непосредственно в классах, которые реализуют данный интерфейс. В приведенном ранее примере класса CupOfCof fee многие из свойств и методов более общего назначения, вроде AddSugar (), Milk, Sugar и Instant, можно было бы сгруппировать вместе в интерфейс, назвать его, скажем, IHotDrink (Горячий напиток) (имена интерфейсов обычно сопровождаются префиксом в виде заглавной буквы I), и применять для других объектов, например, объектов класса CupOf Tea (Чашка чая). Это позволило бы работать со всеми этими объектами похожим образом и при этом все равно разрешить им иметь свои собственные свойства (например, объектам CupOfCof fee— свойство BeanType (Сорт кофейных зерен), а объектам CupOfTea — свойство Leaf Type (Сорт чайных листьев)). Интерфейсы, реализуемые для объектов, в UML изображаются с использованием синтаксиса в форме окружности с линией. На рис. 8.6 члены интерфейса IHotDrink вынесены в отдельный прямоугольник и перечислены с использованием подобного классам синтаксиса. «Interface» IHotDrink -Hnstant: bool +Milk : bool +Sugar: byte ♦Description : string +AddSugar(in amount: byte) : byte CupOfCoffee +BeanType : string CupOfTea +LeafType : string О IHotDrink О IHotDrink Рис. 8.6. Представление интерфейсов в UML Один класс может поддерживать несколько интерфейсов, а несколько классов могут поддерживать один и тот же интерфейс. Следовательно, интерфейсы позволяют упрощать жизнь для пользователей и других разработчиков. Например, предположим, что есть некий код, в котором используется объект с определенным интерфейсом. Если не использовать другие свойства и методы этого объекта, один объект можно будет легко заменять другим (приводившийся ранее код, в котором использовался интерфейс IHotDrink, например, мог работать как с экземплярами CupOfCoffee, так и с экземплярами CupOfTea). Кроме того, сам разработчик данного объекта может выпустить его обновленную версию, и при условии, что она поддерживает находящийся в
Глава 8. Введение в объектно-ориентированное программирование 213 эксплуатации интерфейс, другой разработчик сможет легко применить новую версию в своем коде. После публикации интерфейса, т.е. предоставления к нему доступа другим разработчикам или конечным пользователям, рекомендуется не изменять его. Можно представить, что интерфейс является своего рода контрактом между создателями класса и его потребителями, в котором создатели, по сути, заявляют, что каждый класс, поддерживающий интерфейс X, будет поддерживать такие-то методы и свойства. Если интерфейс позже изменится, например, в результате модернизации лежащего в основе кода, это может привести к тому, что его потребители больше не смогут работать с ним корректным образом или вообще утратят возможность запускать его. Поэтому вместо этого рекомендуется создавать новый интерфейс, расширяющий возможности старого и имеющий, например, такой номер версии, как Х2. Такой подход уже стал стандартным, поэтому интерфейсы с пронумерованными версиями встречаются довольно часто. Освобождаемые объекты Одним из заслуживающих особого внимания интерфейсов является интерфейс I Disposable. Объект, поддерживающий этот интерфейс, должен обязательно реализовать метод Dispose (), т.е. предоставлять код этого метода. Dispose () может вызываться тогда, когда объект уже больше не нужен (например, непосредственно перед его выходом за пределы области видимости), и должен использоваться для освобождения любых критических ресурсов, которые в противном случае могут оставаться занятыми до тех пор, пока не будет вызван метод деструктора во время сборки мусора. Это обеспечивает больший контроль над ресурсами, которыми пользуются объекты. Язык С# позволяет применять структуру, делающую применение этого метода очень эффективным. Ключевое слово using дает возможность инициализировать использующий критические ресурсы объект в таком блоке кода, где метод Dispose () вызывается при достижении конца автоматически: <имя_класса> <имя_переменной> = new <имя_класса> (); using (<имя_переменной>) { } В качестве альтернативного варианта, экземпляр объекта <имя_переменной> можно также создавать и в виде части оператора using: using ( <имя_класса> <имя_переменной> = new <имя_класса> () ) { } И в том, и в другом случае переменная <имя_переменной> будет использоваться внутри блока using и автоматически удаляться в его конце (т.е. по завершении выполнения содержащегося внутри этого блока кода будет автоматически вызываться метод Dispose ()). Наследование Наследование (inheritance) является одним из самых важных механизмов в ООП. Любой класс может наследоваться от другого класса, а это значит, что он будет иметь все те же члены, что и класс, от которого он унаследован. В терминологии ООП класс,
214 Часть I. Язык С# от которого наследуется другой класс (или, другими словами, создается производный класс) называется родительским или базовым классом. Важно обратить внимание на то, что в С# напрямую наследоваться классы могут только от одного базового класса, хотя у базового класса может быть свой собственный базовый класс и т.д. Механизм наследования позволяет расширять или создавать специфические классы от одного более общего базового класса. Например, возьмем класс, представляющий животное с фермы. Этот класс мог бы называться Animal (Животное) и обладать методами вроде EatFood () (Кормить) или Breed () (Разводить). От него можно было бы создать производный класс по имени Cow (Корова), который бы поддерживал все те же самые методы, но при этом также имел и свои собственные, например, Моо () (Мычать) и SupplyMilk () (Давать молоко), а также еще один производный класс по имени Chicken (Курица) с методами Cluck () (Кудахтать) и LayEgg () (Снести яйцо). В UML наследование изображается с помощью стрелок, как показано на рис. 8.7. Animal +EatFood() +Breed() zx— i ; ; i Chicken Cow +Cluck() +Moo() +LayEgg() +SupplyMilk() Рис. 8.7. Представление отношений наследования в UML На рис. 8.7 возвращаемые типы членов для простоты не показаны. При применении наследования от базового класса важным становится вопрос о доступности членов. Приватные члены базового класса не являются доступными для производного класса, а общедоступные — являются. Однако общедоступные члены доступны как для производного класса, так и для внешнего кода. Поэтому если бы использовать можно было только два таких уровня доступности, создание члена, доступного как для базового, так и для производного класса, но не для внешнего кода, оказалось бы невозможным. Для исключения вероятности возникновения подобной проблемы существует еще и третий уровень доступности — protected, при котором доступ к члену имеют только производные классы. Что касается внешнего кода, то для него член с уровнем доступности protected идентичен члену с уровнем private. Помимо уровня защиты для члена еще также может определяться и поведение, связанное с наследованием. Члены базового класса могут быть виртуальными (virtual), а это означает то, что они могут переопределяться в классе, который их наследует. Другими словами, в производном классе для членов может предоставляться альтернативная реализация. Эта альтернативная реализация не подразумевает удаление ис-
Глава 8. Введение в объектно-ориентированное программирование 215 ходной реализации, которая все равно остается доступной в рамках классах, но от исходного кода она ее действительно закрывает. Если никакой альтернативной реализации не предоставлено, тогда любой внешний код, получающий доступ к члену через производный класс, автоматически использует реализацию этого члена, которая содержится в базовом классе. Виртуальные члены не могут быть приватными, поскольку это приводит к парадоксу: нельзя говорить, что член может переопределяться в производном классе и одновременно утверждать, что он не должен быть доступен в производном классе. В примере с классом Animal можно было бы сделать виртуальным метод Eat Food () и предоставить для него новую реализацию в каком-то производном классе, например, в Cow, как показано на рис. 8.8. На этом рисунке метод EatFood () отображается и в классе Animal, и в классе Cow; это указывает на то, что у каждого из классов имеется своя собственная реализация данного метода. I Chicken +Cluck() +LayEgg() Animal +EatFood() +Breed() L \ | Cow +Moo() +SupplyMilk() +EatFood() Рис. 8.8. Представление виртуального метода в UML Базовые классы могут также определяться как абстрактные (abstract). Создавать экземпляр абстрактного класса напрямую нельзя; использовать такой класс можно только путем выполнения от него наследования. У абстрактных классов могут быть абстрактные члены, которые не могут иметь никакой реализации в базовом классе и для которых реализация должна предоставляться в производных классах. Например, если бы класс Animal был абстрактным, тогда в UML-представлении он выглядел бы так, как показано на рис. 8.9. Имена абстрактных классов в UML изображаются с курсивным начертанием (или с использованием пунктирной линии для содержащего их прямоугольника). На рис. 8.9 методы EatFood () и Breed () отображаются в прямоугольниках абстрактных классов Chicken и Cow; это указывает на то, что они являются либо абстрактными (и тогда подлежат переопределению в других производных классах), либо виртуальными (т.е. уже были переопределены в самих классах Chicken и Cow). Разумеется, абстрактные базовые классы могут предоставлять реализацию для членов, что встречается очень часто. Невозможность создания экземпляра абстрактного класса вовсе не означает, что в нем нельзя инкапсулировать функциональные возможности.
216 Часть I. Язык С# Animal +EatFood() +Breed() 7Y Chicken +Cluck() +LayEgg() +EatFood() +Breed() Cow +Moo() +SupplyMilk() +EatFood() +Breed() Рис. 8.9. Представление абстрактных классов в UML И, наконец, еще классы могут быть герметизированными (sealed). Такие классы не могут выступать в роли базового класса и, следовательно, не могут иметь и никаких производных классов. В языке С# предусмотрен один общий базовый класс для всех объектов, имеющий имя object (которое является псевдонимом класса System.Object из .NET Framework). Этот класс более подробно рассматривается в главе 9. Интерфейсы, о которых рассказывалось ранее в этой главе, тоже могут наследоваться от других интерфейсов. В отличие от классов, однако, они могут наследоваться от нескольких базовых интерфейсов (тем же образом, каким классы могут поддерживать несколько интерфейсов). Полиморфизм Одним из результатов наследования является наличие в классах, унаследованных от базового, некоторых совпадений с ним среди предоставляемых ими методов и свойств. Благодаря этому зачастую оказывается возможным применять для объектов, которые создаются от классов с общим базовым типом, идентичный синтаксис. Например, при наличии у класса Animal метода по имени EatFood () синтаксис для вызова этого метода из производных классов Cow и Chicken будет выглядеть почти одинаково: Cow myCow = new Cow() ; Chicken myChicken = new Chicken (); myCow.EatFood(); myChicken.EatFood(); Полиморфизм (polymorphism) позволяет двинуться еще дальше, а именно — присваивать переменную базового типа переменной одного из производных типов, как показано ниже: Animal myAnimal = myCow; Никакого приведения выполнять не требуется. Далее можно просто вызывать методы базового класса через эту переменную: myAnimal.EatFood() ;
Глава 8. Введение в объектно-ориентированное программирование 217 Данная строка кода приведет к вызову реализации метода EatFood (), содержащейся в производном классе. Важно обратить внимание на то, что вызывать подобным образом методы, определенные в производном классе, нельзя. То есть следующий код работать не будет: myAnimal.Moo (); Однако приводить неременную типа базового класса в переменную типа производного класса и вызывать метод производного класса показанным ниже образом можно: Cow myNewCow = (Cow)myAnimal; myNewCow.Moo(); Эта операция приведения будет приводить к выдаче исключения в случае, если тип исходной переменной не будет совпадать ни с типом Cow, ни с типом производного от него класса. Для определения типа объекта существуют свои приемы, о которых речь пойдет в следующей главе. Полиморфизм является чрезвычайно полезной технологией для решения задач с использованием минимального количества кода в разных объектах, происходящих от одного единственного класса. Следует отметить, что полиморфизм может применяться не только в отношении классов, имеющих одинаковый родительский класс. Он точно так же может применяться и, скажем, в отношении дочерних и "внучатых" классов, главное, чтобы в их иерархии наследования присутствовал какой-то общий класс. Напоследок важно обратить внимание и запомнить, что в С# все классы наследуются от базового класса object, который является корневым в их иерархиях наследования. Благодаря этому все объекты могут считаться экземплярами класса object. Именно это и позволяет методу Console. WriteLine () обрабатывать практически бесконечное количество комбинаций параметров при построении строк. Каждый параметр после первого воспринимается как экземпляр object, что позволяет выводить выходные данные любого объекта на экран. Для этого вызывается метод ToString () (являющийся членом класса object). Этот метод можно переопределять и предоставлять для него более подходящую реализацию, а можно и просто воспользоваться реализацией, предлагаемой по умолчанию, в которой он возвращает имя класса (квалифицированное пространствами имен, в которых он присутствует). Полиморфизм интерфейсов Ранее уже говорилось о том, что для группирования вместе связанных методов и свойств могут применяться интерфейсы. Создавать экземпляры интерфейсов таким же образом, как и экземпляры объектов, нельзя, но зато можно создавать переменную типа интерфейса и затем использовать ее для получения доступа к методам и свойствам, предоставляемым этим интерфейсом в объектах, которые его поддерживают. Например, предположим, что вместо базового класса Animal метод EatFood () помещается в интерфейс по имени IConsume. Этот интерфейс могут поддерживать оба класса— Cow и Chicken, — но только в таком случае каждый из них должен обязательно иметь свою реализацию метода EatFood () (поскольку интерфейсы не содержат реализаций). После этого к этому методу станет можно получать доступ с помощью примерно такого кода: Cow myCow = new Cow () ; Chicken myChicken = new Chicken(); IConsume consumelnterface; consumelnterface = myCow; consumelnterface.EatFood(); consumelnterface = myChicken; consumelnterface.EatFood();
218 Часть I. Язык С# Такой подход предоставляет простой способ для вызова множества объектов одинаковым образом и избавляет от зависимости от общего базового класса. В этом коде вызов consumelnterface .EatFood () будет приводить к вызову метода EatFoodO либо класса Cow, либо класса Chicken, в зависимости от того, какой экземпляр будет присвоен переменной типа интерфейса. Здесь важно обратить внимание на то, что производные классы наследуют интерфейсы, поддерживаемые их базовыми классами. В предыдущем примере интерфейс I Consume может поддерживаться как классом Animal, так и обоими классами Cow и Chicken. Запомните, что классы с общим базовым классом вовсе необязательно имеют общие интерфейсы и наоборот. Отношения между объектами Наследование является примером простых отношений между объектами, при которых базовый класс полностью отражен производным классом и при которых производный класс может также иметь доступ к внутренним деталям своего базового класса (посредством защищенных членов). Однако бывают и другие ситуации, в которых отношения между объектами начинают играть более важную роль. В настоящем разделе дается краткий обзор следующих отношений. □ Включение. Один класс содержит другой. Эти отношения похожи на отношения наследования, но позволяют классу-контейнеру управлять доступом к членам содержащегося внутри него класса и даже выполнять дополнительную обработку перед использованием этих членов. □ Коллекции. Один класс выступает в роли контейнера для нескольких экземпляров другого класса. Эти отношения напоминают те, что образуются при создании массивов объектов, но предусматривают дополнительные функциональные возможности, наподобие индексации, поиска, изменения размера и т.д. Включение Отношение включения легко достигается использованием для хранения экземпляра объекта поля-члена. Это поле может быть общедоступным, в случае чего пользователи объекта-контейнера будут иметь доступ к предоставляемым им методам и свойствам во многом подобно тому, как это происходит при наследовании. Однако доступа к внутренним деталям класса через производный класс, который возможен в случае наследования, в таком случае не будет. В качестве альтернативного варианта содержащийся внутри объект-член может делаться приватным членом. В таком случае ни один из его членов не будет доступен напрямую пользователям, даже если те являются общедоступными. Вместо этого доступ к этим членам может предоставляться с помощью членов класса-контейнера. Такой подход позволит иметь полный контроль над тем, какие члены содержащегося внутри класса должны предоставляться, если вообще должны, а также производить дополнительную обработку в членах класса-контейнера перед получением доступа членам содержащегося внутри класса. Например, класс Cow мог бы содержать класс Udder (Вымя) с общедоступным методом Milk () (Доить). Объект Cow мог бы вызывать этот метод по мере необходимости, например, в виде части своего метода SupplyMilk (), но пользователям объекта Cow () такие детали при этом были бы не видны (или не важны). Содержащиеся внутри других классы в UML могут изображаться с помощью ассоциативных линий. В простом случае концы этих линий обозначаются единицей A), указывая тем самым на то, что речь идет об отношениях типа один к одному (т.е., например, что один экземпляр Cow будет содержать один экземпляр Udder).
Глава 8. Введение в объектно-ориентированное программирование 219 Для ясности экземпляр класса Udder может также быть изображен в виде приватного поля класса Cow, как показано на рис. 8.10. 1 Udder + Milk() Cow -containedUdder: Udder + Моо() +SupplyMilk() 1 | Рис. 8.10. Представление отношения включения в UML Коллекции В главе 5 говорилось о том, что для хранения нескольких переменных одинакового типа можно использовать массивы. То же самое можно делать и для объектов (напоминаем, что типы переменных, которые приводились в этой книге, на самом деле тоже являлись объектами, так что ничего особо удивительного в этом быть не должно). Ниже приведен пример: Animal[] animals = new Animal[5]; Коллекция представляет собой, по сути, массив, но с дополнительными возможностями. Коллекции реализуются в виде классов во многом точно так же, как и другие объекты. Их имена часто совпадают с именами объектов, которые они хранят, но только во множественном числе: например, класс с именем Animals может содержать коллекцию объектов Animal. Главное отличие от массивов состоит в том, что коллекции обычно реализуют дополнительные функции, вроде методов Add () и Remove () для добавления и удаления элементов из коллекции. У них также обычно имеется свойство Item, которое возвращает объект по его индексу. Довольно часто это свойство реализуется так, чтобы оно предусматривало более сложный доступ. Например, класс Animals мог бы быть спроектирован так, чтобы к каждому конкретному объекту Animal доступ можно было получать по его имени. В UML подобные отношения изображаются так, как показано на рис. 8.11. \J.. Animal Animals 1 Рис. 8.11. Представление отношения коллекции в UML
220 Часть I. Язык С# Члены на этом рисунке не показаны, поскольку иллюстрируются отношения. Числа на концах соединительных линий указывают, что один объект Animals будет содержать ноль или более объектов Animal. О коллекциях более подробно будет рассказываться в главе 11. Перегрузка операций Ранее в настоящей книге уже показывалось, как операции могут применяться для манипулирования простыми типами переменных. Встречаются ситуации, в которых бывает логично использовать операции с объектами, создаваемыми от собственных классов. Подобное возможно потому, что классы могут содержать инструкции о том, как обращаться с операциями. Например, в класс Animal можно было бы добавить новое свойство по имени Weight (Вес). И тогда можно было бы сравнивать вес животных с помощью такого кода: if (cowA.Weight > cowB.Weight) { } За счет перегрузки операций, однако, можно также было бы предоставить логику, благодаря которой свойство Weight использовалось бы в коде неявно, т.е. написать такой код: if (cowA > cowB) { } Здесь операция "больше чем" (>) была перегружена. Перегруженной называется такая операция, для который был специально написан код для выполнения требуемого действия; этот код добавляется в определение одного из классов, для которого она должна выполняться. В предыдущем примере используются два объекта Cow, поэтому определение перегрузки операции содержится в классе Cow. Операции могут также перегружаться и для получения возможности работать с разными классами одинаковым образом, что тогда требует добавления кода в одно (или даже оба) из определений классов. Важно обратить внимание, что перегружать подобным образом в языке С# можно только существующие операции; создавать новые нельзя. Однако реализации можно предоставлять как для унарных, так и для бинарных версий операции вроде +. О том, как это делать, будет рассказываться в главе 13. События Объекты на одном из этапов обработки могут генерировать (и потреблять) события (event). События являются важными вхождениями, в связи с которыми в других частях кода могут выполняться определенные действия, подобные, но более мощные, чем генерация исключений. Например, может быть необходимо, чтобы при добавлении объекта Animal в коллекцию Animals выполнялся определенный код, не являющийся ни частью класса Animals, ни частью того кода, который вызывает метод Add (). Чтобы добиться такого поведения, потребуется добавить в код обработчик событий, представляющий собой функцию особого вида, которая вызывается при возникновении события. Кроме того, потребуется также сконфигурировать этот обработчик, чтобы он ожидал поступления именно интересующего события.
Глава 8. Введение в объектно-ориентированное программирование 221 С помощью событий можно создавать управляемые событиями приложения, которые являются гораздо более полезными, чем может показаться поначалу. Достаточно вспомнить о том, что все Windows-приложения, например, полностью зависят от событий. Каждый щелчок на кнопке или перетаскивание ползунка полосы прокрутки, который выполняет пользователь, осуществляется через обработку событий, по мере поступления событий от мыши или клавиатуры. Как именно это все происходит в Windows-приложениях, еще будет показано чуть позже в этой главе, а вообще события детально рассматриваются в главе 13. Ссылочные типы и типы-значения Данные в С# сохраняются в переменной одним из двух способов, который зависит от типа переменной. Этот тип относится к одной из двух категорий: ссылка или значение. Отличия описаны ниже. □ Типы-значения хранят себя и свое содержимое в одном месте в памяти. □ Ссылочные типы хранят ссылку на другое место в памяти (называемое кучей), в котором уже и непосредственно хранится их содержимое. На самом деле чрезмерно беспокоиться об этом при использовании С# не нужно. Пока что в этой книге переменные string (которые относятся к ссылочным типам) и другие простые переменные (большинство из которых относится к типам-значениям, как, например, переменные int) применялись практически одинаково. Одно из главных отличий между типами-значениями и ссылочными типами состоит в том, что типы-значения всегда содержат значения, в то время как ссылочные типы могут быть нулевыми (null), т.е. не содержать вообще никакого значения. Существует, однако, возможность создавать тип-значение, ведущий себя в этом отношении подобно ссылочному типу (содержать null), путем использования допускающих null типов, которые являются разновидностью обобщений (generics). Обобщения представляют собой усовершенствованную технологию, и более подробно рассматриваются в главе 12. Единственными простыми типами, которые представляют собой ссылочные типы, являются string и object, хотя массивы неявно тоже являются ссылочными типами. Каждый создаваемый класс будет представлять собой ссылочный тип, поэтому здесь основной акцент будет делаться именно на них. Структуры Главное отличие между типами-структурами и классами состоит в том, что типы- структуры представляют собой типы-значения. То, что структуры и классы похожи, наверняка уже приходило вам в голову, особенно тогда, когда в главе 6 было показано, как в типах-структурах можно использовать функции. Более подробно об этом пойдет речь в главе 9. Объектно-ориентированное программирование для Windows-приложений В главе 2 демонстрировался пример создания простого Windows-приложения на языке С#. Приложения Windows очень сильно зависят от приемов ООП, и поэтому в настоящем разделе именно на этом и делается акцент для иллюстрации некоторых освещавшихся в настоящей главе моментов. В частности, в следующем практическом занятии предлагается еще один простой пример.
222 Часть I. Язык С# Практическое занятие Объекты В деЙСТВИИ 1. Создайте новое Windows-приложение по имени Ch08Ex01 и сохраните его в каталоге C:\BegVCSharp\Chapter08. 2. Добавьте в него новый элемент управления Button из окна Toolbox и поместите его в центр формы Forml, как показано на рис. 8.12. 3. Дважды щелкните на элементе Button, чтобы добавить код для обработки щелчка. Измените код, который появится, следующим образом: private void buttonl_Click(object sender, System.EventArgs e) { ((Button)sender).Text = "Clicked!"; // Выполнен щелчок Button newButton = new Button (); newButton.Text = "New Button!"; newButton.Click += new EventHandler(newButton_Click); Controls.Add(newButton); } private void newButton_Click(object sender, System.EventArgs e) { ((Button)sender).Text = "Clicked!!"; } } 4. Запустите приложение. После этого на экране должна появиться форма, выглядящая так, как показано на рис. 8.13. *> Forml с с G су buttonl О р D D Рис. 8.12. Добавление кнопки Рис. 8.13. Приложения Ch08Ex01 в работе 5. Щелкните на кнопке с надписью buttonl. После этого изображение на экране должно измениться (рис. 8.14). 6. Щелкните на кнопке с надписью New Button! (Новая кнопка). После этого изображение на экране должно измениться так, как показано на рис. 8.15. Описание полученных результатов Добавление всего лишь нескольких строк кода позволило создать Windows-приложение, которое кое-что делает и демонстрирует применение некоторых приемов ООП в С#. Выражение "объектом является все, что угодно" в случае Windows-приложений — более чем правда.
Глава 8. Введение в объектно-ориентированное программирование 223 ^r Fonnl NevButtorV I *^*сма | Рис. 8.14. Форма приложения Ch08Ex01 Рис. 8.15. Форма приложения Ch08Ex01 после щелчка на кнопке button 1 после щелчка на кнопке New Button! Начиная с формы, которая всем заправляет, и заканчивая элементами управления, которые присутствуют на этой форме, все время требуется применять приемы ООП. В данном упражнении были задействованы несколько из понятий, которые рассматривались ранее в этой главе, для того, чтобы показать, как все они сочетаются вместе. Первым делом на форму Forml приложения была добавлена новая кнопка, представляющая собой объект Button. Далее путем выполнения двойного щелчка на кнопке был добавлен обработчик событий для перехвата генерируемого объектом Button события Click. Этот обработчик был добавлен в код объекта Form, который инкапсулирует приложение, в виде приватного метода: private void buttonl_Click(object sender, System.EventArgs e) { } В этом коде ключевое слово private выступает в роли спецификатора. Пока что об этом не стоит особо беспокоиться, потому что в следующей главе код С#, требуемый для применения приемов ООП, описанных в настоящей главе, будет объясняться более подробно. В первой добавленной строке коде изменяется текст на кнопке, на которой выполняется щелчок, что подразумевает применение полиморфизма, о котором рассказывалось ранее в главе. Объект Button, представляющий эту кнопку, отправляется обработчику событий в виде параметра object, который приводится к типу Button (что возможно, поскольку объект Button унаследован от System.Object, т.е. класса .NET, для которого object является псевдонимом). Затем вносится изменение в свойство Text объекта для изменения отображаемого текста: ((Button)sender).Text = "Clicked!"; Далее создается новый объект Button с использованием ключевого слова new (обратите внимание, что пространства имен специально создаются в этом проекте так, чтобы они позволяли применять подобный простой синтаксис; в противном случае для создания этого объекта нужно было бы использовать полностью квалифицированное имя— System.Windows .Forms .Button): Button newButton = new Button () ; newButton.Text = "New Button!"; ■ %* Forml
224 Часть I. Язык С# В другом месте в коде после этого добавляется соответствующий новый обработчик событий, который используется для реагирования на событие Click, генерируемое новой кнопкой: private void newButton_Click (object sender, System.EventArgs e) { ((Button)sender).Text = "Clicked!!"; } Затем этот обработчик регистрируется в качестве слушателя события Click с использованием синтаксиса перегрузки операций. С помощью конструктора создается новый объект EventHandler с именем новой функции-обработчика событий: newButton.Click += new EventHandler(newButton_Click); Напоследок используется свойство Controls. Это свойство является объектом, представляющим коллекцию всех имеющихся на форме элементов управления, метод Add () которого применяется для добавления в форму новой кнопки: Controls.Add(newButton); Свойство Controls иллюстрирует то, что свойства не обязательно должны относиться к простым типам вроде строк или целых чисел, и что они могут представлять собой объекты любого рода. В этом коротком примере были использованы почти все из описанных в настоящей главе приемов. Это позволяет сделать вывод о том, что приемы объектно-ориентированного программирования вовсе не всегда являются сложными — чтобы понять это, достаточно просто взглянуть на них под другим углом. Резюме В этой главе было приведено полное описание приемов объектно-ориентированного программирования. За основу был взят контекст программирования на С#, но отразилось это по большей части только на иллюстрировавшихся примерах. То есть большая часть изложенного в настоящей главе материала подходит для применения приемов ООП на любом языке. Сначала были рассмотрены основные вопросы вроде того, что подразумевается под термином "объект", и как объект может являться экземпляром класса. Далее рассказывалось о том, что объекты могут иметь разные члены, вроде полей, свойств и методов, что эти члены могут иметь ограниченную доступность, и чем общедоступные члены отличаются от приватных. Сразу же после этого было показано, что члены еще могут быть защищенными, а также виртуальными и абстрактными (и что абстрактные методы допускается иметь только абстрактным классам). Было показано и то, чем статические (совместно используемые) члены отличаются от членов экземпляра и почему в некоторых случаях может быть выгоднее использовать именно статические классы. Затем был вкратце рассмотрен жизненный цикл объекта вместе с конструкторами, которые могут применяться на этапе его создания, и деструкторами, которые вступают в действие на этапе его удаления. Чуть позже, после изучения возможности группирования членов в интерфейсах, была описана и более сложная методика уничтожения объектов с участием освобождаемых объектов, поддерживающих интерфейс IDisposable. Остальная часть главы была в основном посвящена перечислению средств и приемов ООП, многие из которых будут более подробно рассматриваться в последующих главах. В частности, здесь рассказывалось о наследовании, позволяющем наследовать
Глава 8. Введение в объектно-ориентированное программирование 225 классы от базовых классов, о двух разновидностях полиморфизма, позволяющих применять базовые классы и совместно используемые интерфейсы, и об отношениях, позволяющих помещать в объект один или более других объектов (за счет применения отношений включения и коллекций). Напоследок было показано, как применение методики перегрузки операций может упрощать синтаксис использования объектов и то, как объекты генерируют события. В заключительной части главы большая часть рассмотренных теоретических сведений была продемонстрирована на примере создания простого Windows-приложения. В следующей главе речь пойдет об определении классов в С#. Упражнения 1. Какие из перечисленных ниже уровней доступности действительно существуют в ООП? • friend • public • secure • private • protected • loose • wildcard 2. "Деструктор объекта нужно обязательно вызывать вручную, иначе память будет использоваться неэффективно". Верно это или нет? 3. Нужно ли создавать объект для вызова статического метода его класса? 4. Нарисуйте UML-диаграмму, подобную приведенным ранее в главе, для перечисленных ниже классов и интерфейса. • Абстрактный класс с именем HotDrink (Горячий напиток), имеющий такие методы, как Drink (Пить), AddMilk (Добавлять молоко) и AddSugar (Добавлять сахар), и свойства вроде Milk (Молоко) и Sugar (Сахар). • Интерфейс по имени ICup (Чашка), имеющий такие методы, как Refill (Наполнять снова) и Wash (Мыть), и свойства вроде Color (Цвет) и Volume (Объем). • Класс по имени CupOfCoffee (Чашка кофе), унаследованный от класса HotDrink (Горячий напиток), поддерживающий интерфейс ICup и обладающий таким дополнительным свойством, как ВеапТуре (Сорт кофейных зерен). • Класс по имени CupOfTea (Чашка чая), унаследованный от класса HotDrink, поддерживающий интерфейс ICup и обладающий таким дополнительным свойством, как Leaf Type (Сорт чайных листьев). 5. Напишите код для функции, которая будет в качестве параметра принимать один из двух возможных в предыдущем примере объектов типа Cup (CupOfCoffee или CupOfTea). Эта функция должна вызывать методы AddMilk, Drink и Wash для любого передаваемого ей объекта Сир.
Определение классов В главе 8 рассматривались теоретические аспекты объектно-ориентированного программирования (ООП). В этой главе предоставляется возможность применить теорию на практике и, в частности, увидеть то, как определяются классы в С#. Способы определения членов классов здесь не рассматриваются, потому что все основное внимание уделяется способам определения самих классов. Может показаться, что такой подход является несколько ограниченным, но не стоит волноваться — материла для изучения здесь более чем достаточно. Сначала рассматривается синтаксис, необходимый для определения базовых классов, ключевые слова, которые можно использовать для указания доступности класса и не только, а также способ, которым можно задавать наследование. Также рассказывается и об определениях интерфейсов, поскольку они во многом похожи на определения классов. В остальной части настоящей главы освещены другие родственные вопросы, имеющие отношение к определению классов в языке С#. □ Класс System.Object. □ Полезные средства, предоставляемые VS и VCE. □ Библиотеки классов. □ Сравнение интерфейсов и абстрактных классов. □ Типы-структуры. □ Копирование объектов. Определение классов в С# В языке С# для определения классов применяется ключевое слово class: class MyClass { // Члены класса. }
Глава 9. Определение классов 227 В этом коде определяется класс по имени MyClass. После определения класса его экземпляр можно создавать в любом месте проекта, где имеется доступ к этому определению. По умолчанию классы объявляются внутренними, что делает их доступными только для кода, который находится внутри текущего проекта. Объявлять их таковыми можно явно с использованием ключевого слова internal, как показано ниже (хотя делать это не обязательно): internal class MyClass { // Члены класса. } В качестве альтернативного варианта можно указывать, что класс является общедоступным и должен быть также доступен и в коде других проектов. Для этого нужно использовать ключевое слово public, как показано ниже: public class MyClass { // Члены класса. } Классы, объявляемые сами по себе, подобно этому, не могут быть ни приватными, ни защищенными, но классы, объявляемые членами других классов, могут делаться таковыми с помощью соответствующих модификаторов, как будет показано в следующей главе. Помимо этих двух ключевых слов, играющих роль модификаторов доступа, можно использовать либо ключевое слово abstract и тем самым делать класс абстрактным (т.е. не допускающим создания экземпляров, позволяющим только наследовать от него другие классы и способным иметь абстрактные члены), либо ключевое слово sealed и тем самым сделать класс герметизированным (не допускающим наследования от него никаких других классов). Эти два ключевых слова являются взаимно исключающими, т.е. указывать можно только какое-то одно из них. В частности, абстрактный класс объявляется следующим образом: public abstract class MyClass { // Члены класса, могут быть и абстрактными. } Здесь MyClass представляет собой общедоступный абстрактный класс, хотя внутренние абстрактные классы тоже допускается создавать. Герметизированные классы объявляются следующим образом: public sealed class MyClass { // Члены класса. } Подобно абстрактным, герметизированные классы могут быть как общедоступными, так и внутренними. Наследование тоже может задаваться в определении класса. Делается это добавлением после имени класса двоеточия со следующим за ним именем базового класса, как показано ниже: public class MyClass : MyBase { // Члены класса. }
228 Часть I. Язык С# В определениях классов разрешено указывать только один базовый класс и в случае наследования от абстрактного класса нужно обязательно реализовать все наследуемые абстрактные члены (если только производный класс тоже не является абстрактным). Компилятор не допускает, чтобы производный класс был более доступным, чем его базовый класс. Это означает, что внутренний класс может наследоваться от общедоступного класса, а вот общедоступный класс от внутреннего базового — нет. То есть следующий код является допустимым: public class MyBase // Члены класса. } internal class MyClass : MyBase { // Члены класса. } А приведенный ниже код скомпилировать не удастся: internal class MyBase { // Члены класса. } public class MyClass : MyBase { // Члены класса. } Если базовый класс не используется, класс наследуется только от базового класса System.Object (который в С# имеет псевдоним object). В конечном счете, класс System.Object является корневым в иерархии наследования абсолютно всех классов. Об этом фундаментальном классе более подробно рассказываться позже в главе. Помимо базовых классов подобным образом после двоеточия могут также указываться и поддерживаемые интерфейсы. Если специфицируется базовый класс, он должен указываться после двоеточия первым, а интерфейсы — уже за ним. Если же никакого базового класса не задано, тогда интерфейсы указываются сразу же после двоеточия. Для разделения имен базового класса (если таковой задан) и интерфейсов служат запятые. Например, в определение класса MyClass добавить интерфейс можно было бы следующим образом: public class MyClass : IMylnterface { // Члены класса. } Все члены интерфейса должны обязательно быть реализованы в каком-то поддерживающем этот интерфейс классе, хотя допускается предоставлять "пустую" реализацию (безо всякого функционального кода), если не требуется ничего делать с данным членом интерфейса, а также реализовать члены интерфейса в виде абстрактных членов абстрактных классов. Следующее объявление является недопустимым, поскольку базовый класс MyBase не является первым в списке наследования: public class MyClass : IMylnterface, MyBase { // Члены класса. }
Глава 9. Определение классов 229 Правильный способ указания базового класса и интерфейса выглядит так: public class MyClass : MyBase, IMylnterface /7 Члены класса. } He следует забывать о том, что интерфейсов может быть несколько, поэтому следующий код тоже допустим: public class MyClass : MyBase, IMylnterface, IMySecondlnterface { // Члены класса. } В табл. 9.1 перечислены все комбинации модификаторов доступа, которые разрешено применять в определениях классов. Таблица 9.1. Допустимые комбинации модификаторов доступа для определений классов Модификатор Значение Отсутствует или internal Класс, к которому может быть получен доступ только из текущего проекта public Класс, к которому может быть получен доступ из любого места abstract или Класс, к которому может быть получен доступ только из теку- mternai abstract щего проекта, и который не допускает создания экземпляров, т.е. позволяет создавать только производные классы public abstract Класс, к которому может быть получен доступ из любого места, и который не допускает создания экземпляров, т.е. позволяет создавать только производные классы sealed или internal sealed Класс, к которому может быть получен доступ только из текущего проекта, и который не допускает создания производных классов, т.е. допускает создание только экземпляров public sealed Класс, к которому может быть получен доступ из любого места, и который не допускает создания производных классов, т.е. допускает создание только экземпляров Определения интерфейсов Интерфейсы объявляются похожим на классы образом, но только с использованием не ключевого слова class, а ключевого слова interface: interface IMylnterface { // Члены интерфейса. } Ключевые слова public и internal, играющие роль модификаторов доступа, применяются точно таким же образом, и подобно классам интерфейсы по умолчанию объявляются внутренними. Чтобы сделать интерфейс общедоступным, нужно использовать ключевое слово public: public interface IMylnterface { // Члены интерфейса. }
230 Часть I. Язык С# Ключевые слова abstract и sealed являются недопустимыми, поскольку ни один из этих модификаторов не имеет смысла в контексте интерфейсов (они не содержат никакой реализации, из-за чего создавать их экземпляры напрямую нельзя, а для того чтобы приносить пользу, они должны обязательно допускать наследование). Наследование интерфейсов тоже задается похожим с классами образом. Главное отличие состоит лишь в том, что в их случае может использоваться несколько базовых интерфейсов, как показано ниже: public interface IMylnterface : IMyBaselnterfасе, IMyBaselnterface2 { // Члены интерфейса. } Интерфейсы не являются классами и, следовательно, не наследуются от System. Object. Тем не менее, члены System.Object доступны через переменную типа интерфейса, исключительно для удобства. Кроме того, как упоминалось ранее, создавать экземпляр интерфейса так же, как экземпляр класса, нельзя. В следующем практическом занятии приводится пример некоторых определений классов вместе с кодом, где они используются. практическое занятие) Определение классов 1. Создайте новое консольное приложение по имени Ch09Ex01 и сохраните его в каталоге С: \BegVCSharp\Chapter09. 2. Измените код в Program, cs следующим образом: namespace Ch09Ex01 { public abstract class MyBase internal class MyClass : MyBase public interface IMyBaselnterface internal interface IMyBaseInterface2 internal interface IMylnterface : IMyBaselnterface, IMyBaselnterface2 internal sealed class MyComplexClass : MyClass, IMylnterface class Program { static void Main(string[] args) { MyComplexClass myObj = new MyComplexClass () ; Console.WriteLine(myObj.ToString()); Console.ReadKey(); } }
Глава 9. Определение классов 231 3. Запустите приложение. На рис. 9.1 показан результат, который должен получиться. I f«e:///C:/BeflVCSh«rjVChaptefOWCh09EjriI/... Рис. 9.1. Приложение Ch 0 9Ех 01 в действии Описание полученных результатов Классы и интерфейсы, определенные в этом проекте, в целом образуют иерархию наследования, которая показана на рис. 9.2. System.Object T" «interface» IMyBaselnterface i «interface» IMyBaselnterface2 i i i «interface» IMylnterface 0---J MyBase / \ MyClass I \ MyComplexClass ZL Program -Main(in args : string[]) /-\ ... . . м \_j iMyintBndce Puc. 9.2. Иерархия наследования классов и интерфейсов в проекте Ch09Ex01 Здесь класс Program тоже включен, потому что он определяется подобно другим классам, хотя в состав основной иерархии и не входит. Метод Main (), который имеет этот класс, представляет собой точку входа приложения. Определения MyBase и IMyBaselnterface являются общедоступными, т.е. эти классы будут доступны и в других проектах. Все остальные классы и интерфейсы являются внутренними, т.е. будут доступны только в этом проекте. Код в Main () вызывает метод ToStringO объекта my Ob j, представляющего собой экземпляр класса MyComplexClass: MyComplexClass myObj = new MyComplexClass(); Console.WriteLine(myObj.ToString());
232 Часть I. Язык С# Этот метод является одним из унаследованных от System.Object (на диаграмме он не показан) и возвращает имя класса объекта в виде строки вместе с именами любых имеющих к нему отношение пространств имен. Данное приложение, возможно, ничего особенного и не делает, но немного позже в главе оно будет использоваться снова для демонстрации нескольких важных понятий и приемов. Класс System.Object Поскольку все классы унаследованы от System.Object, у всех них имеется доступ как к защищенным, так и к общедоступным членам этого класса, потому ознакомиться с ними совершенно не помешает. В табл. 9.2 перечислены основные методы System.Object. Таблица 9.2. Основные методы System.Object Метод Тип возвращаемого Виртуальный Статический Описание значения Object () Нет -Object () (также известный как Finalize ();подробнее о нем будет рассказываться в следующем разделе) Equals(object) Нет bool Да Нет Представляет собой конструктор для объектов типа System.Object. Автоматически вызывается конструкторами объектов производных типов Нет Представляет собой деструктор для объектов типа System.Object. Автоматически вызывается деструкторами объектов производных типов; не может вызываться вручную Нет Сравнивает тот объект, для которого он был вызван, с другим объектом, и если они равны, возвращает true. В реализации по умолчанию проверяет, ссылаются ли параметры объекта на тот же объект (поскольку объекты являются ссылочными типами). Может переопределяться, если необходимо сделать так, чтобы он сравнивал объекты каким-то другим образом, например, чтобы он сравнивал состояние двух объектов
Глава 9. Определение классов 233 Окончание табл. 9.2 Метод Тип возвращаемого Виртуальный Статический Описание значения Equals(object, object) bool Нет Да ReferenceEquals (object, object) ToStringO bool Нет string Да Да Нет MemberwiseClone () object Нет Нет GetType() GetHashCodeO System .Type int Нет Да Нет Нет Сравнивает два передаваемых ему объекта и проверяет, не являются ли они эквивалентными. Эта проверка осуществляется с использованием метода Equals(object). Если оба объекта оказываются нулевыми ссылками, возвращается true Сравнивает два передаваемых ему объекта и проверяет, не ссылаются ли они на один и тот же экземпляр Возвращает строку, соответствующую экземпляру объекта. По умолчанию таковой является квалифицированное имя типа класса, но это поведение можно переопределять и предоставлять реализацию, более подходящую для данного типа класса Копирует объект путем создания его нового экземпляра и копирования его членов. Копирование членов не приводит к созданию никаких новых экземпляров этих членов. Все члены ссылочного типа в новом объекте ссылаются на те же самые объекты, что и в исходном классе. Этот метод является защищенным, так что может использоваться только в рамках самого класса или его производных классов Возвращает тип объекта в виде Объекта System.Type Используется в качестве хеш-функции для объектов там, где это необходимо. Хеш-функцией называется такая функция, которая возвращает значение, указывающее на состояние объекта в сжатой форме
234 Часть I. Язык С# Эти методы являются базовыми методами, которые должны обязательно поддерживаться типами объектов в .NET Framework, хотя некоторые из них могут и никогда не использоваться (или применяться только в особых обстоятельствах, как метод GetHashCode, например). Метод Get Type () может быть полезен при внедрении полиморфизма, поскольку позволяет выполнять различные операции с объектами в зависимости от их типа, а не одну и ту же операцию для всех объектов, как это часто бывает. Например, при наличии функции, принимающей параметр типа object (это означает, что ей можно передавать практически все что угодно), можно было бы сделать так, чтобы в случае обнаружения объектов определенного типа выполнялись дополнительные задачи. В частности, с использованием комбинации из метода GetType и операции typeof (которая представляет собой С#-операцию, преобразующую имя класса в объект System.Type) можно было бы обеспечить выполнение операции сравнения, как показано ниже: if (myObj.GetType () == typeof(MyComplexClass)) { // myObj является экземпляром класса MyComplexClass. } Возвращаемый объект System. Type способен на гораздо большее, но здесь демонстрируется только это. Также полезно переопределять метод ToString, особенно в ситуациях, когда содержимое объекта можно легко представить с помощью одной понятной для человеческого глаза строки. Эти методы System.Object еще не раз будут встречаться в последующих главах, поэтому остальные детали о них можно будет узнать далее. Конструкторы и деструкторы При определении класса в С# определять ассоциируемые с ним конструкторы и деструкторы обычно не обязательно, поскольку, если таковые не предоставляются, компилятор добавляет их автоматически во время сборки кода. Однако если требуется, можно предоставлять свои собственные и с их помощью выполнять, соответственно, инициализацию объектов и очистку после их уничтожения. Простой конструктор можно добавить в класс с помощью такого синтаксиса: class MyClass { public MyClass () { // Код конструктора. > } Этот конструктор должен иметь то же имя, что и у класса, в котором он содержится, не принимать никаких параметров (что делает его конструктором по умолчанию для данного класса) и быть общедоступным, чтобы с его помощью могли создаваться экземпляры объектов этого класса (более подробно об этом говорилось в главе 8). Также допускается использовать и приватный конструктор по умолчанию, т.е. такой, который не может применяться для создания экземпляров объектов данного класса (и тем самым превращать класс в несоздаваемый, о чем более подробно рассказывалось в главе 8): class MyClass {
Глава 9. Определение классов 235 private MyClass () { // Код конструктора. } } И, наконец, подобным образом в класс можно добавлять и не используемые по умолчанию конструкторы, просто указанием параметров: class MyClass { public MyClass() { // Код конструктора по умолчанию. } public MyClass (int myint) { // Код конструктора не по умолчанию (с параметром myint) . } } Количество предоставляемых конструкторов может быть практически бесконечным (разумеется, нужно принимать во внимание, что ресурсы памяти и возможные наборы параметров ограничены). Деструкторы объявляются с помощью немного другого синтаксиса. Деструктор, используемый в .NET (и предоставляемый классом System.Object), называется Finalize, но не это имя применяется для объявления деструктора. Вместо переопределения Finalize используется следующий код: class MyClass { -MyClass () { // Тело деструктора. } } То есть деструктор класса объявляется путем указания имени класса (как и конструктор) с префиксом ~ (тильда) впереди. Код в деструкторе выполняется на этапе сборки мусора, позволяя освобождать ресурсы. После вызова данного деструктора также осуществляются и неявные вызовы деструкторов базовых классов, в числе которых и вызов деструктора Finalize корневого класса System.Object. Подобный подход позволяет .NET Framework гарантировать их выполнение, поскольку в случае переопределения деструктора Finalize вызовы деструктором базовых классов нужно было бы выполнять явно, что потенциально опасно (о том, как вызывать методы базовых классов, более подробно рассказывается в следующей главе). Последовательность выполнения конструкторов В случае выполнения нескольких задач в конструкторах класса может быть удобнее размещать весь связанный с этим код в одном месте и получать те же преимущества, что и при разбиении кода на функции, о чем говорилось в главе 6. Это можно делать с помощью метода (как будет показано в главе 10), но язык С# предлагает один замечательный альтернативный вариант, а именно — позволяет конфигурировать любой конструктор так, чтобы перед выполнением своего собственного кода он вызывал какой-то другой конструктор.
236 Часть I. Язык С# Первым делом, однако, необходимо более подробно узнать о том, что же происходит по умолчанию при создании экземпляра класса. Этот вопрос также заслуживает внимания и сам по себе, а не только потому, что облегчает централизацию кода инициализации, как было сказано выше. Во время разработки объекты часто ведут себя не совсем так, как ожидается, из-за ошибок, которые возникают во время вызова конструкторов — обычно либо из-за неправильного создания экземпляра какого-то базового класса в иерархии наследования или неверного предоставления информации конструкторам базовых классов. Понимание того, что происходит на этом этапе жизненного цикла объекта, может значительно упростить решение проблем подобного рода. Чтобы был создан экземпляр производного класса, сначала должен обязательно быть создан экземпляр его базового класса, а чтобы был создан экземпляр этого базового класса, сначала должен быть создан экземпляр его собственного базового класса, и т.д., вплоть до самого класса System.Object (который является для всех классов корневым). Поэтому, какой бы конструктор не использовался для создания экземпляра класса, первым все равно всегда вызывается конструктор System.Object .Object. Независимо от того, какой конструктор применяется в производном классе (по умолчанию или не по умолчанию), если только не задается противоположное поведение, для базового класса применяется конструктор по умолчанию. (Ниже будет показано, как изменить это поведение.) Проиллюстрируем последовательность выполнения на простом примере. Рассмотрим следующую иерархию объектов: public class MyBaseClass { public MyBaseClass () { } public MyBaseClass(int i) { } } public class MyDerivedClass : MyBaseClass { public MyDerivedClass () public MyDerivedClass(int 1) public MyDerivedClass(int i, int j) } В такой иерархии экземпляр класса MyDerivedClass может создаваться так: MyDerivedClass myObj = new MyDerivedClass(); Тогда последовательность происходящих событий будет выглядеть следующим образом. □ Сначала будет выполняться код конструктора System.Object.Object. □ Затем будет выполняться код конструктора MyBaseClass .MyBaseClass. □ И, наконец, последним будет выполняться код конструктора MyDerivedClass. MyDerivedClass.
Глава 9. Определение классов 237 В качестве альтернативного варианта экземпляр класса MyDerivedClass может создавать и так: MyDerivedClass myObj = new MyDerivedClassD); Тогда последовательность событий будет выглядеть, как описано ниже. □ Сначала будет выполняться код конструктора System. Obj ect. Ob j ect. □ Затем будет выполняться код конструктора MyBaseClass .MyBaseClass. □ И, наконец, последним будет выполняться код конструктора MyDerivedClass . MyDerivedClass(int i). И, наконец, экземпляр класса MyDerivedClass может создаваться еще и так: MyDerivedClass myObj = new MyDerivedClassD, 8) ; Это приведет к тому, что последовательность событий будет выглядеть следующим образом. □ Сначала будет выполняться код конструктора System. Ob j ect. Ob j ect. □ Затем будет выполняться код конструктора MyBaseClass .MyBaseClass. □ И, наконец, последним будет выполняться код конструктора MyDerivedClass . MyDerivedClass (int i, int j). Такая система подходит для большинства случаев, но в некоторых ситуациях может потребоваться чуть больший контроль над происходящими событиями. Например, в случае применения последнего способа для создания экземпляра класса MyDerivedClass может быть необходимо, чтобы последовательность событий выглядела, как описано ниже. □ Первым выполнялся код конструктора System. Ob j ect. Ob j ect. □ Вторым —код конструктора MyBaseClass .MyBaseClass (int i). □ Третьим — код конструктора MyDerivedClass .MyDerivedClass (int i, int j ). Тогда код, в котором используется параметр int i, можно будет поместить в MyBaseClass (int i), а это значит, что у конструктора MyDerivedClass (int i, int j) будет меньше работы — ему придется обрабатывать только параметр int j. (Здесь предполагается, что в обоих сценариях предназначение параметра int i выглядит идентично, что может не всегда быть так, но на практике при такой организации обычно именно так и бывает.) Язык С# позволяет при желании задавать подобное поведение. Для этого можно использовать так называемый инициализатор конструктора (constructor initializer), который состоит из кода, размещаемого в определении метода после символа двоеточия. Например, указать подлежащий использованию конструктор базового класса в определении конструктора в производном классе в рассматриваемом примере можно было бы следующим образом: public class MyDerivedClass : MyBaseClass { public MyDerivedClass (int i, int j) : base(i) { } } Ключевое слово base инструктирует процесс создания экземпляров .NET использовать конструктор базового класса, у которого имеются заданные параметры. В данном примере у него есть только один параметр— типа int (значение которого является
238 Часть I. Язык С# тем же значением, которое передается конструктору MyDerivedClass в параметре i), так что применяться будет MyBaseClass (int i). Это означает, что MyBaseClass вызываться не будет, поэтому последовательность событий будет выглядеть так, как показывалось перед этим, т.е. необходимым образом. Это ключевое слово можно еще использовать для указания литеральных значений для конструкторов базовых классов, например, в случае применения конструктора по умолчанию класса MyDerivedClass для вызова конструктора не по умолчанию класса MyBaseClass: public class MyDerivedClass : MyBaseClass { public MyDerivedClass () : base E) { } } Это приведет к получению следующей последовательности. □ Первым будет выполняться код конструктора System. Obj ect. Object. □ Вторым —код конструктора MyBaseClass .MyBaseClass (int i). □ Третьим — код конструктора MyDerivedClass .MyDerivedClass (). Помимо ключевого слова base существует еще одно ключевое слово, которое можно применять в качестве инициализатора конструктора— this. Данное ключевое слово инструктирует отвечающий за создание экземпляров процесс .NET использовать для текущего класса перед вызовом указанного конструктора конструктор не по умолчанию: public class MyDerivedClass : MyBaseClass { public MyDerivedClass () : this E, 6) public MyDerivedClass(int i, int j) : base(i) } В этом случае последовательность будет выглядеть так, как описано ниже. □ Первым будет выполняться код конструктора System.Object.Object. □ Вторым —код конструктора MyBaseClass .MyBaseClass (int i). □ Третьим — код конструктора MyDerivedClass. MyDerivedClass (int i, int j). □ Четвертым — код конструктора MyDerivedClass .MyDerivedClass. Единственное ограничение здесь состоит в том, что с помощью инициализатора конструкторов можно указывать только один единственный конструктор. Однако, как было видно в предыдущем примере, на самом деле это не является таким уж серьезным ограничением, поскольку все равно позволяет получать довольно сложные последовательности выполнения. Если для конструктора не указывается никакой инициализатор, компилятор автоматически добавляет инициализатор base (). Это приводит к получению поведения по умолчанию, которое описывалось ранее в этом разделе.
Глава 9. Определение классов 239 Средства объектно-ориентированного программирования, доступные в VS и VCE Из-за того, что ООП является фундаментальной темой в .NET Framework, в VS и VCE для облегчения процесса разработки ООП-приложений предлагается несколько специальных средств. Некоторые из них рассматриваются в настоящем разделе. Окно Class View В главе 2 уже упоминалось о том, что окно Solution Explorer (Проводник решений) делит экранное пространство с еще одним окном, которое называется Class View (Представление классов). Окно Class View отображает иерархию классов в приложении и позволяет сразу просматривать все характеристики используемых классов. На рис. 9.3 показано, как выглядело бы это окно в случае проекта из предыдущего практического занятия. На этом рисунке видно, что данное окно имеет два главных раздела; в нижнем разделе отображаются члены типов. Чтобы увидеть, как это происходит на примере данного проекта, а также, какие еще действия можно выполнять в окне Class View, необходимо отобразить некоторые элементы, которые сейчас являются скрытыми. Чтобы сделать это, отметьте элементы в выпадающем списке Class View Grouping (Группировка представления классов) в верхней части окна Class View, как показано на рис. 9.4. После этого в окне Class View должны стать видимыми все члены, и появится кое- какая дополнительная информация (рис. 9.5). Здесь может использоваться масса различных обозначений, в том числе и значки, перечисленные в табл. 9.3 \Пл1т т l.ia^-_^ ШШШШШШШШШШШШШШШШШятшш - 1 fT? Si Di Project References Э О Ch09Ex01 00 •* IMyBaselnterface За н° IMyBaselnterface 2 GB 5° IMylnterface в 4* My6ase ffi it MyClass S HJ MyComplexClass SB $f Program ^ИИЯ^ЯтЯ^Д! Class view Г" Рис. 9.3. Внешний вид окна Class View |£Ц [*\ Show Base Types [*]j Show Project References |*{j Show Hidden Types And Members [*) Show Public Members [vj Show Protected Members , *| Show Private Members | * | Show Other Members I*]' Show Inherited Members Puc. 9.4. Изменение настроек окна Class View cusTview «Search» 3 t ♦ {) a» Project Reference» _j mscorlib -^ System •J System.Core -j System.Data J System.Data.DataSetExtenslons •Ш System.Xml J System.Xml.linq Ch09Ex01 ~° IMyBaselnterface i° IMyBaseInterface2 J* IMylnterface ■*S MyBase ;ft MyClass ■ || Base Types Ji Program v Equal$(object, object) % Equals(object) • GetHashCodeO ♦ GetTypeO >♦ MemberwiseCloneO ♦ ReferenceEqualslobject, object) • ToStnngO |Q- ■>••:■.■ : Classv Puc. 9.5. Отображение скрытых элементов и дополнительной информации в окне Class View
240 Часть I. Язык С# Таблица 9.3. Возможные значки Значок э о м ~о V Zf V # ^ «? / Ъ ♦о Что обозначает Проект Пространство имен Класс Интерфейс Метод Свойство Поле Структура Перечисление Элемент перечисления Событие Делегат Сборка Обратите внимание на то, что некоторые из этих значков используются для обозначения определений не только классов, но и других типов, например, структур и перечислений. Еще под ними могут отображаться и другие обозначения, указывающие на уровень доступа (под общедоступными элементами такие обозначения не отображаются). Эти обозначения показаны в табл. 9.4. Таблица 9.4. Обозначения, указывающие на уровень доступа Значок 2> > £3 Уровень доступа, на который он указывает Приватный Защищенный Внутренний Для обозначения абстрактных, герметизированных и виртуальных элементов никакие значки не используются. Помимо просмотра этой информации здесь еще можно получать доступ к коду многих элементов. Выполнение двойного щелчка на элементе или щелчка правой кнопкой мыши и выбор в контекстном меню пункта Go To Definition (Перейти к определению) позволяет сразу же осуществлять переход к тому коду в проекте, в котором содержится его определение, если таковой доступен. Если код не доступен, например, как для базовых типов (вроде System.Object), тогда вместо этого предлагается опция Browse Definition (Обзор определений), которая переводит в окно Object Browser (Обзор объектов) (которое будет более подробно описываться в следующем разделе). Все остальные элементы на рис. 9.5 являются ссылками проектов (Project References). Они позволяют просматривать те сборки, на которые ссылаются проекты, а в данном
Глава 9. Определение классов 241 случае— еще и базовые типы .NET в mscorlib и system, типы доступа к данным в system.data и типы манипулирования XML в system.xml. To есть эти ссылки можно разворачивать и просматривать содержащиеся в сборках пространства имен и типы. Кроме того, в окне Class View доступна функция, позволяющая отыскивать вхождения тех или иных типов и членов в коде. Использовать ее можно путем выполнения на интересующем элементе щелчка правой кнопкой мыши и выбора в контекстном меню пункта Find All References (Найти все ссылки). Выбор этой опции приводит к отображению результатов в окне Find Symbol Results (Результаты поиска символов), которое появляется в нижней части экрана в виде вкладки в области Error List (Список ошибок). Помимо этого, в окне Class View можно также и переименовывать элементы. При попытке сделать это, тут же отображается опция для переименования ссылок на этот элемент во всех местах, где они встречаются в коде. Это исключает вероятность опечаток в именах классов и позволяет изменять их так часто, как хочется. Окно Object Browser Окно Object Browser (Обзор объектов) представляет собой расширенную версию окна Class View и позволяет просматривать другие доступные для проекта классы, а также внешние классы. Переход в него может осуществляться как автоматически (пример такой ситуации приводился ранее), так и вручную, выбором в меню View (Вид) пункта Other Windows1^Object Browser (Другие окна^Юбзор объектов). Оно отображается в основном окне, где с ним можно работать точно так же, как и с окном Class View. В этом окне отображается та же информация, что и в окне Class View, но с большим количеством типов .NET. При выборе элемента информация о нем отображается в еще одном отдельном окне, как показано на рис. 9.6. Base64FormattingOptions BitConverter Boolean е itfnji Byte CennotUnloedAppDomeinExcept % t *J BedlmageFormetException 1 И BaseDFormattingOptions A. Af BitConvtrter t -f» Boolean л ij Buffer • %V Byte ъ . • v OpenStandardErrorfl * OpenStendardlnput(int] * OpenStenderdlnputfl * OperSt»nd»rdOutput|int) v OpenStandardOutputO v ReadO » ReedKeytbool) ; CennotUnloedAppDomeinExceptlon 9 ReadlmeO ► Char .. „ ^ t: ij CharEnumerator X -Ц ClSCompiientAttnbute 4. jfc Companson<T> ';QQEO Summon > *J ConsoleCanceiEventArgs Obtain» the next character or function key pressed by the user. The pressed key is displayed in the console window. public static Sptea.ComoteKcylnfo ReadKeyO Member of <*«Л<А Uw0»f 1 j| ConsoieCancelEventHandler t jr* ConsoleColor t >f ConsoieKey I f» ConsoleKeytnfo В if ConsoleModifiers I *" ConsoleSpecialKey I "J ContextBoundObject • ~i ContextMershaiException • •'. ContextStaticAttnbute Return*: A System.ConsoleKeylnfo object that describes the System.ConsoleKey constant and Unicode character, if any, that correspond to the pressed console key. The System.ConsoleKeylnfo object also describes, in a bitwise combination of System.ConsoleModifiers values, whether one or more SHIFT, ALT. or CTRL modifier keys was pressed simultaneously with the console key. . S)drm jjiv.iiiiK.'jjn«t>«»ui щИн Лм lyjtm SowdtJnBfopti% iitdtactBdftw* iomtibMi« аЙигЙипИи Рис. 9.6. Выбор элемента в окне Object Browser Здесь был выбран метод ReadKey класса Console (который находится в пространстве имен System внутри сборки mscorlib). В информационном окне справа внизу отображается сигнатура этого метода, класс, которому он принадлежит, и краткая информация о выполняемой им функции. Все эти сведения могут быть полезны при изучении типов .NET, а также при желании просто вспомнить, что делает тот или иной класс. Помимо этого, данным информационным окном можно пользоваться и для создаваемых типов.
242 Часть I. Язык С# Внесите следующее изменение в код Ch09Ex01: /// <suxnmary> ///В этом классе содержится моя программа! /// </summary> class Program { static void Main(string[] args) { MyComplexClass myObj = new MyComplexClass(); Console.WriteLine(myObj.ToString()); Console.ReadKey() ; Вернитесь в окно Object Browser. Внесенное изменение проявится в информационном окне. Это является примером XML-документации, о которой более подробно будет рассказываться в главе 31. Если вы вносили данное изменение вручную, то наверняка заметили, что ввод всего лишь трех символов косой черты /// вынуждает IDE-среду добавлять большую часть остального кода самостоятельно. Она автоматически анализирует код, к которому применяется XML-документация и создает базовую XML-документацию, что является еще одним доказательством того, что VS и VCE представляют собой просто замечательные инструменты для работы. Добавление классов В VS и VCE содержатся средства, которые могут ускорять выполнение некоторых распространенных задач и часть которых подходит для ООП. Одним из них является мастер добавления новых элементов (Add New Item Wizard), который позволяет добавлять в проект новые классы с минимальным количеством ввода. Доступ к нему можно получать либо выбором в меню Project (Проект) пункта Add New Item (Добавить новый элемент), либо щелчком правой кнопкой мыши на проекте в окне Solution Explorer и выбором соответствующего пункта в контекстном меню. После этого на экране появляется диалоговое окно, позволяющее выбирать элемент для добавления. Внешний вид этого окна в VS и VCE отличается, но функциональные возможности одинаковы. В VS в нем по умолчанию на выбор предлагаются большие значки, а в VCE— значки поменьше. В той и другой IDE для добавления класса выполняются следующие действия: выбирается элемент Class (Класс) в поле Templates (Шаблоны), как показано на рис. 9.7, предоставляется имя для файла, в котором будет содержаться данный класс, и осуществляется щелчок на кнопке Add (Добавить). 1 штттт^шлт 1 ; Templetes | : Vnoel ЯооЧо infUHed tempi**» i3 J *3 ' About Box Application Application ConftgunjtJ... Manifest Fil« 3 -d J> j ; Otbuggtr Interface lINQtoSQl j : VIsueHxer Clesses J 1 1 SfttingjF.lt Text File Ultr Control 1 ! An empty d»j> definition 1 j Ntfflt: MyNewCICSS.CS i Assembly Inform its... и locii Detebise s User Control (WW) A CUss 51 MM Pirent и MM.N- «J Code File 5 Resources File J kmi File & DttaSet 11 Service-besed Dettbese j A« 1 ) -uel p- 1 . 1 1 ! Сетке* 1 Рис. 9.7. Добавление класса
Глава 9. Определение классов 243 Созданному классу присваивается такое же имя, как было предоставлено для файла. В приводившемся ранее практическом занятии определения классов добавлялись вручную в файл Program, cs. Зачастую размещение классов в отдельных файлах упрощает их отслеживание. Ввод информации в окне Add New Item (Добавление нового элемента) при открытом проекте Ch0 9Ex01 приведет к генерированию следующего кода в файле MyNewClass. cs: using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ch09Ex01 { class MyNewClass } Этот класс — MyNewClass — определяется в том же самом пространстве имен, что и класс точки входа — Program, благодаря чему его можно будет использовать в коде точно так же, как и если бы он был определен в том же самом файле. Здесь видно, что класс, который генерируется автоматически, не содержит никакого конструктора. Напоминаем, что в случае, когда определение класса не включает конструктор, при компиляции кода компилятор самостоятельно добавляет необходимый стандартный конструктор. Диаграммы классов Одной из мощнейших функциональных возможностей среды VS, которая пока еще не демонстрировалась, является генерация диаграмм классов и применение их для изменения проектов. Редактор диаграмм классов VS позволяет легко генерировать UML- подобные диаграммы кода. Увидеть его в действии можно в следующем практическом занятии, где его применение иллюстрируется на примере генерации диаграммы классов для созданного ранее проекта Ch09Ex01. К сожалению, в VCE возможность создания диаграмм классов не предусмотрена, поэтому испробовать предлагаемые в следующем практическом занятии инструкции могут только те, у кого имеется VS. J]p^^^ Генерирование диаграммы классов 1. Откройте проект Ch09Ex01, который создали ранее в этой главе. 2. В окне Solution Explorer выделите файл Program.cs и затем щелкните на кнопке View Class Diagram (Просмотреть диаграмму классов) в панели инструментов, как показано на рис. 9.8. ■г Solution ChOtfa.. ~ « X Solution 'Ch09E»c (l project) ас. ;«в DmntiMx ' е Л- М Properties ' & эА References Ir" LfJ Solution Explorer ЩЩЩЩ" Рис. 9.8. Кнопка View Class Diagram
244 Часть I. Язык С# 3. После этого на экране появится диаграмма классов, имеющая имя Class Diagraml.cd. 4. Щелкните на элементе IMylnterf асе и с помощью окна Properties измените значение его свойства Position на Right. 5. Далее щелкните правой кнопкой мыши на элементе MyBase и в контекстном меню, которое появится после этого, выберите пункт Show Base Type (Показать базовый тип). 6. Передвиньте объекты в диаграмме, чтобы их компоновка выглядела более привлекательно. В конечном итоге диаграмма должна приобрести примерно такой вид, как показан на рис. 9.9. IMyB.ieInterf.ee | Interface IMyB.idnterf.ce2 * Interface ■ J IMylnt.rf.c. 1) Interface "♦ IMyflejelnterrece -♦1Мувие1Меглке2 Object Ctes m a „„» s Class I MyComplerxCI.il Scaled Class -»MyClass V__ 1 J • ] ' ""\ • IMylnterf ace Рис. 9.9. Конечный внешний вид диаграммы классов Описание полученных результатов Приложив совсем небольшое количество усилий, в этом практическом занятии вы создали диаграмму классов, не сильно отличающуюся от UML-диаграммы, показанной на рис. 9.2. Рассмотрим характеристики полученной диаграммы. □ Классы изображаются в виде голубых прямоугольников вместе с именем и типом. □ Интерфейсы изображаются в виде зеленых прямоугольников вместе с именем и типом. □ Наследование изображается с помощью стрелок с белыми наконечниками (и, в некоторых случаях, соответствующего текста внутри прямоугольников классов). □ Классы, реализующие интерфейсы, снабжаются обозначением в виде окружности с линией. □ Абстрактные классы изображаются с пунктирным контуром и выделенным курсивом именем. □ Герметизированные классы изображаются с толстым черным контуром.
Глава 9. Определение классов 245 Выполнение щелчка на объекте приводит к отображению дополнительной информации внутри окна Class Details (Детали класса) в нижней части экрана. Здесь можно просматривать (и изменять) члены класса. Изменять детали класса также можно и в окне Properties (Свойства). В главе 10 будет более подробно рассказываться о том, как добавлять в классы новые члены с помощью диаграммы классов. С помощью панели Toolbox (Элементы управления) в диаграмму можно добавлять новые элементы, вроде классов, интерфейсов и перечислений, и определять отношения между ними. При таком подходе код для добавляемых новых элементов генерируется автоматически. С применением этого редактора можно проектировать целые семейства типов графическим образом, никогда не прибегая к редактору кода. В том, что касается фактического добавления функциональных возможностей, то его, конечно, нужно выполнять вручную, но данный редактор все равно является прекрасной отправной точкой. В последующих главах еще будет рассказываться о нем и том, что он может делать, например, о том, как в нем применять панель Object Test Bench (Панель тестирования объектов) для тестирования классов перед их использованием в коде. Пока желающие могут изучить доступные в нем возможности самостоятельно. Проекты библиотек классов Классы можно размещать не только в отдельных файлах внутри проекта, но и вообще в совершенно отдельных проектах. Проект, в котором не содержится ничего, кроме классов (вместе с определениями других имеющих к ним отношение типов, но не входной точки), называется библиотекой классов. Проекты типа библиотеки классов компилируются в .dll-сборки и доступ к их содержимому можно получать путем добавления ссылок на них из других проектов (которые могут являются частью того же самого решения, но не обязательно). Это расширяет границы обеспечиваемой объектами инкапсуляции, поскольку библиотеки классов могут пересматриваться и обновляться без затрагивания тех проектов, в которых они используются, что позволяет легко модернизировать предоставляемые классами службы (которые могут влиять на работу множества пользующихся ими приложений). В следующем практическом занятии демонстрируется пример создания и использования проекта типа библиотеки классов, а также отдельного проекта, в котором применяются содержащиеся в нем классы. актическое занятие ИсПОЛЬЗОВаНИв библиотеки КЛЭССОВ 1. Создайте новый проект типа Class Library (Библиотека классов) по имени Ch09ClassLib и сохраните его в каталоге С: \BegVCSharp\Chapter09, как показано на рис. 9.10. 2. Измените в нем имя файла Class I. cs на MyExternalClass . cs (выполнив щелчок правой кнопкой мыши на этом файле в окне Solution Explorer и выбрав в контекстном меню пункт Rename (Переименовать)). В диалоговом окне, которое появится далее, щелкните на кнопке Yes (Да). 3. После этого код в файле MyExternalClass .cs автоматически изменится и станет отражать изменение в имени класса:
246 Часть I. Язык С# VimmI Studio «wtOed temptate з s ^ Window) Class library WPf FormiAp... Apphc»tion Stircn Online Templttej WPf Browser Application Contole Application a Empty Project A project tor creating a <• da» Mbrary (dli) I.NET Framewort 3.5) Name: ChOSCIasslib СЖЗЯш II I I I L I IN! I 1 I , , Puc. 9.10. Создание нового проекта типа Class Library class MyExternalClass 4. Добавьте в проект новый класс с именем файла MylnternalClass. cs. 5. Измените код так, чтобы стало явно видно, что класс MylnternalClass является внутренним (internal): internal class MylnternalClass { 6. Скомпилируйте проект (у этого проекта нет точки входа, поэтому запускать его как обычный проект нельзя — вместо этого его можно скомпоновать, выбрав в меню Build (Сборка) пункт Build Solution (Собрать решение)). 7. Создайте новый проект типа Console Application (Консольное приложение), назовите его Сп09Ех02 и сохраните в каталоге С: \BegVCSharp\Chapter09. 8. Выберите в меню Project (Проект) пункт Add Reference (Добавить ссылку) или выберите опцию с таким же названием после выполнения щелчка правой кнопкой мыши на папке References (Ссылки) в окне Solution Explorer. 9. Перейдите на вкладку Browse (Обзор), отыщите каталог С: \BegVCSharp\Chapter09\ Chapter09\Ch09ClassLib\bin\Debug\ и дважды щелкните на файле Ch09ClassLib.dll. 10. По завершении операции удостоверьтесь в том, что соответствующая ссылка была добавлена в окно Solution Explorer (рис. 9.11). 11. Откройте окно Object Browser и разверните представляющий добавленную ссылку узел, чтобы увидеть, какие объекты в нем содержатся, как показано на рис. 9.12. 12. Измените код в файле Program, cs следующим образом: i J3 Solution Ch09ExO2' A project) - ^CM9Ext2 .t. Л Properties i-j ыИ References ♦a System •ul System.Core -J System.Data •uJ System.Data.DataSetfxtensions •vJ SystemJOnl -чЗ System.Xml.llnq <jj Program.cs ^Solution Explorer f3tftm>*iW! using System; using System.Collections.Generic; Puc. 9.11. Ссылки в окне Solution Explorer
Глава 9. Определение классов 247 using System.Linq; using System.Text; using Ch09ClassLib; namespace Ch09Ex02 { class Program { static void Mam (string [ ] args) { MyExternalClass myObj = new MyExternalClass() ; Console.WriteLine(myObj.ToString()); Console.ReadKey(); Proj}r»m.c> Ob^ct Biowtcf j Biowsc: Ai Component* «Sc«ch> \\& 1ЗВ •л {) Ch09Clas$lib с 1$ MyCxternalClass В [a BaseTypej 1$ Object t ,j8 Ch09Ex02 ~ X i i««l.fllFU ' J 3 Ш lis «O MicrojoftBuild.Conver$ion.v3.5 Iffi ЧЭ MicrojoftBulld.Engine 1 ffi .a Microjoft.Build.Framework ! t j Microjoft.Build.Utilities v3.5 ! ♦ . _i Microsoft VisualBasic Etl -t-I Microsoft VisuaK STICIR i ♦ uJ mjcorhb *l<1 f 1т"тгиммиТ Assembly Ch09CU<slib 1 C:\BegVCSharp\ChapterO9\Ch09Cla55Lib\Ch09Cla5sLib\bin J \Debug\Ch09ClassLib.dll •n' в Pwc. 9.72. Просмотр содержимого добавленной ссылки в окне Object Browser 13. Запустите приложение. На рис. 9.13 показан результат, который должен получиться. г ' ' " " ' ■ j Ш file:///C:/B*flVCShariVCh«pltft)9/CW)9Ex02/... \шшшшшт i —■— ШШШШЩЩЩШИ^^Ш Рис. 9.13. Приложение Ch09Ex02 в действии Описание полученных результатов В этом примере были созданы два проекта: проект типа библиотеки классов и проект типа консольного приложения. Первый называется Ch09ClassLib и содержит два класса: MyExternalClass, который является общедоступным, и MylnternalClass, который является доступным только внутри проекта. Обратите внимание, что последний по умолчанию был неявным при создании, поскольку не имел никакого модификатора доступа. Однако уровень доступности рекомендуется указывать явно, поскольку это делает код более удобочитаемым, из-за чего и было добавлено ключевое слово internal. Что касается проекта типа консольного приложения, то он называется Сп0 9Ех02 и содержит простой код, применяющий проект библиотеки классов Ch09ClassLib.
248 Часть I. Язык С# Приложение, в котором используются классы, определенные во внешней библиотеке, часто называют клиентским приложением библиотеки, а код, в котором применяется определенный разработчиком класс — соответственно, клиентским кодом. Чтобы использовать классы из Ch0 9ClassLib, вы добавили в консольное приложение Ch09Ex02 ссылку на Ch09ClassLib.dll. То есть в данном примере вы просто указали на выходной файл библиотеки классов, хотя могли бы так же легко и скопировать этот файл в какую-то локальную папку Ch0 9Ex02, что позволило бы продолжать разработку библиотеки классов без оказания воздействия на само консольное приложение. Для замены старой версии сборки новой тогда было бы достаточно просто скопировать сгенерированный новый файл DLL-библиотеки поверх старого. После добавления ссылки вы просмотрели доступные файлы с помощью окна Object Browser. Поскольку класс MylnternalClass является внутренним, его в этом окне видно не было — он не доступен для внешних проектов. Класс MyExternalClass, однако, доступен для внешних проектов, и именно он и применяется в консольном приложении. Заменить код в консольном приложении кодом, пытающимся использовать внутренний класс, можно было бы следующим образом: static void Main(string [ ] args) { MylnternalClass myObj = new MylnternalClass () ; Console.WriteLine(myObj.ToString()); Console.ReadKey(); } При попытке скомпилировать этот код, однако, компилятор выдал бы сообщение об ошибке: 'Ch09ClassLib.MylnternalClass' is inaccessible due to its protection level 'Ch09ClassLib.MylnternalClass ' является недоступным из-за его уровня защиты Такой прием использования классов из внешних сборок является ключевым в программировании на С# и .NET Framework. На самом деле при использовании любых классов из .NET Framework тоже применяется именно он, поскольку эти классы обрабатываются тем же самым образом. Интерфейсы или абстрактные классы В этой главе уже было показано, как создавать интерфейсы и абстрактные классы (хотя пока и без членов, чему посвящена глава 10). Эти два типа во многом похожи, поэтому совершенно не помешает узнать, как определять то, в каких случаях лучше использовать интерфейс, а в каких — абстрактный класс. Сначала давайте поговорим о сходствах. И абстрактные классы, и интерфейсы могут иметь члены, наследуемые производным классом. Ни интерфейсы, ни абстрактные классы не разрешают создавать непосредственные экземпляры, но позволяют объявлять переменные таких типов, что дает возможность присваивать унаследованные от этих типов объекты этим переменным с помощью полиморфизма и далее использовать члены этих типов через эти переменные, хотя и без непосредственного доступа к другим членам производного объекта. Теперь поговорим об отличиях. Производные классы могут наследоваться только от одного единственного базового класса, а это значит, что напрямую может наследоваться только один абстрактный класс (хотя и допускается, чтобы в цепочке наследования присутствовало несколько абстрактных классов). Что касается интерфейсов,
Глава 9. Определение классов 249 то их, наоборот, может быть в классах сколько угодно, но никакой серьезной разницы это не дает— похожие результаты могут достигаться и в том, и в другом случае. Просто подход с интерфейсами немного отличается. Абстрактные классы могут иметь как абстрактные члены (каковые не предусматривают наличия кода и должны быть реализованы в производном классе, если только тот сам не является абстрактным), так и не абстрактные члены (каковые предусматривают наличие кода и могут быть виртуальными, благодаря чему могут переопределяться в производном классе). Члены интерфейсов, наоборот, должны обязательно реализоваться в том классе, который использует данный интерфейс, и не предусматривают наличие кода. Более того, члены интерфейсов по определению являются общедоступными (поскольку предназначены для внешнего применения), а члены абстрактных классов могут также быть и приватными (при условии, если они не являются абстрактными), защищенными, внутренними или защищенными внутренними (в случае чего они являются доступными только из кода внутри приложения либо из производного класса). Помимо этого интерфейсы не могут содержать ни полей, ни конструкторов, ни деструкторов, ни статических членов, ни констант. Это указывает на то, что эти два типа имеют разное предназначение. Абстрактные классы предназначены для использования в качестве базового класса для объектов, имеющих определенные общие ключевые характеристики, например, общее предназначение и структуру. Интерфейсы же предназначены для применения классами, которые могут отличаться на более фундаментальном уровне, но при этом все равно делать какие-то одинаковые вещи. Например, рассмотрим семейство объектов, представляющих поезда. В базовом классе— Train (Поезд) — должно содержаться основное определение поезда, вроде информации о ширине колеи и типе двигателя (который может быть паровым, дизельным и т.д.). Однако такой класс должен быть абстрактным, поскольку такого понятия, как "унивеРсальныи поезд, не существует. Создание "настоящего" поезда требует добавления характеристик, отражающих специфику конкретно данного поезда, т.е., например, создания производных классов вроде PassengerTrain (Пассажирский поезд), FreightTrain (Товарный поезд) и 424DoubleBogey (Двухосный поезд 424), как показано на рис. 9.14. Train 5 PassengerTrain FreightTrain 424DoubleBogey Рис. 9.14. Диаграмма классов, представляющих поезда Аналогичным образом можно определить и семейство объектов, представляющих автомобили, с абстрактным базовым классом Саг (Автомобиль) и производными классами вроде Compact (Малолитражка), SUV (Внедорожник) и PickUp (Пикап). Тогда классы Саг и Train могут даже наследоваться от одного общего базового класса, например, Vehicle (Транспорт), как показано на рис. 9.15.
250 Часть I. Язык С# 1 | Compact Car f 1 SUV Vehicle * Pickup Train ? i i PassengerTrain | FreightTrain | | 424DoubleBogey | | Puc. 9.15. Диаграмма классов, представляющих автомобили Некоторые из классов ниже в иерархии могут иметь общие характеристики по своему предназначению, а не только по тому классу, от которого наследуются. Например, классы PassengerTrain, Compact, SUV и Pickup допускают возможность перевозки пассажиров, а раз так, значит, они могут обладать общим интерфейсом IPassengerCarrier (Пассажирский транспорт). Классы FreightTrain и PickUp допускают возможность перевозки тяжелых грузов, а раз так, значит, могут тоже иметь общий интерфейс IHeavyLoadCarrier (Транспорт для перевозки тяжелых грузов), как показано на рис. 9.16. Vehicle i Car Train Compact Pickup PassengerTrain FreightTrain 424DoubleBogey «interface»» IPassengerCarrier «interface» IHeavyLoadCarrier Puc. 9.16. Интерфейсы IPassengerCarrier и IHeavyLoadCarrier
Глава 9. Определение классов 251 Путем разбиения системы объектов подобным образом перед назначением конкретных деталей можно легко определять, где лучше использовать абстрактные классы, а не интерфейсы, а где наоборот, лучше использовать интерфейсы, а не абстрактные классы. Результата, получившегося в этом примере, не удалось бы достигнуть путем применения только интерфейсов или только наследования абстрактных классов. Типы-структуры В главе 8 уже отмечалось, что структуры и классы очень похожи между собой, но только структуры представляют собой типы-значения, а классы — ссылочные типы. Что это на самом деле означает? Легче всего будет объяснить на конкретном примере, вроде того, что предлагается в следующем практическом занятии. в! Классы или структуры 1. Создайте новый проект консольного приложения по имени Ch09Ex03 и сохраните его в каталоге С: \BegVCSharp\Chapter09. 2. Измените его код следующим образом: namespace Ch09Ex03 { class MyClass public int val; struct myStruct public int val; class Program static void Main(string[] args) { MyClass objectA = new MyClass () ; MyClass objectB = objectA; objectA. val = 10; objectB. val = 20; myStruct structA = new myStruct () ; myStruct structB = structA; structA. val = 30; structB. val = 40; Console.WriteLine("objectA.val = {0}", objectA.val); Console.WriteLine("objectB.val = {0}", objectB.val); Console. WriteLine ("structA. val = {0}", structA. val) ; Console.WriteLine("structB.val = {0}", structB.val); Console.ReadKey(); } 3. Запустите приложение. На рис. 9.17 показан результат, который должен получиться.
252 Часть I. Язык С# Рис. 19.17. Приложение Ch09Ex03 в действии Описание полученных результатов В этом примере в приложение сначала добавляются определения двух типов: определение структуры по имени myStruct, имеющей одно общедоступное поле val, и определение класса по имени MyClass, который содержит идентичное поле (о членах классов, таких как поля, более подробно будет рассказываться в главе 10; пока что важно усвоить лишь то, что синтаксис выглядит одинаково). Далее для экземпляров обоих этих типов выполняются следующие одинаковые операции. 1. Объявляется переменная данного типа. 2. Создается новый экземпляр данного типа в этой переменной. 3. Объявляется вторая переменная данного типа. 4. Второй переменной присваивается первая переменная. 5. Присваивается значение полю val в экземпляре первой переменной. 6. Присваивается значение полю val в экземпляре второй переменной. 7. Значения полей val обеих переменных отображаются на экране. Хотя выполняемые для переменных обоих типов операции выглядят одинаково, исход получается разный. При отображении значения поля val типы-объекты имеют одинаковые значения, а типы-структуры — разные. Почему? Дело в том, что объекты представляют собой ссылочные типы. При присваивании объекта переменной фактически этой переменной присваивается просто указатель на объект, на который она должна ссылаться. На языке кода указателем называется адрес в памяти. В данном случае этим адресом является ячейка в памяти, где находится объект. При присваивании первой ссылки на объект второй переменной типа MyClass с помощью показанной ниже строки кода фактически осуществляется копирование этого адреса: MyClass objectB = objectA; В результате получается, что в обеих переменных содержатся просто указатели на один и тот же объект. Что касается структур, то они представляют собой типы-значения. Вместо указателя на структуру в переменной содержится сама структура. Поэтому при присваивании первой структуры второй переменной типа myStruct с помощью показанной ниже строки кода, фактически осуществляется копирование всей информации из одной структуры в другую: myStruct structB = structA; Подобное поведение уже встречалось ранее в книге при рассмотрении простых типов переменных вроде int. Его результатом в данном случае является то, что в двух переменных типа структуры оказываются разные структуры. Процесс применения
Глава 9. Определение классов 253 указателей полностью скрывается от глаз разработчика в управляемом коде С#, тем самым делая код гораздо проще. К низкоуровневым операциям вроде манипулирования указателями в С# допускается получать доступ с помощью небезопасного кода, но эта тема является более сложной и здесь не рассматривается. Поверхностное копирование или глубокое копирование Копирование объектов из одной переменной в другую по значению, а не по ссылке (т.е. тем же образом, что и структур) может оказаться довольно сложной задачей. Поскольку один объект может содержать ссылки на множество других объектов, вроде полей и т.д., такое копирование может требовать выполнения массы операций по обработке. Простого копирования каждого члена из одного объекта в другой может быть недостаточно, поскольку некоторые из этих членов могут сами ссылаться на какие-то другие типы. В .NET Framework все это было учтено. Простого копирования объектов по членам можно добиться с помощью метода MemberwiseClone, унаследованного от класса System.Object. Этот метод является защищенным, но при желании можно объявить для объекта общедоступный метод, который вызывает данный. Подход, применяющий этот метод, называется поверхностным копированием, поскольку не берет в расчет члены ссылочного типа. Это означает, что члены ссылочного типа в новом объекте ссылаются на те же объекты, что и эквивалентные им члены в исходном объекте, что во многих случаях является далеко не идеальным. При желании, чтобы новые экземпляры интересующих членов создавались путем копирования и самих их значений (а не только ссылок), необходимо выполнять глубокое копирование. Существует один интерфейс, который можно реализовать и который позволяет делать это стандартным образом: называется он ICloneable. В случае применения этого интерфейса нужно также обязательно реализовать и тот единственный метод, который он содержит— Clone. Этот метод возвращает значение типа System.Object. Для получения такого объекта можно применять какую угодно обработку, путем реализации, однако, выбранного метода. Это означает, что при желании можно реализовать глубокое копирование (хотя обеспечение точного поведения не является обязательным, так что при необходимости можно выполнять и поверхностное копирование). Более подробно об этом пойдет речь в главе 11. Резюме В этой главе было показано, как определять классы и интерфейсы в С#, благодаря чему приведенная в предыдущей главе теория приобрела более конкретную форму. В частности, здесь было рассказано о синтаксисе, требуемом в С# для создания базовых определений, о ключевых словах, которые в них можно использовать для указания уровня доступности, о способе, которым можно осуществлять наследование от интерфейсов и других классов, о том, как определять абстрактные и герметизированные классы для управления таким наследованием, а также том, как определять конструкторы и деструкторы. Далее был вкратце рассмотрен класс System.Object, являющийся корневым базовым классом для любого определяемого класса. Этот класс предоставляет несколько методов, часть которых являются виртуальными (virtual), что позволяет переопределять их реализации. Еще этот класс позволяет воспринимать экземпляр любого объекта как экземпляр данного типа, тем самым предоставляя возможность применять полиморфизм с любым объектом.
254 Часть I. Язык С# Затем были описаны некоторые средства, которые доступны в VS и VCE для разработки приложений с применением приемов ООП, а именно — окна Class View и Object Browser, и быстрый способ добавления новых классов в проект. Вдобавок было показано, как создавать сборки, не предназначенные для выполнения, но содержащие определения классов, пригодные для использования в других проектах. После этого было более подробно рассказано об абстрактных классах и интерфейсах, а именно — об их сходствах и отличиях и том, в каких ситуациях лучше применять те и другие. Напоследок снова была затронута темы ссылочных типов и типов-значений, но на этот раз немного больше с точки зрения структур (которые представляют собой эквивалентные объектам типы-значения). Это вылилось в обсуждение возможности выполнения поверхностного и глубокого копирования объектов, которое более подробно будет рассматриваться позже в книге. Следующая глава посвящена способам определения членов классов (таких как свойства и методы), которые позволят повысить знания ООП в С# до уровня, достаточного для разработки реальных приложений. Упражнения 1. Что неверно в приведенном ниже коде? public sealed class MyClass { // Члены класса. } public class myDerivedClass : MyClass { // Члены класса. } 2. Как определяются не создаваемые классы? 3. Почему не создаваемые классы все равно являются полезными? Как пользоваться их возможностями? 4. Создайте проект типа библиотеки классов по имени Vehicles и напишите в нем код, реализующий семейство упоминавшихся ранее в этой главе объектов типа Vehicle. Реализовать необходимо девять объектов и два интерфейса. 5. Создайте проект типа консольного приложения по имени Traffic, ссылающийся на проект Vehicles. dll (из упражнения 4). Добавьте в него функцию по имени AddPasenger, принимающую любой объект с интерфейсом IPassengerCarrier. Для доказательства работоспособности кода вызывайте эту функцию с использованием экземпляров всех объектов, которые поддерживают этот интерфейс, вызывая для каждого из них унаследованный от System.Object метод ToString и выводя результат на экран.
10 Определение членов класса В этой главе продолжается изучение определений классов в С#. Выражается оно в описании способов определения членов классов, а именно — полей, свойств и методов. Первым делом анализируется код, который необходимо использовать для определения членов каждого из этих типов, и показывается то, как можно быстро генерировать структуру кода с помощью мастеров, а также то, как быстро изменять код членов, редактируя их свойства. После рассмотрения основных приемов, необходимых для определения членов класса, приводится описание и некоторых более сложных приемов с их участием, вроде сокрытия членов базового класса, вызова переопределенных членов базового класса, определения вложенных типов и определения частичных классов. Напоследок предоставляется возможность применить изученную теорию на практике путем создания простой библиотеки классов, которая будет разрабатываться и использоваться дальше и в последующих главах. В частности, в настоящей главе рассматриваются следующие основные темы. □ Работа с членами классов, а именно — полями, свойствами и методами. □ Создание библиотеки классов. Определение членов В рамках определения класса предоставляются определения для всех его членов, таких как поля, методы и свойства. Каждый член имеет собственный уровень доступности, определяемый во всех случаях с помощью одного из перечисленных ниже ключевых слов. □ public (общедоступный) — делает члены доступными из любого кода. □ private (приватный) — делает члены доступными только из того кода, который является частью данного класса (используется по умолчанию, если не указывается никакое ключевое слово).
256 Часть I. Язык С# □ internal (внутренний)— делает челны доступными только из кода проекта (сборки), внутри которого они определяются. □ protected (защищенный) — делает члены доступными только из того кода, который является частью либо данного класса, либо класса, производного от него. Два последних ключевых слова могут комбинироваться, т.е. также существуют и члены protected internal. Такие члены доступны только из классов, которые являются производными от кода, содержащегося внутри проекта (а точнее — внутри сборки). Поля, методы и свойства могут также объявляться с применением ключевого слова static и в таком случае являются статическими членами, принадлежащими классу, а не экземплярам объектов, как говорилось в главе 8. Определение полей Поля определяются с применением стандартного формата объявления переменных (и, при желании, инициализации), а также перечисленных выше модификаторов: class MyClass { public int Mylnt; } Общедоступные (public) поля в .NETFramework именуются в соответствие с форматом PascalCasing, а не camelCasing, и здесь тоже применяется именно такая схема именования. Поэтому имя поля в приведенном примере и выглядит как Mylnt, а не mylnt. Такая схема именования не является обязательной, но ее применение действительно имеет смысл. Что касается приватных (private) полей, то для их именования никаких особых рекомендаций нет, хотя обычно принято применять для них все-таки схему camelCasing. При определении полей также может использоваться ключевое слово readonly, означающее, что данному полю значение может присваиваться только либо во время выполнения кода конструктора, либо посредством операции начального присваивания: class MyClass { public readonly int Mylnt = 17 ; } Как уже упоминалось во вводном разделе этой главы, поля могут объявляться статическими с использованием ключевого слова static: class MyClass { public static int Mylnt; } К статическим полям доступ осуществляется через класс, в котором они определяются (в предыдущем примере таковым является класс MyClass .Mylnt), а не через экземпляры объектов этого класса. Для создания константных значений служит ключевое слово const. Члены const являются статическими по определению, поэтому в их случае применять модификатор static не нужно (на самом деле это будет считаться ошибкой).
Глава 10. Определение членов класса 257 Определение методов При определении методов применяется стандартный формат определения функций, но также указывается уровень доступности и, при желании, модификаторы static, как показано в следующем примере: class MyClass { public string GetStringO { return "Here is a string."; //(Вот строка) } } Подобно полям, общедоступные методы в .NET Framework именуются в соответствие со схемой PascalCasing. Запомните, что указание ключевого слова static делает метод доступным только через класс, в котором он определяется, но не через экземпляры объектов этого класса. При определении методов могут еще использоваться и следующие ключевые слова. □ virtual (виртуальный) — указывает, что данный метод может переопределяться. □ abstract (абстрактный) — указывает, что данный метод должен обязательно переопределяться в не абстрактных производных классах (может использоваться только в абстрактных классах). □ override (переопределенный) — указывает, что данный метод переопределяет какой-то метод, определенный в базовом классе. □ extern (внешний) — указывает, что определение данного метода находится в каком-то другом месте. Ниже приведен пример переопределения метода: public class MyBaseClass { public virtual void DoSomething () { // Базовая реализация. } } public class MyDerivedClass : MyBaseClass { public override void DoSomething () { // Реализация в производном классе, переопределяющая базовую реализацию. } } Вместе с ключевым словом override может также использоваться и ключевое слово sealed (герметизированный), указывающее, что в данный метод больше не могут вноситься изменения ни в каких производных классах, т.е. метод не может переопределяться в производных классах. Ниже приведен пример: public class MyDerivedClass : MyBaseClass { public override sealed void DoSomething () { // Реализация в производном классе, переопределяющая базовую реализацию. } }
258 Часть I. Язык С# Ключевое слово extern позволяет предоставлять внешнюю по отношению к проекту реализацию метода, но эта тема является более сложной и здесь не рассматривается. Определение свойств Свойства определяются сходным с полями образом, но имеют больше особенностей. В частности, как уже упоминалось ранее, свойства являются более сложными, чем поля, поскольку могут выполнять дополнительную обработку перед изменением состояния — и, на самом деле, могут вообще не изменять состояния. Это получается благодаря тому, что они обладают двумя подобными функциям блоками, один из которых отвечает за получение значения свойства, а второй — за установку значения свойства. Эти блоки, также называемые средствами доступа (accessors), определяются с помощью, соответственно, ключевого слова get и ключевого слова set и могут применяться для управления уровнем доступа к свойству. Можно опускать тот или иной из них и тем самым создавать свойства, доступные только для записи или только для чтения (в частности, пропуск блока get позволяет обеспечивать доступ только для записи, а пропуск блока set — доступ только для чтения). Разумеется, это касается только внешнего кода, поскольку у кода внутри класса будет доступ к тем же данным, что и у этих блоков кода. Еще можно использовать со средствами доступа модификаторы доступности, т.е. делать, например, блок get общедоступным (public), а блок set — защищенным (protected). Для получения действительного свойства нужно обязательно включать хотя бы один из этих блоков (очевидно, что от свойства, которое нельзя ни считывать, не изменять, было бы мало толку). Базовая структура свойства состоит из стандартного ключевого слова, указывающего на уровень доступа (такого как public, private и т.д.), за которым идет имя типа, имя свойства и один или оба блока get и set, содержащие код обработки свойства: public int MylntProp { get { // Код для получения значения свойства. } set { // Код для установки значения свойства. } } Для именования общедоступных свойств в .NET тоже применяется схема PascalCasing, a не camelCasing, и потому здесь, как для полей и методов, используется именно она. Первая строка в определении свойства как раз и является тем небольшим фрагментом, который очень похож на определение поля. Отличие состоит в том, что в конце строки находится не точка с запятой, а блок кода, содержащий вложенные блоки get и set. В блоках get должно обязательно присутствовать возвращаемое значение типа свойства. Простые свойства часто ассоциируются с одним приватным полем, управляя доступом к нему, в случае чего блок get может возвращать значение поля и напрямую: // Поле, используемое свойством. private int my Int; // Свойство. public int MylntProp {
Глава 10. Определение членов класса 259 get { return mylnt; } set { // Код установки значения свойства. } } Код, находящийся за пределами класса, не сможет получать доступ к данному полю mylnt напрямую из-за того, что уровень доступности последнего указан как private. Вместо этого внешнему коду для получения доступа к этому полю придется использовать свойство. Функция set присваивает значение полю похожим образом. Здесь можно применять ключевое слово value для ссылки на значение, получаемое от пользователя свойства: // Поле, используемое свойством, private int mylnt; // Свойство. public int MylntProp { get { return mylnt; } set { mylnt = value; } } value равно значению того же типа, что и у свойства, так что если в свойстве используется тот же тип, что в поле, беспокоиться о приведении типов не нужно. Это простое свойство делает немного больше, чем просто защищает от прямого доступа к полю mylnt. Реальная мощь свойств проявляется тогда, когда осуществляется немного больше контроля над процессами. Например, блок set мог бы быть реализован и следующим образом: set { if (value > = 0 && value <= 10) mylnt = value; } Эта реализация предусматривает изменение mylnt только в случае присваивания свойству значения в диапазоне между 0 и 10. В ситуациях, подобных этой, требуется делать важный выбор касательно того, что должно происходить в случае использования недействительного значения. Существует четыре возможных варианта: □ ничего (как и в предыдущем коде); □ полю должно присваиваться значение по умолчанию; □ выполнение кода должно продолжаться так, будто ничего страшного не произошло, но с регистрацией события в журнале для последующего анализа причин его возникновения; □ должно генерироваться исключение.
260 Часть I. Язык С# В общем случае предпочтительными являются два последних варианта. Выбор того или иного зависит от того, каким образом будет использоваться класс и насколько много контроля должно предоставляться его пользователям. Генерация исключения предоставит пользователям довольно много контроля и позволит знать о том, что происходит, благодаря чему те смогут реагировать соответствующим образом. Применять в качестве такого исключения можно одно из стандартных исключений, которые доступны в пространстве имен System: set { if (value > = 0 && value <= 10) mylnt = value; else throw (new Агдшге^ОиГОгТгапдеЕхсер^опС "MylntProp", value, "MylntProp must be assigned a value between 0 and 10.")) ; // MylntProp может присваиваться только значение в диапазоне от 0 до 10 } Обработать его можно в коде, использующем свойство, с помощью логики try. . . catch. . . finally, о которой более подробно рассказывалось в главе 7. Регистрация данных, к примеру, в текстовом файле, может оказаться удобным вариантом, скажем, в производственном коде, где проблемы на самом деле не должны возникать. Такой вариант позволит разработчикам проверять показатели производительности и при необходимости даже отлаживать существующий код. В свойствах, как и в методах, могут использоваться ключевые слова virtual, override и abstract, что в случае полей совершенно невозможно. И, наконец, последней особенностью свойств является то, что блоки get и set в них могут иметь свои собственные уровни доступности, как показано ниже: // Поле, используемое свойством, private int mylnt; // Свойство. public int MylntProp { get { return mylnt; } protected set { mylnt = value; } } Здесь использовать блок set сможет только код, находящийся либо внутри самого класса, либо внутри его производных классов. То, какие уровни доступности могут применяться для блоков get и set, зависит от уровня доступности самого свойства: делать их доступнее того свойства, к которому они относятся, запрещено. Это означает, что в свойствах с уровнем доступности private не может использоваться вообще никаких модификаторов доступности для их блоков get и set, в то время как в свойствах с уровнем доступности public, наоборот, для блоков get и set могут задаваться все возможные модификаторы. В следующем практическом занятии предоставляется возможность поэкспериментировать с определением и использованием полей, методов и свойств.
Глава 10. Определение членов класса 261 Использование полей, методов и свойств 1. Создайте новое консольное приложение по имени ChlOExOl и сохраните его в каталоге С:\BegVCSharp\ChapterlO. 2. Добавьте в него новый класс по имени MyClass с помощью ссылки Add Class (Добавить класс), что приведет к определению этого нового класса в отдельном новом файле MyClass . cs. 3. Измените код в этом файле MyClass. cs следующим образом: public class MyClass { public readonly string Name; private int intVal; public int Val { get { return intVal; } set • { if (value > = 0 && value <= 10) intVal = value; else throw (new ArgumentOutOfRangeException("Val", value, "Val must be assigned a value between 0 and 10.")); // Val может присваиваться только значение в диапазоне от 0 до 10 } } public override string ToStringO { return "Name: " + Name + "\nVal: " + Val; } private MyClass () : this("Default Name") public MyClass(string newName) { Name = newName; intVal = 0; } } 4. Далее измените код в файле Program, cs следующим образом: static void Main(string [ ] args) { Console.WriteLine("Creating object myObj..."); // Создание объекта myObj.. . MyClass myObj = new MyClass("My Object"); Console.WriteLine("myObj created."); // Объект myObj создан for (int i = -1; i <= 0; i++)
262 Часть I. Язык С# try { Console.WriteLine("\nAttempting to assign {0} to myObj.Val...", i) ; // Попытка присвоить myObj.Val значение myObj.Val = i; Console.WriteLine("Value {0} assigned to myObj.Val.", myObj.Val); // Значение myObj.Val присвоено } catch (Exception e) { Console.WriteLine("Exception {0} thrown.", e.GetType().FullName); // Сгенерировано исключение Console.WriteLine("Message:\n\"{0}\"", e.Message); Console.WriteLine("\nOutputting myObj.ToString()..."); // Вывод myObj.ToString () ... Console.WriteLine(myObj.ToString ()); Console.WriteLine("myObj.ToString() Output."); Console.ReadKey(); } 5. Запустите приложение. На рис. 10.1 показан результат, который должен получиться. Рис. 10.1. Приложение Chi OExOl в действии Описание полученных результатов В коде внутри Main () создается и используется экземпляр класса MyClass, определенного в файле MyClass . cs. Создание экземпляра этого класса должно осуществляться с использованием конструктора не по умолчанию, потому что конструктор по умолчанию в MyClass является приватным: private MyClass () : this("Default Name") { } Использование this ("Default Name") гарантирует получение значения полем Name в случае вызова данного конструктора, что возможно при применении этого класса для создания нового производного класса. Это необходимо, поскольку не присваивание значения полю Name может позже стать источником ошибок. Используемый здесь конструктор не по умолчанию присваивает значения readonly-полю Name (значение которому может присваиваться только либо в объявлении поля, либо в конструкторе) и private-полю intVal.
Глава 10. Определение членов класса 263 Далее функция Main () делает две попытки по присваиванию значения свойству Val объекта myObj (являющегося экземпляром класса MyCalss). Цикл for служит в ней для присваивания значений -1 и 0 (за два прохода), а структура try. . .catch — для выполнения проверки на предмет того, не было ли сгенерировано какое-нибудь исключение. При присваивании свойству значения -1 выдается исключение типа System.ArgumentOutOfRangeException, и код в блоке catch выводит информацию об этом исключении в окне консоли. На следующей итерации цикла свойству Val успешно присваивается значение 0, которое далее через это свойство присваивается полю intVal. Напоследок для вывода отформатированной строки, представляющей содержимое объекта, применяется переопределенный метод ToString (): public override string ToString () { return "Name: " + Name + "\nVal: " + Val; } Этот метод должен обязательно объявляться с применением ключевого слова override, поскольку он переопределяет виртуальный метод ToString, определенный в базовом классе System.Object. В приведенном здесь коде используется непосредственно свойство Val, а не приватное (private) поле intVal. He существует причин, по которым свойства не следовало бы использовать в коде внутри классов подобным образом, хотя это и может немного сказываться на производительности (но настолько немного, что это вряд ли удастся заметить). Конечно, применение свойства также обеспечивает выполнение операции проверки на правильность, заложенной в самих свойствах, что тоже может быть очень выгодно для кода внутри класса. Добавление членов из диаграммы классов В предыдущей главе уже рассказывалось о том, как пользоваться диаграммой классов для изучения имеющихся в проекте классов. Там также говорилось о том, что диаграмму классов можно применять для добавления членов, и именно об этом пойдет речь в настоящем разделе. Диаграмма классов является средством среды VSue VCE не доступна. Все инструменты для добавления и редактирования членов отображаются в представлении Class Diagram (Диаграмма классов) внутри окна Class Details (Детали классов). Чтобы убедиться в этом, создайте диаграмму для класса MyClass из приложения ChlOExOl. Чтобы увидеть существующие члены, разверните представление этого класса в конструкторе классов (щелкнув на значке с изображением двух указывающих вниз стрелок). На рис. 10.2 показан внешний вид, который оно должно приобрести после этого. I MyCtam Class Й Fields ф intVal Ф Name =• Properties ЯГ Val i Methods J* MyClass V ToString Ш) (+ 1 ove... Puc. 10.2. Представление класса My CI ass в развернутом виде
264 Часть I. Язык С# Тогда при выборе класса MyClass в окне Class Details появится информация, показанная на рис. 10.3. Рис. 10.3. Окно Class Details при выборе класса MyClass На рисунке видно, что в этом окне будут отображаться как все определенные на текущий момент члены, так и пустые ячейки, позволяющие при желании добавить новых членов просто путем ввода их имени прямо в этих ячейках. Добавление методов Чтобы добавить в класс новый метод, достаточно просто ввести его имя в ячейке с меткой <add method> (добавить метод). После ввода имени метода с помощью клавиши <ТаЬ> можно перейти к настройке его следующих параметров, начиная с возвращаемого типа, уровня доступности, суммарной информации (которая впоследствии превращается в XML-документацию, о чем более подробно пойдет речь в главе 31), и заканчивая указанием того, должен ли этот метод скрываться в диаграмме классов. Когда метод уже добавлен, можно развернуть представляющий его узел и аналогичным образом добавить для него другие параметры. Что касается параметров, в их случае еще доступна возможность использовать модификаторы out, ref и pa rams. На рис. 10.4 показан пример добавления нового метода. Рис. 10.4. Добавление нового метода Вместе с этим новым методом в класс будет добавлен и следующий код: public double MyMethod(double paramX, double paramY) { throw new System.NotlmplementedException() ; } Остальные параметры метода можно будет настроить уже в окне Properties (Свойства), которое показано на рис. 10.5.
Глава 10. Определение членов класса 265 Iptopertsts MyMethod Method кры щ—— Ассея Name Type В Mtw Custom Attributes В M«i-wi«'i> Inheritance Modifier New Static В 'Ml ГЧмитгиМтп Remark} Returns Summary Co—on »~i~xl • -- -- .и,,, ,„„ , m public MyMethod double None False Fane Puc. 10.5. Окно Properties для нового метода Помимо всего прочего в этом окне метод можно сделать статическим. Очевидно, что такой подход не избавляет полностью от реализации метода, но зато обеспечивает базовой структурой и уж точно сокращает количество опечаток. Добавление свойств Добавление свойств осуществляется во многом аналогично. На рис. 10.6 показано новое свойство, добавленное в окне Class Details. ■н^ш^тьш^гмият V. ft MyCiess '« * MyMethod double | : ф 4S ToStrinp, string ♦ 5 ~ "Irm^^^m •jf vai int * j* intvai . int • 4 ufiTjfl'^JpSSaJfJiWf?! Д Class Details j"" ^^^^J^ public public pnvtt» public ™f^™ M,de И h ^ШГШ шш^, и Я , И jl Puc. 10.6. Добавление нового свойства Оно приведет к добавлению в класс такого кода: public int Mylnt { get { throw new System.NotlmplementedException(); } set { } Вам останется самостоятельно ввести лишь реализацию, а именно — отобразить данное свойство на поле, удалить блок set или get при желании сделать свойство доступным только для чтения или только для записи либо применить к ним модификаторы доступности. Базовая структура, однако, будет предоставлена автоматически.
266 Часть I. Язык С# Добавление полей Добавление полей выполняется так же легко. Нужно просто ввести имя поля, выбрать модификатор типа и доступа и все, базовая структура для продолжения готова. Рефакторизация членов Одним из приемов, который приходится очень кстати при добавлении свойств, является возможность генерировать свойство из поля. Она представляет собой пример рефакторизации (refactoring), под которой, в общем, подразумевается изменение кода не вручную, а с помощью какого-то инструмента. Осуществляться подобное может выполнением щелчка правой кнопкой на члене либо в диаграмме классов, либо в представлении кода. Возможности рефакторизации, предлагаемые в VCE, являются очень ограниченными и> к сожалению, не включают описываемой здесь возможности инкапсуляции полей. В этом отношении в VS имеется гораздо больше опций, чем в VCE. Например, если бы в классе MyClass присутствовало поле public string myString; тогда на нем можно было бы щелкнуть правой кнопкой мыши и выбрать в контекстном меню Refactors Encapsulate Field (Рефакторизация^Инкапсулировать поле). Это привело бы к отображению на экране диалогового окна Encapsulate Field (Инкапсуляция поля), показанного на рис. 10.7. Encapsulate F»ld Field name: myString Property name: Update references: • External О All S] Preview reference changes S Search In comments P] Search In strings \ZKJril Рис. 10.7. Диалоговое окно Encapsulate Field Принятие предлагаемых по умолчанию опций в этом окне завершилось бы изменением кода в классе MyClass следующим образом: private string myString; public string MyString get { return myString; } set { myString = value;
Глава 10. Определение членов класса 267 Здесь уровень доступности поля myString был изменен на private, а общедоступное свойство с именем MyString — создано и автоматически связано с полем myString. Очевидно, что сокращение времени, уходящего на монотонное создание свойств для полей, является большим плюсом. Автоматические свойства Свойства являются предпочтительным способом для получения доступа к состоянию объекта, поскольку они не позволяют никакому внешнему коду реализовать хранилище данных внутри объекта. Они также обеспечивают больший контроль над способом получения доступа к внутренним данным, как уже несколько раз демонстрировалось в коде настоящей главы. Однако обычно они определяются стандартным образом, т.е. в результате создания какого-то приватного члена, доступ к которому должен осуществляться непосредственно через общедоступное свойство. Необходимый для этого код выглядит практически так же, как код, который приводился в предыдущем разделе и был автоматически сгенерирован средством рефакторизации VS. Рефакторизация несомненно ускоряет дело в том, что касается ввода кода, но в С# есть еще одно припрятанное средство: автоматические свойства. Автоматическое свойство объявляется с помощью упрощенного синтаксиса, и компилятор С# заполняет все пробелы автоматически. В частности, компилятор объявляет приватное (private) поле, которое применяется для хранения, и использует его в блоках get и set внутри свойства, позволяя не беспокоиться о деталях. Структура кода, который нужно использовать для определения автоматического свойства, выглядит следующим образом: public int MylntProp { get; set; } Уровень доступности, тип и имя свойства определяются обычным образом, а вот реализации для блоков get и set внутри него не предоставляются. Реализации этих блоков (и лежащего в основе поля) генерируются самим компилятором. В случае применения автоматического свойства доступ к данным возможен только через это свойство, поскольку получать доступ к лежащему в основе приватному полю, не зная его имени (ведь оно будет определяться только во время компиляции), не получится. Однако назвать это ограничением нельзя, поскольку использование непосредственно имени самого свойства является вполне приемлемым вариантом. Единственное настоящее ограничение автоматических свойств состоит в том, что они обязательно должны включать и блок get, и блок set, т.е. определять подобным образом доступные только для чтения или только для записи свойства нельзя. Дополнительные вопросы, касающиеся членов классов Теперь, когда были рассмотрены все основные детали, касающиеся определения членов, пришла пора рассмотреть и более сложные связанные с членами вопросы. Поэтому в этом разделе речь пойдет о следующем. □ Как скрывать методы базового класса. □ Как вызывать переопределенные или скрытые методы базового класса. □ Как создавать вложенные определения типов.
268 Часть I. Язык С# Сокрытие методов базового класса При наследовании какого-нибудь (не абстрактного) члена базового класса, так же наследуется и его реализации. Если унаследованный член является виртуальным, тогда его реализацию можно легко переопределить с помощью ключевого слова override. Однако, помимо этого, независимо от того, является унаследованный член виртуальным или нет, при желании его реализацию еще можно и просто сокрыть. Это удобно в случае, например, когда общедоступный унаследованный член работает не совсем так, как хотелось бы. Делается это с использованием приблизительно такого кода: public class MyBaseClass { public void DoSomething() { // Базовая реализация. } } public class MyDerivedClass : MyBaseClass { public void DoSomething () { // Реализация в производном классе, скрывающая базовую реализацию. } } Хотя этот код и будет работать нормально, при его выполнении будет выдаваться предупреждение о сокрытии члена базового класса, дающее возможность исправить ситуацию, если член был скрыт по ошибке и на самом деле должен использоваться. При желании действительно скрыть член класса, можно использовать ключевое слово new для явного указания на то, что выполнение данной операции на самом деле является желаемым: public class MyDerivedClass : MyBaseClass { new public void DoSomething () { // Реализация в производном классе, скрывающая базовую реализацию. } } Этот код будет работать точно так же, но без выдачи предупреждения. Теперь не помешает разобраться с тем, в чем заключается разница между сокрытием и переопределением членов базового класса. Рассмотрим следующий код: public class MyBaseClass { public virtual void DoSomething () { Console.WriteLine("Base imp"); // Базовая реализация } } public class MyDerivedClass : MyBaseClass { public override void DoSomething () { Console .WriteLine ("Derived imp") ; // Производная реализация } }
Глава 10. Определение членов класса 269 Здесь реализация метода в базовом классе заменяется переопределением, в результате чего в следующем коде будет использоваться новая версия, хотя и через тип базового класса (за счет полиморфизма): MyDerivedClass myObj = new MyDerivedClass(); MyBaseClass myBaseObj; myBaseObj = myObj; myBaseObj.DoSomething (); Результат выполнения этого кода будет выглядеть следующим образом: Derived imp Производная реализация В качестве альтернативного варианта, метод базового класса может быть и просто скрыт, как показано ниже: public class MyBaseClass { public virtual void DoSomething () { Console.WriteLine ("Base imp"); // Базовая реализация } } public class MyDerivedClass : MyBaseClass { new public void DoSomething () { Console.WriteLine("Derived imp"); // Производная реализация } } Для того чтобы этот код работал, методу базового класса вовсе необязательно быть виртуальным, а эффект получается точно такой же плюс по сравнению с предыдущим кодом изменить необходимо всего лишь одну строку. Результат выполнения этого кода, как в случае виртуального, так и в случае не виртуального метода базового класса, будет выглядеть так: Base imp Хотя базовая реализация и скрыта, к ней все равно остается возможным получать доступ через базовый класс. Вызов переопределенных и скрытых методов базового класса Как при переопределении, так и при сокрытии члена базового класса, к нему все равно можно получать доступ из производного класса. Ситуаций, в которых это может быть полезно, существует много; примеры даны ниже. □ Это может быть полезно при желании скрыть какой-то унаследованный общедоступный член от пользователей производного класса, но при этом все равно сохранить доступ к его функциональным возможностям внутри данного класса. □ Это может быть полезно при желании добавить реализацию унаследованного виртуального класса вместо того, чтобы просто заменять ее новой переопределенной реализацией. Для достижения подобного применяется ключевое слово base, которое ссылается на реализацию базового класса, содержащуюся внутри производного класса (подобно
270 Часть I. Язык С# тому, как оно применяется для управления конструкторами, что демонстрировалось в предыдущей главе): public class MyBaseClass { public virtual void DoSomething() { // Базовая реализация. } } public class MyDerivedClass : MyBaseClass { public override void DoSomething () { // Реализация в производном классе, расширяющая базовую реализацию. base.DoSomething(); // Еще реализация в производном классе. } } Этот код выполняет версию DoSomething, которая содержится в MyBaseClass, являющимся базовым классом MyDerivedClass, из версии DoSomething, которая содержится в MyDerivedClass. Поскольку ключевое слово base работает с экземплярами объектов, его использование в статическом члене считается ошибкой. Ключевое слово this Помимо ключевого слова base в предыдущей главе также рассказывалось и о ключевом слове this. Как и base, ключевое слово this может применяться в членах класса и, подобно base, ссылается на экземпляр объекта, хотя и на текущий (т.е. использовать его в статических членах нельзя, поскольку статические члены не являются частью экземпляра объекта). Наиболее полезной функцией ключевого слова this является его способность передавать ссылку на текущий экземпляр объекта метода, как показано в следующем примере: public void doSomething () { MyTargetClass myObj = new MyTargetClass (); myObj.DoSomethingWith(this); } Здесь объект MyTargetClass, экземпляр которого создается, имеет метод по имени DoSomethingWith, который принимает один единственный параметр такого типа, который является совместимым с типом класса, содержащего предыдущий метод. То есть тип этого параметра может представлять собой либо тип данного класса, либо тип класса, производным от которого является данный класс, либо тип интерфейса, реализуемого данным классом, либо (конечно же) тип System.Object. Вложенные определения типов Типы вроде классов можно определять не только в пространствах имен, но и внутри других классов. Такой подход открывает возможность использовать для определения целый ряд модификаторов доступности, а не только public и internal, а также позволяет применять ключевое слово new для сокрытия определения типа, унаследованного от базового класса. Например, в следующем коде помимо класса MyClass также еще определяется и вложенный класс по имени myNestedClass:
Глава 10. Определение членов класса 271 public class MyClass { public class myNestedClass { public int nestedClassField; } } Для создания экземпляра myNestedClass за пределами myClass придется обязательно квалифицировать его имя, как показано ниже: MyClass.myNestedClass myObj = new MyClass.myNestedClass(); Однако выполнение подобной операции может оказаться и совершенно невозможным, в случае объявления вложенного класса с уровнем доступности private или другим уровнем, являющимся несовместимым с кодом на этапе выполнения такой операции создания экземпляра. Возможность создавать вложенные определения типов существует главным образом для того, чтобы позволять определять классы, являющиеся приватными по отношению к содержащему их классу, чтобы больше никакой другой код в пространстве имен не мог получать к ним доступ. Реализация интерфейсов Прежде чем продолжить, давайте подробнее остановимся на том, как определяются и реализуются интерфейсы. В предыдущей главе уже рассказывалось о том, что интерфейсы определяются подобно классам с использованием примерно такого кода: interface IMylnterface { // Члены интерфейса. } Члены интерфейсов тоже определяются подобно членам классов, но имеют несколько следующих важных отличий. □ При определении членов интерфейсов не разрешается использовать модификаторы доступа (ни public, ни private, ни protected, ни internal); все члены интерфейсов неявно являются общедоступными. 3 Члены интерфейсов не могут содержать тело кода. □ В интерфейсах не могут определяться члены типа полей. □ Члены интерфейсов не могут определяться с использованием таких ключевых слов, как static, virtual, abstract или sealed. □ В члены интерфейсов запрещено добавлять определения типов. Члены интерфейсов, однако, можно определять с использованием ключевого слова new при желании скрыть тех членов, которые наследуются от базовых интерфейсов: interface IMyBaselnterface { void DoSomething(); } interface IMyDerivedlnterface : IMyBaselnterface { new void DoSomething (); } Делается это тем же самым образом, как и сокрытие унаследованных членов класса.
272 Часть I. Язык С# В тех свойствах, которые определяются в интерфейсах, может определяться как один, так и оба блока get и set в зависимости от того, какой уровень доступа обеспечивается к свойству: interface IMylnterface { int Mylnt { get; set; } } Здесь у int-свойства Mylnt имеется оба блока— и get, и set. В случае свойства с более ограниченным доступом какой-то из них может быть опущен. Данный синтаксис напоминает синтаксис автоматических свойств, но важно не забывать о том, что автоматические свойства определяются только для классов, а не для интерфейсов, и что в их случае обязательным является наличие обоих блоков get и set. В интерфейсах не указывается, как должны храниться данные свойств. В них, например, нельзя указывать те поля, которые могут использоваться для хранения данных свойств. И, наконец, последнее: интерфейсы, подобно классам, могут определяться в виде членов классов (но не в виде членов других экземпляров, поскольку интерфейсы не могут содержать определений типов). Реализация интерфейсов в классах В классе, который реализует интерфейс, должны обязательно содержаться реализации для всех членов этого интерфейсов, которые, в свою очередь, должны обязательно соответствовать указанным сигнатурам (а также указанным блокам get и set) и являться общедоступными (public): public interface IMylnterface { void DoSomething (); void DoSomethingElse(); } public class MyClass : IMylnterface { public void DoSomething() { } public void DoSomethingElse () { } } Члены интерфейсов допускается реализовать с использованием ключевого слова virtual или abstract, но не ключевого слова static или constant. Члены интерфейсов могут также реализоваться в базовых классах: public interface IMylnterface { void DoSomething (); void DoSomethingElse (); }
Глава 10. Определение членов класса 273 public class MyBaseClass { public void DoSomething () { } } public class MyDerivedClass : MyBaseClass, IMylnterface { public void DoSomethingElse () { } } Наследование от базового класса, реализующего данный интерфейс, означает, что данный интерфейс также неявно поддерживается и производным классом. Например: public interface IMylnterface { void DoSomething (); void DoSomethingElse (); } public class MyBaseClass : IMylnterface { public virtual void DoSomething () { } public virtual void DoSomethingElse () { } } public class MyDerivedClass : MyBaseClass { public override void DoSomething () { } } Очевидно, что реализации в базовых классах лучше определять как виртуальные, чтобы в производных классах их можно было заменять, а не скрывать. Если бы пришлось скрывать член базового класса с использованием ключевого слова new, а не переопределять его, как было показано здесь, тогда метод IMylnterface.DoSomething всегда бы ссылался на ту версию, которая содержится в базовом классе, даже если бы доступ к производному классу осуществлялся через интерфейс. Явная реализация членов интерфейса Члены интерфейсов могут также реализоваться и явно самим классом. В случае применения такого подхода доступ к члену может осуществляться только через интерфейс, но не класс. Что касается членов, реализуемых неявно, которые демонстрировались в предыдущем разделе, то к ним доступ может осуществляться и тем, и другим способом. Например, если бы класс MyClass реализовал метод DoSomething интерфейса IMylnterface неявно, как это и было в предыдущем примере, тогда следующий код был бы допустимым: MyClass myObj = new MyClass (); myObj.DoSomething();
274 Часть I. Язык С# Такой код тоже был бы допустимым: MyClass myObj = new MyClassO; IMylnterface mylnt = myObj; mylnt.DoSomething() ; Если же MyDerivedClass реализовал бы DoSomething явно, тогда использовать бы можно было только второй прием. Необходимый для этого код выглядит следующим образом: public class MyClass : IMylnterface { void IMylnterface.DoSomething() { } public void DoSomethingElse () { } } Здесь метод DoSomething реализуется явно, а метод DoSomethingElse— неявно. Получать доступ непосредственно через экземпляр объекта myClass можно только к последнему. Добавление в свойства блоков get и set с уровнем доступности, отличным от public Ранее было сказано, что при реализации интерфейса со свойством нужно обязательно реализовать соответствующие блоки get и set. Это не совсем так: в свойство внутри класса, где определяющий это свойство интерфейс содержит только блок set, допускается добавлять блок get и наоборот. Подобное, однако, допускается только в случае использования для этого блока более ограничительного модификатора доступа, чем у того, что определен в интерфейсе. Из-за того, что блоки, определяемые в интерфейсе, изначально имеют уровень доступа public, получается, что добавлять можно только блоки с уровнем, отличным от public. Ниже приведен пример: public interface IMylnterface { int MylntProperty { get; } } public class MyBaseClass : IMylnterface { protected int mylnt; public int MylntProperty { get { return mylnt; } protected set { mylnt = value; } }
Глава 10. Определение членов класса 275 Частичные определения классов При создании классов с множеством членов одного или другого типа дело может довольно сильно усложняться, а файлы кода — становится очень длинными. Одним из приемов, который способен помогать в подобных случаях и который уже демонстрировался в предыдущих главах, является организации кода по разделам. Определение разделов в коде позволяет сворачивать и разворачивать их и тем самым делать код более удобным для восприятия. Например, определение класса может выглядеть следующим образом: public class MyClass { #region Fields private int mylnt; #endregion #region Constructor public MyClass () { mylnt = 99; } #endregion #region Properties public int Mylnt { get { return mylnt; } set { mylnt = value; } } #endregion #region Methods public void DoSomething () { // Выполнение каких-нибудь действий... } #endregion } Здесь можно разворачивать и сворачивать поля, свойства, конструктор и методы класса, концентрируя внимание только на том коде, который представляет интерес. Подобным образом могут также создаваться и вложенные разделы, т.е. такие, которые должны становиться видимыми только при разворачивании того раздела, внутри которого они содержатся. Однако даже в случае применения такого приема ситуация все равно может выйти из-под контроля. Поэтому существует еще один, альтернативный вариант и заключается он в использовании частичных определений классов. Проще говоря, он подразумевает разбиение определения класса на несколько файлов, помещая, например, определения полей, свойств и конструктора в один файл, а методов — в другой. Все, что для этого требуется— это просто использовать ключевое слово partial с классом в каждом файле, который содержит ту или иную часть его определения:
276 Часть I. Язык С# public partial class MyClass { } В случае применения частичных определений классов ключевое слово partial должно присутствовать в каждом из содержащих часть определения файлов. Частичные классы очень часто используются в Windows-приложениях для сокрытия кода, отвечающего за компоновку форм. На самом деле это уже демонстрировалось в главе 2. Код формы Windows в классе по имени Forml, например, хранился и файле Forml. cs, и в файле Forml. Designer. cs. Это позволяет концентрировать внимание на функциональных возможностях форм, не беспокоясь о дополнении кода не представляющей интерес информацией. Напоследок важно обратить внимание на еще одну касающуюся частичных классов деталь, а именно на то, что интерфейсы, применяемые к одной части частичного класса, применяются ко всему классу. Это значит, что определение: public partial class MyClass : IMylntefacel { } public partial class MyClass : IMyInteface2 { } будет эквивалентно определению: public class MyClass : IMylntefacel, IMyInteface2 { } To же самое касается и атрибутов, о которых более подробно будет рассказываться в главе 27. Частичные определения методов В частичных классах могут также определяться и частичные методы. Такие методы определяются в одном частичном определении класса без тела, а реализуются в другом частичном определении класса. И в том, и в другом месте с ними обязательно используется ключевое слово partial: public partial class MyClass partial void MyPartialMethod () ; public partial class MyClass partial void MyPartialMethod() // Реализация метода } Частичные методы могут быть статическими, но всегда являются приватными и не могут иметь возвращаемого значения. Любые используемые в них параметры не могут быть параметрами out, хотя и могут быть параметрами ref. С ними не может
Глава 10. Определение членов класса 277 применяться ни один из следующих модификаторов: virtual, abstract, override, new, sealed и extern. Из-за перечисленных ограничений сразу может и не быть понятно, какой цели служат частичные методы. На самом деле они играют важную роль не при использовании, а при компиляции кода. Возьмем следующий код: public partial class MyClass { partial void DoSomethingElse() ; public void DoSomething () { Console.WriteLine("DoSomething() execution started."); // Метод DoSome thing () начал выполняться DoSomethingElse(); Console.WriteLine("DoSomething() execution finished."); // Метод DoSome thing () завершен } } public partial class MyClass { partial void DoSomethingElse () { Console.WriteLine("DoSomethingElse() called."); // Вызван метод DoSomethingElse () } } Здесь частичный метод DoSomethingElse определяется и вызывается в первом определении частичного класса, а реализуется — во втором. В таком случае вывод при вызове DoSomething из консольного приложения будет таким, как и ожидалось: DoSomethingO execution started. DoSomethingElse() called. DoSomethingO execution finished. В случае удаления второго определения частичного класса или вообще всей реализации частичного метода (или превращения этого кода в комментарии), однако, вывод будет выглядеть следующим образом: DoSomethingO execution started. DoSomethingO execution finished. Вы можете предположить, что здесь происходит следующее: при вызове метода DoSomethingElse исполняющая среда обнаруживает, что у этого метода нет реализации, и потому переходит к выполнению следующей строки кода. На самом деле происходит нечто более изощренное. При компиляции кода, в котором содержится определение частичного метода без реализации, компилятор фактически вообще удаляет весь этот метод целиком. Он также удаляет и все связанные с этим методом вызовы. В результате при выполнении кода никакой проверки не предмет наличия реализации не делается, поскольку нет никакого вызова, который бы запрашивал такую проверку. Все это приводит к небольшому, но, тем не менее, существенному, улучшению производительности. Как и частичные классы, частичные методы играют полезную роль в настройке генерируемого автоматически или создаваемого конструктором кода. Конструктор может объявлять частичные методы, которые разработчик может как реализовать, так и не реализовать, в зависимости от ситуации. Если он выбирает не реализовать их, на
278 Часть I. Язык С# производительности это никак не сказывается, потому что в скомпилированном коде такого метода все равно, по сути, уже не существует. А теперь подумайте о том, почему у частичных методов не может быть возвращаемого типа? Если вы сможете ответить на этот вопрос, значит, вы полностью разобрались в данной теме; именно потому вопрос оставлен для самостоятельной проработки. Пример приложения Для иллюстрации некоторых из приведенных выше приемов в настоящем разделе описывается процесс создания модуля класса, который будет использоваться в качестве основы и в последующих главах. В состав этого модуля входит два класса. □ Класс Card, представляющий стандартную игральную карту трефовой, бубновой, червовой или пиковой масти достоинством от туза до короля. □ Класс Deck, представляющий полную колоду из 52 карт и предусматривающий осуществление доступа к картам по их позиции в колоде, а также обладающий возможностью тасования колоды. Помимо этого в настоящем разделе еще также демонстрируется процесс создания и простого клиента для получения уверенности в том, что в модуле все действительно работает, но то, как данный модуль можно применять в полнофункциональном карточном приложении, пока не показывается. Продумывание приложения В библиотеке классов данного приложения, которая будет, кстати, называться ChlOCardLib, должны содержаться классы. Прежде чем приступать к написанию кода, однако, нужно сначала продумать структуру и функциональные возможности этих классов. Класс Card Класс Card, по сути, является контейнером для двух доступных только для чтения полей: suit (масть) и rank (достоинство). Причина, по которой эти поля должны быть доступными только для чтения, состоит в том, что в наличии "пустой" карты нет никакого смысла, и возможность изменять карты после их создания не требуется. Для упрощения данного процесса нужно будет сделать используемый по умолчанию конструктор приватным, а также предоставить альтернативный конструктор, способный создавать карту указываемой масти и достоинства. Помимо этого, в классе Card еще потребуется переопределить метод ToString объекта SystemObject, чтобы легко получать представляющую карту строку в понятном для человека виде. Чтобы немного упростить дело, для полей suit и rank потребуется предоставить перечисления. Таким образом, получается, что структура класса Card должна выглядеть так, как показано на рис. 10.8. Card +suit +rank +ToString() Рис. 10.8. Структура класса Card
Глава 10. Определение членов класса 279 Класс Deck Класс Deck должен обслуживать 52 объекта Card. Для этого вполне подойдет и простой массив. Напрямую получать доступ к этому массиву будет невозможно, потому что доступ к объекту Card должен осуществляться через метод Get Card, возвращающий объект Card с заданным индексом. Помимо этого в классе Deck должен еще также присутствовать метод Shuffle для тасования карт в массиве. Таким образом, получается, что структура класса Deck должна выглядеть так, как показано на рис. 10.9. 1 Card ( +suit h + rank +ToString() Deck -cards : Card[] +GetCard() + Deck() +Shuffle() )...* Рис. 10.9. Структура класса Deck Написание библиотеки классов В этом примере предполагается, что вы уже достаточно хорошо знакомы с IDE-средой, чтобы не нуждаться в практических занятиях, так что явным образом шаги здесь не перечисляются, поскольку они ничем не отличаются от тех, что уже не раз доводилось использовать. Здесь все основное внимание уделяется детальному рассмотрению кода, хотя некоторые указания все же встречаются для исключения вероятности возникновения каких-нибудь проблем. Все классы и перечисления должны содержаться в проекте типа библиотеки классов по имени ChlOCardLib. Таким образом, получается, что в этом проекте всего должно быть" четыре файла . cs, а именно: Card.cs с определением класса Card, Deck.cs с определением класса Deck, а также Suit.cs и Rank.cs с определениями соответствующих перечислений. Большую часть этого кода можно собрать с помощью входящего в состав VS инструмента построения диаграмм классов. Тем, кто использует VCE, а потому не имеет доступа к инструменту построения диаграмм, не стоит беспокоиться. В каждом из следующих разделов будет также приводиться и весь генерируемый этим инструментом код, благодаря чему они тоже смогут спокойно следить за происходящим процессом. В данном проекте никаких отличий в коде между двумя этими IDE-средами нет. Для начала необходимо выполнить описанные ниже действия. 1. Создать новый проект типа библиотеки классов, присвоить ему имя ChlOCardLib и сохранить в каталоге С: \BegVCSharp\ChapterlO. 2. Удалить из этого проекта файл Classl.cs.
280 Часть I. Язык С# 3. Если используется среда VS, открыть диаграмму классов для этого проекта с помощью окна Solution Explorer (для того, чтобы появился значок диаграммы классов, выбран должен быть сам проект, а не решение). Вначале диаграмма классов должна быть пустой, поскольку в данном проекте пока еще не содержится никаких классов. В случае отображения в этом представлении классов Resources и Settings, их можно скрыть, выполнив на каждом из них щелчок правой кнопкой мыши и выбрав в контекстном меню пункт Remove from Diagram (Удалить из диаграммы). Добавление перечислений Suit и Rank Добавить перечисление в диаграмму классов можно, перетащив в нее из панели Toolbox (Панель управления) элемент Enum (Перечисление) и заполнив соответствующим образом поля в диалоговом окне, которое появится после этого. Например, для перечисления Suit его следует заполнить так, как показано на рис. 10.10. Рис. 10.10. Добавление перечисления Епит Далее необходимо добавить члены перечисления в окне Class Details (Детали класса). Требуемые значения показаны на рис. 10.11. |CM.b*t,*k J if [ Name •' •«Л Diamond itf Heart *!* ,.j e/ Spade «/-'-; "^ rrj Output j value Class Details 1 Summary Hide Puc. 10.11. Добавление членов перечисления En um Затем точно таким же образом добавляется из панели Toolbox перечисление Rank. Значения перечисления Rank показаны на рис. 10.12. :' Ц от fetalis - Rank • Т77ШШШ Deuce J Four •/»Sta n/* Sevtn «Г Eight .if Nine ufUn ..." lack oj" Queen •if King put ^Jpr*»kpplnt$ , Value ■kSl -3 Class Details: muni Summary ■П Hide Puc. 10.12. Добавление членов перечисления Rank
Глава 10. Определение членов класса 281 Значением первого члена назначается Асе (Туз) для того, чтобы при сохранении данных в перечислении соблюдалось соответствие с достоинством карты, т.е. чтобы, например, карта Six (Шестерка) сохранялась как 6. По завершении диаграмма должна иметь вид, показанный на рис. 10.13. Код, сгенерированный для этих двух перечислений в файлах Suit.cs и Rank.cs, будет выглядеть следующим образом: using System; using System.Collections.Generic; using System.Text; namespace ChlOCardLib { public enum Suit { Club, Diamond, Heart, Spade, } } using System; using System.Collections .Generic- using System.Text; namespace ChlOCardLib { public enum Rank { Ace = 1, Deuce, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, } } Те, кто использует VCE, могут добавить этот код вручную просто путем добавления самих файлов Suit. cs и Rank. cs, а затем ввода в них приведенного выше кода. В этом коде стоит обратить внимание, что дополнительные команды, добавленные генератором кода после последнего члена перечисления, не будут приводить к созданию дополнительного "пустого" члена (хотя они действительно являются немного неаккуратными). Рис. 10.13. Диаграмма классов после добавления перечислений Suit и Rank
282 Часть I. Язык С# Добавление класса Card В этом разделе описан процесс добавления класса Card как комбинированным применением конструктора классов и редактора кода в VS, так с использованием только редактора кода в VCE. Добавление класса в конструкторе классов осуществляется во многом так же, как и добавление перечисления, а именно — за счет перетаскивания соответствующего элемента из панели Toolbox в диаграмму. В данном случае нужно перетащить в диаграмму элемент Class и присвоить ему имя Card. Далее следует в окне Class Details добавить поля rank и suit, а в окне Properties установить для свойства Constant Kind каждого из этих полей значение readonly. Также нужно добавить два конструктора: используемый по умолчанию (с уровнем доступности private) и принимающий два параметра— newSuit и newRank, соответственно, типа Suit и Rank (с уровнем доступности public). И, наконец, последнее, что следует сделать— переопределить метод ToSTring путем изменения значения свойства Inheritance Modifier в окне Properties на override. На рис. 10.14 показано окно Class Details и класс Card со всей введенной информацией. Рис. 10.14. Окно Class Details после ввода информации, необходимой для класса Card Затем необходимо изменить код класса Card в файле Card.cs следующим образом (или добавить показанный ниже код в новый класс по имени Card в пространстве имен ChlOCardLib, если дело происходит в среде VCE): public class Card { public readonly Suit suit; public readonly Rank rank; public Card(Suit newSuit, Rank newRank) suit = newSuit; rank = newRank; private Card() public override string ToStringO return "The " + rank + " of " + suit + "s"; }
Глава 10. Определение членов класса 283 Здесь переопределенный метод ToString записывает строковое представление хранящегося значения перечисления в возвращаемую строку, а конструктор не по умолчанию инициализирует значения полей suit и rank. Добавление класса Deck Для класса Deck с помощью диаграммы классов необходимо определить следующие члены. □ Приватное поле с именем cards и типом Card [ ]. □ Общедоступный конструктор, который должен использоваться по умолчанию. □ Общедоступный метод с именем GetCard, принимающий один параметр типа int по имени cardNumb и возвращающий объект типа Card. □ Общедоступный метод с именем Shuffle, не принимающий никаких параметров и возвращающий void. После добавления этих членов класс Deck в окне Class Details должен выглядеть так, как показано на рис. 10.15. Рис. 10.15. Окно Class Details после ввода информации, необходимой для класса Deck Чтобы сделать картину в диаграмме еще более ясной, можно отобразить отношения между добавленными членами и типами. Для этого нужно щелкнуть правой кнопкой мыши на каждом члене в диаграмме классов по очереди и выбрать в контекстном меню пункт Show as Association (Показать как ассоциацию): □ cards в Deck □ suit в Card □ rank в Card По завершении диаграмма должна приобрести вид, показанный на рис. 10.16. Далее нужно изменить код в файле Deck. cs так, как показано в приведенном ниже коде (или, если используется среда VCE, сначала добавить новый соответствующий класс и ввести в нем приведенный здесь код). В этом коде первым делом реализуется конструктор, который просто создает и присваивает 52 карты полю cards. Для этого осуществляется проход по всем возможным комбинациям значений из двух перечислений с использованием каждой для создания карты, что в результате приводит к тому, что в cards первоначально содержится упорядоченный список карт:
284 Часть I. Язык С# D«ek Class : tf Methods ♦ Deck ♦ GetCard ♦ Shuffle (*) & cards * Methods ♦ Card (♦ 1 overload) A* ToStnng ♦ rank * Ace Deuce Three tow F,ve Sot Eight Ten Jack Queen King ♦ suit Рис. 10.16. Диаграмма классов после отображения отношений using System; using System.Collections.Generic; using System.Text; namespace ChlOCardLib { public class Deck { private Card[] cards; public Deck() { cards = new Card [52] ; for (int suitVal = 0; suitVal < 4; suitVal++) { for (int rankVal = 1; rankVal < 14; rankVal++) { cards [suitVal * 13 + rankVal -1] = new Card((Suit) suitVal, (Rank) rankVal); } } } Затем реализуется метод GetCard, который либо возвращает объект Card, соответствующий запрашиваемому индексу, либо генерирует исключение, как уже показывалось ранее: public Card GetCard(int cardNum) { if (cardNum > = 0 && cardNum <= 51) return cards [cardNum] ; else throw (new System.ArgumentOutOfRangeException ("cardNum" , cardNum, "Value must be between 0 and 51.")) ;
Глава 10. Определение членов класса 285 И, наконец, последним реализуется метод Shuffle. Этот метод делает следующее: создает временный массив карт и произвольным образом копирует в него карты из существующего массива cards. Основным телом в нем является цикл, выполняющий итерации от 0 до 51. На каждой итерации этого цикла генерируется произвольное число от 0 до 51 с помощью экземпляра класса System.Random из .NET Framework. После создания экземпляра объект этого класса генерирует случайное число от 0 до X с применением метода Next (X). Когда число готово, оно просто используется в качестве индекса того объекта Card во временном массиве, в который должна быть скопирована карта из массива cards. Для фиксирования информации об уже присвоенных картах создается массив переменных bool, которым по мере копирования карт присваивается значение true. Во время генерации случайных чисел осуществляется сверка с этим массивом для выяснения того, не была ли уже скопирована какая-то карта в место во временном массиве, на которое указывает сгенерированное случайное число. Если была, тогда просто генерируется очередное случайное число. Такой подход нельзя назвать самым эффективным, поскольку до обнаружения свободного места для копирования карты может быть сгенерировано немало случайных чисел. Однако он работает, является очень простым и позволяет коду С# выполняться настолько быстро, что заметить задержку вряд ли удастся. Выглядит код метода Shuffle следующим образом: public void Shuffle () { Card[] newDeck = new Card[52]; bool[] assigned = new bool [52] ; Random sourceGen = new Random () ; for (int i = 0; i < 52; i++) { int destCard = 0; bool foundCard = falser- while (foundCard = false) { destCard = sourceGen.NextE2); if (assigned[destCard] == false) foundCard = true; } assigned[destCard] = true; newDeck[destCard] = cards[i]; } newDeck.CopyTo(cards, 0) ; } } } В последней строке в коде этого метода используется метод СоруТо класса System. Array (применяемого всякий раз, когда создается массив) для копирования каждой карты из newDeck в cards. Это означает, что вместо создания новых экземпляров используется один и тот же набор объектов Card из одного и того же объекта cards. В случае применения вместо этого cards = newDeck экземпляр объекта, на который ссылается cards, должен был бы заменяться другим, что обязательно приводило бы к появлению проблем при наличии еще где-нибудь в коде ссылки на исходный экземпляр cards, который бы так и оставался не перетасованным. На этом создание кода библиотеки классов завершено.
286 Часть I. Язык С# Клиентское приложение для библиотеки классов Чтобы ничего не усложнять, можно добавить в содержащее библиотеку классов решение клиентское консольное приложение. Для этого щелкните правой кнопкой мыши на этом решении в окне Solution Explorer и выберите в контекстном меню пункт Add^New Project (Добавить^Новый проект). Называться новый проект должен ChlOCardClient. Для использования созданной ранее библиотеки классов в новом проекте консольного приложения добавьте в него ссылку на проект этой библиотеки классов, который называется ChlOCardLib. После создания проекта консольного приложения сделать это можно с помощью диалогового окна Add Reference (Добавление ссылки) и, в частности, доступной в нем вкладки Projects (Проекты), как показано на рис. 10.17. Рис. 10.17. Добавление ссылки на проект библиотеки классов с помощью вкладки Projects диалогового окна Add Reference На этой вкладке нужно выделить проект и щелкнуть на кнопке ОК, после чего ссылка будет добавлена. Поскольку новый проект был создан вторым, потребуется указать, что именно он является стартовым проектом в решении, т.е. должен запускаться при выполнении щелчка на кнопке Run (Выполнить). Для этого щелкните правой кнопкой мыши на имени этого проекта в окне Solution Explorer и выберите в контекстном меню пункт Set as Startup Project (Сделать стартовым проектом). Далее следует добавить код, использующий новые классы. Ничего особенного для этого не требуется, поэтому вполне подойдет и код, показанный ниже: using System; using System.Collections.Generic; using System.Linq; using System.Text; using ChlOCardLib; namespace ChlOCardClient { class Classl { static void Main(string[] args) { Deck myDeck = new Deck () ;
Глава 10. Определение членов класса 287 myDeck.Shuffle(); for (int i = 0; i < 52; i++) { Card tempCard = myDeck.GetCard(i) ; Console.Write(tempCard.ToString()); if (i != 51) Console.Write(", "); else Console. WriteLine(); } Console.ReadKey(); } } } На рис. 10.18 можно видеть результат. Рис. 10.18. Клиентское консольное приложение Chi OCardClien t в действии Этим результатом, как видно на рисунке, является тасование 52 игральных карт в колоде. В последующих главах вы продолжите разрабатывать и использовать данную библиотеку классов. Резюме В настоящей главе завершено рассмотрение способов определения базовых классов. Разумеется, еще осталось рассказать очень о многом, но те приемы, которые были описаны здесь, уже позволят создавать довольно сложные приложения. В этой главе были продемонстрированы как способы определения полей, методов и свойств, вместе с различными уровнями доступа и ключевыми словами-модификаторами, так и инструменты, которыми можно пользоваться для сборки структуры всего класса за вполовину меньшее время. Еще здесь детально рассматривалось поведение, связанное с наследованием, и, в частности, то, как можно скрывать ненужных унаследованных членов с помощью ключевого слова new и расширять члены базового класса вместо того, чтобы заменять их реализацию, с помощью ключевого слова base. Кроме того, здесь также рассказывалось об определении вложенных классов, об определении и реализации интерфейсов, как явно, так и неявно, и о том, как разбивать определения на несколько файлов кода за счет использования частичных классов и частичных методов. Напоследок для примера было приведено описание процесса разработки и применения простой библиотеки классов, представляющей колоду игральных карт, с использованием очень удобного инструмента Class Diagram (Диаграмма классов), который значительно упрощает дело.
288 Часть I. Язык С# Данная библиотека будет еще не раз применяться и в последующих главах. В частности, в настоящей главе были освещены следующие вопросы. □ Способы определения полей, методов и свойств. □ Средства, доступные для организации класса по разделам. □ Дополнительные возможности, связанные с наследованием. □ Способы определения и реализации интерфейсов. □ Способы определения частичных классов и частичных методов. □ Процесс создания и развертывания простой библиотеки классов. В следующей главе речь пойдет о другом типе классов, а именно — о коллекциях, которые часто требуется применять при разработке приложений. Упражнения 1. Напишите код, определяющий базовый класс MyClass и виртуальный метод Get String. Этот метод должен возвращать строку, хранящуюся в защищенном поле myString, доступ к которому возможен только через предназначенное для записи public-свойство ContainedString. 2. Создайте производный от MyClass класс MyDerivedClass. Переопределите в нем метод Getstring так, чтобы он возвращал строку из базового класса путем использования базовой реализации данного метода, но при этом прибавлял к ней текст " (output from derived class) " (вывод, полученный от производного класса). 3. В определениях частичных методов должен обязательно использоваться только возвращаемый тип void. Объясните почему. 4. Напишите класс по имени MyCopyableClass, способный возвращать копию самого себя за счет применения метода GetCopy. Этот метод должен обязательно использовать метод MemberwiseClone, унаследованный от System.Object. Добавьте в этот класс простое свойство и напишите использующий этот класс клиентский код, удостоверившись в том, что он работает. 5. Напишите клиентское консольное приложение для библиотеки ChlOCardLib, вытягивающее сразу пять карт из перетасованной колоды (объекта Deck). В случае если все пять карт относятся к одной масти, это клиентское приложение должно отображать их названия на экране в месте с текстом "Flush!" (Флэш!); в противном случае оно должно прекращать свою работу после просмотра 50 карт вместе с текстом "No flush" (Нет флэша).
Коллекции, сравнения и преобразования Все основные приемы ООП, которыми можно пользоваться в С#, уже были рассмотрены, но существует еще несколько более сложных приемов, с которыми тоже стоит ознакомиться. Поэтому в настоящей главе рассматриваются следующие темы. □ Коллекции. Коллекции позволяют обслуживать группы объектов. В отличие от массивов, применение которых демонстрировалось в предыдущих главах, коллекции могут предоставлять более совершенную функциональность, вроде возможности управлять доступом к содержащимся в них объектам, возможности выполнять по ним поиск, сортировать их и т.д. Здесь вы узнаете, как использовать и создавать классы коллекций, а также ознакомитесь с некоторыми мощными приемами для извлечения из таких классов максимальной пользы. □ Сравнения. При работе с объектами часто требуется выполнять сравнение между ними. Это особенно важно в коллекциях, поскольку именно таким образом в них обеспечивается возможность выполнять сортировку. Здесь вы увидите, как сравнивать объекты разными способами, в том числе и путем перегрузки операций, а также как использовать интерфейсы IComparable и IComparer для сортировки коллекций. □ Преобразования. В предыдущих главах уже показывалось, как преобразовывать объекты из одного типа в другой. В этой главе вы узнаете, как настраивать операции по преобразованию типов так, чтобы они отвечали существующим потребностям. Коллекции В главе 5 рассказывалось о том, как для создания типов переменных, содержащих ряд объектов или значений, использовать массивы. С массивами, однако, связаны свои ограничения. Наибольшее из них состоит в том, что после создания массивы
290 Часть I. Язык С# имеют фиксированный размер, так что добавлять новые элементы в конец существующего массива без создания нового нельзя. Это часто означает, что синтаксис, применяемый для манипулирования массивами, может становиться чрезмерно сложным. Приемы ООП позволяют создавать классы, способные выполняют большую часть из этих манипуляций внутренним образом, тем самым упрощая код, в котором используются списки элементов или массивы. Массивы в С# реализуются в виде экземпляров класса System. Array и являются всего лишь одним из типов, которые называются классами коллекций. Классы коллекции в целом применяются для обслуживания списков объектов и могут предоставлять больше функциональных возможностей, чем простые массивы. Большая часть из этих функциональных возможностей обеспечивается реализацией интерфейсов из пространства имен System.Collections, что стандартизирует синтаксис коллекций. В этом пространстве имен также содержатся и другие интересные вещи, вроде классов, которые реализуют эти интерфейсы другими, отличными от System. Array способами. Поскольку функциональные возможности коллекций (включая базовые функции, вроде получения доступа к элементам коллекции путем применения синтаксиса [индекс]) доступны через интерфейсы, допускается не только использовать базовые классы коллекций вроде System.Array, но и создавать свои собственные специализированные классы коллекций и настраивать их под потребности объектов, которые должны поставляться в виде коллекции. Одним из преимуществ такого подхода, как будет показано позже, является то, что специальные классы коллекций могут быть строго типизированными. То есть при извлечении элементов из такой коллекции, их не нужно приводить к правильному типу. Другое преимущество связано с возможностью предоставлять специализированные методы, например, какой-то быстрый способ для получения подмножеств элементов. Скажем, в примере с колодой карт это мог быть метод для быстрого получения всех элементов Card определенной масти. Базовые функциональные возможности для коллекций в пространстве имен System.Collections предоставляют несколько интерфейсов. □ I Enumerable — предоставляет возможность прогонять элементы в коллекции по циклу. □ ICollection — предоставляет возможность получать информацию о количестве элементов в коллекции и копировать элементы в простой тип-массив (унаследован от IEnumerable) . □ iList — предоставляет для коллекции список элементов вместе с возможностями для получения доступа к этим элементам и другими базовыми возможностями, имеющими отношение к спискам элементов (унаследован от IEnumerable и ICollection). □ IDictionary— похож на IList, но предоставляет список элементов, доступных по значению ключа, а не по индексу (унаследован от IEnumerable и ICollection). Класс System.Array реализует интерфейсы IList, ICollection и IEnumerable. Однако он не поддерживает некоторые из более совершенных функциональных возможностей IList и предоставляет список элементов с использованием фиксированного размера. Использование коллекций Один из классов в пространстве имен Systems.Collections, а именно— System. Collections .ArrayList, тоже реализует интерфейсы IList, ICollection и IEnumerable, но делает это более совершенным образом, чем класс System.Array.
Глава 11. Коллекции, сравнения и преобразования 291 Если массивы являются фиксированными по размеру (т.е. ни добавлять в них, ни удалять из них элементы нельзя), то этот класс может применяться для представления списков элементов разной длины. Чтобы разобраться, какие возможности открывает такая высокоэффективная коллекция, в следующем практическом занятии демонстрируется пример применения такого класса и простого массива. j|p^^^^j|[^Q Массивы или более совершенные коллекции 1. Создайте новый проект типа консольного приложения по имени ChllExOl и сохраните его в каталоге С: \BegVCSharp\Chapterll. 2. Добавьте в него три новых класса Animal (Животное), Cow (Корова) и Chicken (Курица), щелкнув правой кнопкой мыши в окне Solution Explorer и выбрав в контекстном меню пункт Add^Class (Добавить^Класс) для каждого добавляемого класса. 3. Измените код в файле Animal. cs следующим образом: namespace ChllExOl { public abstract class Animal { protected string name; public string Name { get { return name; } set { name = value; } public Animal() name = "The animal with no name"; // Животное без клички public Animal(string newName) name = newName; public void Feed() Console.WriteLine ("{0} has been fed.", name); // Вывод информации о кормлении } } 4. Измените код в файле Cow.cs следующим образом: namespace ChllExOl { public class Cow : Animal {
292 Часть I. Язык С# public void Milk() { Console.WriteLine("{0} has been milked.", name); // Вывод информации о доении } public Cow(string newName) : base(newName) { } } } 5. Измените код в файле Chicken. cs следующим образом: namespace ChllExOl { public class Chicken : Animal { public void LayEggO { Console.WriteLine ("{0} has laid an egg.", name); // Вывод информации о снесении яиц } public Chicken(string newName) : base(newName) { } } } 6. Измените код в файле Program, cs следующим образом: using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; namespace ChllExOl { class Program { static void Main(string [ ] args) { Console.WriteLine ("Create an Array type collection of Animal " + "objects and use it:") ; // Создание коллекции типа Array из объектов // Animal и ее использование Animal [] animalArray = new Animal [2] ; Cow myCowl = new Cow("Deirdre") ; animalArray [0] = myCowl; animalArray [1] = new Chicken ("Ken") ; foreach (Animal myAnimal in animalArray) { Console.WriteLine ("New {0} object added to Array collection, " + "Name = {1}", myAnimal.ToString() , myAnimal.Name) ; // В коллекцию Array добавлен новый объект } Console. WriteLine ("Array collection contains {0} objects.", // Вывод количества объектов в коллекции Array animalArray.Length) ;
Глава 11. Коллекции, сравнения и преобразования 293 animalArray[0].Feed(); ((Chicken)animalArray[1]).LayEgg(); Console.WriteLine(); Console.WriteLine("Create an ArrayList type collection of Animal " + "objects and use it:") ; ArrayList animalArrayList = new ArrayList () ; Cow myCow2 = new Cow("Hayley") ; animalArrayList.Add(myCow2); animalArrayList. Add (new Chicken ("Roy")) ; foreach (Animal myAnimal in animalArrayList) { Console. WriteLine ("New {0} object added to ArrayList collection," + " Name = {1}", myAnimal.ToString() , myAnimal.Name) ; } Console.WriteLine("ArrayList collection contains {0} objects.", animalArrayList.Count); ((Animal)animalArrayList[0]).Feed(); ((Chicken)animalArrayList[1]).LayEgg(); Console.WriteLine(); Console. WriteLine ("Additional manipulation of ArrayList:") ; // Дополнительное манипулирование коллекцией ArrayList animalArrayLi s t. RemoveAt @) ; ((Animal)animalArrayList[0]).Feed(); animalArrayList.AddRange (animalArray) ; ((Chicken)animalArrayList[2]).LayEgg(); Console.WriteLine("The animal called {0} is at index {1}.", myCowl.Name, animalArrayList.IndexOf(myCowl)); // Вывод животных и позиций myCowl.Name = "Janice"; Console.WriteLine("The animal is now called {0}.", ((Animal) animalArrayList [1]) .Name) ; // Вывод животных Console.ReadKey(); } } } 7. Запустите приложение. На рис. 11.1 показан результат, который должен получиться. ■ ni*:///C:/l*erVKariuAppDati/Lociiaefnporary Profects/Ch11Ex01/bin/D*bug/Ch11Ex01.EXE Рис. 11.1. Приложение ChllExOl в действии
294 Часть I. Язык С# Описание полученных результатов В этом примере сначала создаются две коллекции объектов: в первой используется класс System.Array (простой массив), а во второй— System.Collections.ArrayList. Обе коллекции состоят из объектов Animal, которые определяются в файле Animal.cs. Класс Animal является абстрактным, так что создавать его экземпляры нельзя, хотя иметь в коллекции элементы, являющиеся экземплярами классов Cow и Chicken, которые наследуются от класса Animal, можно. Достигается это применением полиморфизма, о котором говорилось в главе 8. После создания внутри метода Main () BClassl.cs над этими массивами выполняются различные манипуляции для отображения их характеристик и возможностей. Некоторые из выполняемых операций подходят как для коллекции Array, так и для коллекции ArrayList, хотя их синтаксис немного отличается. Однако есть и такие, выполнение которых возможно только в случае использования более усовершенствованного типа ArrayList. Давайте сначала рассмотрим похожие операции и сравним код и результаты для обоих типов коллекций. В первую очередь возьмем операцию создания коллекций. В случае простых массивов для того, чтобы использовать массив, его нужно обязательно инициализировать с фиксированным размером. В случае массива animalArray это делается с помощью стандартного синтаксиса, который демонстрировался в главе 5: Animal[] animalArray = new Animal[2]; Коллекции ArrayList, напротив, не нуждаются в указании размера при инициализации, что позволяет создавать список animalArrayList следующим образом: ArrayList animalArrayList = new ArrayList(); С этим классом могут использоваться еще два других конструктора. Первый позволяет копировать содержимое существующей коллекции в новый экземпляр, указывая существующую коллекцию в качестве параметра, а второй — задавать вместимость коллекции, тоже через параметр. Вместимость, заданная в виде значения int, определяет первоначальное количество элементов, которое может содержаться в коллекции. Абсолютной она, однако, не является, поскольку автоматически удваивается, если количество элементов в коллекции превышает это значение. В случае массивов ссылочных типов (вроде Animal и производных от Animal объектов) просто инициализация массива с размером не подразумевает инициализации содержащихся в нем объектов. Для использования конкретного элемента его сначала необходимо инициализировать, а это означает, что элементам нужно присвоить инициализированные объекты: Cow myCowl = new Cow("Deirdre"); animalArray[0] = myCowl; animalArray[1] = new Chicken("Ken"); В показанном выше коде это делается двумя способами: один раз присваиванием с использованием существующего объекта Cow, а второй раз созданием нового объекта Chicken. Главное отличие здесь состоит в том, что первый метод приводит к созданию ссылки на объект в массиве; этот факт используется чуть позже в коде. В случае коллекции ArrayList никаких существующих элементов нет, даже таких, которые бы ссылались на null. Это означает, что присвоить новые экземпляры индексам таким же образом, как был показан выше, нельзя. Поэтому для добавления новых элементов применяется такой метод объекта ArrayList, как Add (): Cow myCow2 = new Cow("Hayley") ; animalArrayList.Add(myCow2); animalArrayList.Add(new Chicken("Roy"));
Глава 11. Коллекции, сравнения и преобразования 295 Не считая немного отличающегося синтаксиса, подобным образом в коллекцию можно добавлять как новые, так и существующие объекты. После такого добавления элементов их можно перезаписывать с использованием синтаксиса, идентичного тому, что применяется для массивов: animalArrayList[0] = new Cow("Alma"); В данном примере это, однако, не делается. В главе 5 было показано, как структура f oreach может применяться для итерации по массиву. Подобное возможно потому, что класс System. Array реализует интерфейс IEnumerable, а единственный доступный в этом интерфейсе метод GetEnumerator () позволяет проходить в цикле по элементам коллекции. Более подробно об этом будет рассказываться позже в этой главе. Что касается данного примера, то здесь структура f oreach используется для вывода информации о каждом объекте Animal в массиве: foreach (Animal myAnimal in animalArray) { Console.WriteLine ("New {0} object added to Array collection, " + "Name = {1}", myAnimal.ToString(), myAnimal.Name); } Объект ArrayList тоже поддерживает интерфейс IEnumerable и может использоваться со структурой foreach. В этом случае синтаксис выглядит идентично: foreach (Animal myAnimal in animalArrayList) { Console.WriteLine("New {0} object added to ArrayList collection, " + "Name = {1}", myAnimal.ToString(), myAnimal.Name); } Далее для вывода информации о количестве элементов в массиве используется свойство Length: Console.WriteLine("Array collection contains {0} objects.", animalArray.Length); Такого же поведения можно добиться и в случае коллекции ArrayList, но только с помощью свойства Count, которое является частью интерфейса ICollection: Console.WriteLine("ArrayList collection contains {0} objects.", animalArrayList.Count); От коллекций, будь то простые массивы или более сложные коллекции, мало толку, если они не предоставляют доступа к элементам, которые им принадлежат. Простые массивы являются строго типизированными, т.е. обеспечивают прямой доступ к типу содержащихся в них элементов. Это означает, что методы их элементов можно вызывать напрямую: animalArray[0].Feed(); В данном случае типом массива является абстрактный класс Animal и поэтому вызывать напрямую методы, предоставляемые производными от него классами, нельзя. Вместо этого нужно использовать синтаксис приведения типов: ( (Chicken)animalArray[1]).LayEgg(); Коллекция ArrayList представляет собой коллекцию объектов System.Object (потому что объекты Animal были присвоены ей посредством полиморфизма). Это означает, что синтаксис приведения в ее случае нужно использовать для всех элементов: ( (Animal)animalArrayList[0]) .Feed(); ( (Chicken)animalArrayList[1]).LayEgg();
296 Часть I. Язык С# В остальной части кода иллюстрируются некоторые возможности коллекции ArrayList, которые выходят за рамки таковых в Array. Сначала из коллекции ArrayList можно удалять элементы путем применения методов Remove () и RemoveAt (), которые являются в классе ArrayList частью реализации интерфейса IList. Эти методы позволяют удалять элементы из массива, соответственно, либо на основании ссылки на удаляемый элемент, либо на базе его индекса. В данном примере используется последний метод для удаления первого элемента в списке, каковым является объект Cow, в свойстве Name которого содержится значение Hayley: animalArrayList.RemoveAt@) ; В качестве альтернативного варианта можно было бы применить и такую строку кода: animalArrayList.Remove(myCow2); потому что локальная ссылка на этот объект уже имеется (вместо того, чтобы создавать новый объект, в массив с помощью метода Add () была добавлена существующая ссылка). И в том, и в другом случае в коллекции остается один элемент— объект Chicken, к которому получается доступ следующим образом: ((Animal)animalArrayList[0]).Feed(); Любые изменения, вносимые в элементы в объекте ArrayList с оставлением в массиве N элементов, будут осуществляться так, чтобы индексы имели вид от 0 до N-1. Например, удаление элемента с индексом 0 приводит к сдвигу всех остальных элементов в массиве на одну позицию, из-за чего доступ к объекту Chicken осуществляется с использованием индекса 0, а не 1. Элемента с индексом 1 больше не существует (поскольку изначально этих элементов всего было два), так что использование такой строки, как показана ниже, привело бы к генерации исключения: ((Animal)animalArrayList[1]).Feed(); Еще коллекции ArrayList позволяют добавлять одновременно несколько элементов с помощью метода AddRange (). Этот метод принимает любой объект с интерфейсом ICollection, в том числе и массив animalArray, который был создан ранее в коде: animalArrayList.AddRange(animalArray); Чтобы удостовериться в его работоспособности, можно попробовать получить доступ к третьему элементу в коллекции, каковым будет второй элемент в animalArray: ( (Chicken) animalArrayList [2] ) .LayEggO ; Метод AddRange () не является частью предоставляемых ArrayList интерфейсов. Он принадлежит конкретно классу ArrayList и иллюстрирует тот факт, что в классах коллекций можно обеспечивать специализированное поведение, выходящее за рамки того, которого требуют уже рассмотренные интерфейсы. Этот класс предлагает и другие интересные методы, вроде InsertRange (), позволяющего вставлять массив объектов в любой точке в списке, и методов, позволяющих выполнять задачи наподобие сортировки и переупорядочивания массива. Напоследок иллюстрируются способы извлечения пользы из возможности иметь несколько ссылок на один и тот же объект. Использование метода IndexOf () (который является частью интерфейса IList) позволяет увидеть не только то, что myCowl (объект, который изначально добавлялся в animalArray) теперь является частью коллекции animalArrayList, но и то, как выглядит его индекс: Console.WriteLme("The animal called {0} is at index {1}.", myCowl.Name, animalArrayList.IndexOf(myCowl));
Глава 11. Коллекции, сравнения и преобразования 297 В качестве расширения данного кода, в двух следующих строках выполняется переименование объекта с помощью ссылки на объект и отображение нового имени на экране с помощью ссылки на коллекцию: myCowl.Name = "Janice"; Console.WriteLine("The animal is now called {0}.", ((Animal)animalArrayList[1]).Name); Определение коллекций Теперь, когда известно, какие возможности могут предоставлять усовершенствованные классы коллекций, пришла пора научиться создавать свои собственные строго типизированные коллекции. Одним из возможных способов является реализация всех требуемых методов вручную, но это может оказаться очень длинным и сложным процессом. Однако существует и альтернативный вариант, и состоит он в наследовании коллекции от класса, подобного System. Collections .CollectionBase, который является абстрактным классом и предоставляет большую часть реализации коллекции автоматически. Настоятельно рекомендуется использовать именно этот вариант. Класс CollectionBase поддерживает такие интерфейсы, как IEnumerable, ICollection и IList, но предоставляет лишь часть требуемой для них реализации, а именно — методы Clear () и RemoveAt () для интерфейса IList и свойство Count для интерфейса ICollection. Все остальное, при желании иметь соответствующие функциональные возможности, нужно реализовать самостоятельно. Для упрощения этого процесса CollectionBase предлагает два защищенных свойства, которые сами обеспечивают доступ к хранимым объектам, а именно— List, которое предоставляет доступ к элементам через интерфейс IList, и Inner List, которое является объектом ArrayList, используемым для хранения элементов. Например, базовые детали класса коллекции для хранения объектов Animal могли бы определяться следующим образом (более полная реализация будет показана чуть позже): public class Animals : CollectionBase { public void Add(Animal newAnimal) List.Add(newAnimal); public void Remove(Animal oldAnimal) List.Remove(oldAnimal); public Animals() } Здесь Add () и Remove () были определены как строго типизированные методы, применяющие для получения доступа к элементам стандартный метод Add () используемого интерфейса IList. Теперь они будут работать только с классами Animal или классами, производными от Animal, что отличается от демонстрировавшихся ранее реализаций ArrayList, где они могли работать с любым объектом. Класс CollectionBase позволяет использовать синтаксис foreach с производными коллекциями, т.е. например, такой код, как показан ниже:
298 Часть I. Язык С# Console.WriteLine("Using custom collection class Animals:"); // Использование специальной коллекции Animals Animals animalCollection = new Animals(); animalCollection.Add(new Cow("Sarah")); foreach (Animal myAnimal in animalCollection) { Console.WriteLine("New {0} object added to custom collection, " + "Name = {1}", myAnimal.ToString(), myAnimal.Name); } Однако делать так нельзя: animalCollection[0].Feed(); Для получения доступа к элементам через их индексы подобным образом, необходимо использовать индексатор. Индексаторы Индексатор (indexer) — это свойство особого вида, которое можно добавлять в класс для обеспечения подобного массивам доступа. На самом деле с помощью индексатора можно обеспечивать и гораздо более сложный доступ, поскольку с ним в квадратных скобках можно определять и использовать параметры любых сложных типов. Реализация простого числового индекса для элементов, однако, является наиболее распространенным способом его применения. Добавить индексатор в состоящую из объектов Animal коллекцию Animals можно следующим образом: public class Animals : CollectionBase { public Animal this[int animalIndex] { get { return (Animal)List[animalIndex]; } set { List[animallndex] = value; } } } Здесь используется ключевое слово this вместе со следующими далее в квадратных скобках параметрами, но весь остальной код выглядит во многом точно так же, как и код любого другого свойства. Такой синтаксис логичен, поскольку позволяет осуществлять доступ к индексатору с использованием имени объекта со следующим далее в квадратных скобках и указывающим на индекс параметром или параметрами (например, MyAnimals [0]). В следующем коде индексатор применяется на свойстве List (т.е. на интерфейсе IList, который обеспечивает в CollectionBase доступ к ArrayList, в котором и хранятся элементы): return (Animal)List[animallndex]; Явное приведение здесь является обязательным, поскольку свойство IList.List возвращает объект System.Object. Здесь важно обратить внимание на то, что тип определяется для индексатора. Именно он будет получаться при доступе к элементу с помощью данного индексатора.
Глава 11. Коллекции, сравнения и преобразования 299 Это означает, что можно использовать такую строку кода: animalCollection[0].Feed(); вместо такой: ((Animal)animalCollection[0]).Feed(); Это является еще одной полезной возможностью строго типизированных специальных коллекций. В следующем практическом занятии предыдущий пример расширяется, чтобы позволить испытать все это на практике Практическое занятие! Реализация коллекции Animals 1. Создайте новое консольное приложение по имени ChllEx02 и сохраните его в каталоге C:\BegVCSharp\Chapterll. 2. Щелкните правой кнопкой мыши на имени этого проекта в окне Solution Explorer и выберите в контекстном меню пункт Add^Add Existing Item (Добавить^ Добавить существующий элемент). 3. Выберите из каталога C:\BegVCSharp\Chapterll\ChllEx01\ChllEx01 файлы Animal. cs, Cow. cs и Chicken. cs и щелкните на кнопке Add (Добавить). 4. Измените объявление пространства имен в каждом из трех добавленных файлов следующим образом: namespace ChllEx02 5. Добавьте новый класс с именем Animals. 6. Измените код в файле Animals. cs следующим образом: using System; using System.Collections; using System.Collections .Generic- using System.Linq; using System.Text; #endregion namespace ChllEx02 { public class Animals : CollectionBase { public void Add (Animal newAnimal) List.Add(newAnimal); public void Remove (Animal newAnimal) List.Remove(newAnimal); public Animals () public Animal this[int animallndex] { get { return (Animal)List[animallndex]; )
300 Часть I. Язык С# set { List[animalIndex] = value; } } } } 7. Измените код в файле Program.cs следующим образом: static void Main(string[] args) { Animals animalCollection = new Animals () ; animalCollection.Add(new Cow("Jack")) ; animalCollection. Add (new Chicken ("Vera")) ; foreach (Animal myAnimal in animalCollection) { myAnimal.Feed(); } Console.ReadKey(); } 8. Запустите приложение. На рис. 11.2 показан результат, который должен получиться. иж \ А \ Рис. 11.2. Приложение СЫ1Ех02 в действии Описание полученных результатов В этом примере код, описанный в предыдущем примере, используется для реализации строго типизированной коллекции объектов Animal в классе по имени Animals. В коде внутри Main () просто создается экземпляр объекта Animals под названием animalCollection, добавляется два элемента (представляющие собой экземпляры Cow и Chicken) и применяется цикл foreach для вызова метода Feed (), который оба объекта наследуют от своего базового класса Animal. Добавление коллекции Cards в CardLib В предыдущей главе вы создавали проект библиотеки классов ChlOCardLib, который состоял из класса Card, представлявшего игральную карту, и класса Deck, представлявшего колоду карт, т.е. из коллекции классов Card. Там эта коллекция была реализована в виде простого массива. В этой главе вы переименуете эту библиотеку в ChllCardLib и добавите в нее новый класс. Этот новый класс будет носить имя Cards и является специальной коллекцией объектов Card, предоставляющей все описанные ранее преимущества. Чтобы сделать это, сначала создайте новый проект библиотеки классов по имени ChllCardLib, сохраните его в каталоге С: \BegVCSharp\Chapterll, выберите в меню Project (Проект) пункт Add Existing Item (Добавить существующий элемент), отыщите в каталоге C:\BegVCSharp\ChapterlO\ChlOCardLib\ChlOCardLib файлы Card.cs, Deck.cs, Suit .cs и Rank.cs и добавьте их в проект ChllCardLib. Как и в предыдущей версии данного проекта (см. главу 10), шаги для внесения изменений приводятся здесь
Глава 11. Коллекции, сравнения и преобразования 301 не в стандартном формате практического занятия. При желании можете открыть версию данного проекта, входящую в состав загружаемого кода для этой главы. Не забудьте, что при копировании исходных файлов из ChlOCardLib в ChllCardLib нужно обязательно изменить объявления пространств имен так, чтобы они ссылались на Chll CardLib. To же самое касается и консольного приложения Chi OCardClien t, которое вы будете использовать для тестирования. В состав загружаемого кода входит проект, содержащий весь код, который может потребоваться для обеспечения различных расширенных возможностей в ChllCardLib. Этот код поделен на разделы, в каждом из которых можно удалять комментарии при желании испробовать его в действии. Если же вы хотите создать данный проект самостоятельно, добавьте в него новый класс Cards и измените код в представляющем его файле Cards. cs следующим образом: using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; namespace ChllCardLib { public class Cards : CollectionBase { public void Add (Card newCard) List.Add(newCard); public void Remove (Card oldCard) List. Remove (oldCard) ; public Cards () public Card this [int cardlndex] { get { return (Card)List[cardlndex]; } set { List[cardlndex] = value; } } // Вспомогательный метод для копирования экземпляров Card в другой // экземпляр Cards — используемый в Deck. Shuffle () . В данной реализации // предполагается, что исходная коллекция и целевая коллекция // имеют одинаковый размер, public void CopyTo (Cards targetCards) { for (int index = 0; index < this.Count; index++) { targetCards[index] = this[index]; } }
302 Часть I. Язык С# // Выполнение проверки для выяснения, содержится // ли в коллекции Cards определенная карта. // Эта проверка подразумевает вызов для коллекции метода // Contains коллекции ArrayList, доступ к которому // получается через свойство InnerList. public bool Contains (Card card) { return InnerList.Contains(card); } } } Теперь измените код в файле Deck.cs так, чтобы в нем использовалась эта новая коллекция, а не массив: using System; using System.Collections .Generic- using System.Linq; using System.Text; namespace ChllCardLib { public class Deck { private Cards cards = new Cards () ; public Deck() { // Здесь была удалена строка кода for (int suitVal = 0; suitVal < 4; suitVal++) { for (int rankVal = 1; rankVal < 14; rankVal++) { cards. Add (new Card ((Suit) suitVal, (Rank) rankVal)) ; } } } public Card GetCard(int cardNum) { if (cardNum >= 0 && cardNum <= 51) return cards[cardNum]; else throw (new System.ArgumentOutOfRangeException("cardNum", cardNum, "Value must be between 0 and 51.")); } public void Shuffle () { Cards newDeck = new Cards () ; bool[] assigned = new bool [52]; Random sourceGen = new Random(); for (int i = 0; i < 52; i++) { int sourceCard = 0; bool foundCard = false; while (foundCard == false) { sourceCard = sourceGen.NextE2); if (assigned[sourceCard] = false) foundCard = true; } assigned[sourceCard] = true;
Глава 11. Коллекции, сравнения и преобразования 303 newDeck.Add(cards[sourceCard]); } newDeck.CopyTo(cards); } } } Здесь требуется не очень много изменений. Большая часть из них состоит в изменении логики тасования так, чтобы она предусматривала факт добавления карты в начало новой Cards-коллекции newDeck из позиции со случайным индексом в cards, а не в позицию со случайным индексом в newDeck из последовательной позиции в cards. Клиентское консольное приложение ChlOCardClient, создававшееся для решения ChlOCardLib, может применяться с этой новой библиотекой с тем же результатом, поскольку сигнатуры методов Deck не изменились. Клиенты этой библиотеки классов теперь, однако, смогут использовать класс коллекции Cards, а не полагаться на массивы объектов Card, например, в случае определения имеющихся на руках карт в приложении для игры в карты. Ключевые коллекции и интерфейс iDictionary Вместо интерфейса IList также допускается, чтобы коллекции реализовали похожий интерфейс IDictionary, который позволяет, чтобы элементы индексировались с помощью ключевого значения (например, строкового имени), а не индекса. Подобное поведение также достигается с помощью индексатора, хотя в таком случае в качестве параметра для индексатора указывается ключ, ассоциируемый с хранимым элементом, а не индекс int, что может делать коллекцию гораздо более дружественной к пользователю. Как и в случае индексированных коллекций, существует базовый класс, который можно использовать для упрощения интерфейса IDictionary— DictionaryBase. Этот класс тоже поддерживает интерфейсы IE numerable и ICollection и предлагает базовые возможности для манипулирования, которые одинаковы для любой коллекции. Класс DictionaryBase подобно CollectionBase реализует некоторые (не все) из тех членов, которые получает от поддерживаемых им интерфейсов. В частности, как и класс CollectionBase, он реализует члены Clear и Count, но не член RemoveAt (), поскольку тот является методом интерфейса IList и не присутствует в интерфейсе IDictionary. Но зато у интерфейса IDictionary есть метод Remove (), и этот метод является одним из тех, которые следует реализовать в классе специальной коллекции на базе DictionaryBase. В следующем коде демонстрируется альтернативная версия класса Animals, на этот раз унаследованная от DictionaryBase и включающая реализации методов Add() и Remove (), а также индексатора с доступом по ключу: public class Animals : DictionaryBase { public void Add(string newID, Animal newAnimal) Dictionary.Add(newID, newAnimal); public void Remove(string animallD) Dictionary.Remove(animallD); public Animals()
304 Часть I. Язык С# public Animal this[string animallD] { get { return (Animal)Dictionary[animallD]; } set { Dictionary[animallD] = value; } } } Отличия этих членов описаны ниже. □ Метод Add(). Принимает два параметра, ключ и значение, для совместного хранения. У словарной коллекции имеется член по имени Dictionary, унаследованный от DictionaryBase, который представляет собой интерфейс IDictionary. У этого интерфейса имеется свой собственный метод Add (), который принимает два параметра-объекта. В данной реализации он принимает строковое значение в качестве ключа и объект Animal в качестве данных, подлежащих сохранению вместе с этим ключом. □ Метод Remove (). Принимает параметр-ключ, а не ссылку на объект. Элемент с указанным значением ключа удаляется. □ Индексатор. Использует не индекс, а строковое значение-ключ, которое применяется для получения доступа хранимому элементу через унаследованный член Dictionary. Опять-таки, приведение типов здесь является необходимым. Еще одно отличие между коллекциями на базе DictionaryBase и коллекциями на базе CollectionBase состоит в том, что немного по-другому работает структура f oreach. Класс коллекции, демонстрировавшийся в предыдущем разделе, позволял извлекать из коллекции непосредственно объекты Animal. Применение f oreach с производным классом DictionaryBase позволяет получать структуры DictionaryEntry, которые представляют собой еще один тип, определенный в пространстве имен System.Collections. Из-за этого для получения самих объектов Animal требуется использовать такой член этой структуры, как Value, или, для получения ассоциируемых с ними ключей, такой член, как Key. To есть, чтобы получить код, эквивалентный прежнему: foreach (Animal myAnimal in animalCollection) { Console.WriteLine("New {0} object added to custom collection, " + "Name = {1}", myAnimal.ToString(), myAnimal.Name); } необходимо сделать следующее: foreach (DictionaryEntry myEntry in animalCollection) { Console.WriteLine("New {0} object added to custom collection, " + "Name = {1}", myEntry.Value.ToString() , ((Animal)myEntry.Value).Name); } Существует возможность переопределить это поведение так, чтобы к объектам Animal можно было получать доступ непосредственно через foreach. Для этого доступно несколько способов, самым простым из которых является реализация итератора.
Глава 11. Коллекции, сравнения и преобразования 305 Итераторы Ранее в этой главе было показано, что интерфейс I Enumerable позволяет использовать циклы f oreach. В циклах f oreach зачастую оказывается выгодно работать со многими классами, а не только с классами коллекций, вроде тех, что демонстрировались в предыдущих разделах. Однако переопределение этого поведения, т.е. предоставление для него своей собственной реализации, не всегда выглядит просто. Чтобы увидеть это, сначала необходимо более внимательно рассмотреть сами циклы f oreach. Перечисленные ниже шаги описывают, что на самом деле происходит в цикле f oreach, осуществляющем проход по коллекции с именем collectionObject. 1. Сначала вызывается метод collectionObject .GetEnumerator (), который возвращает ссылку IEnumerator. Этот метод доступен через реализацию интерфейса IEnumerable, хотя таковая является необязательной. 2. Далее вызывается такой метод возвращенного интерфейса IEnumerator, как MoveNext(). 3. Если метод MoveNext () возвращает true, используется такое свойство интерфейса IEnumerator, как Current, для получения ссылки на объект, которая далее применяется в цикле f oreach. 4. Два последних шага повторяются до тех пор, пока MoveNext () не вернет false, после чего цикл завершается. Для обеспечения такого поведения в своих классах потребуется переопределить несколько методов, отслеживать индексы, обслуживать свойство Current и т.д. — другими словами, прикладывать слишком много усилий для достижения столь малого результата. К счастью, существует и более простой альтернативный вариант — итератор. Применение итераторов, по сути, приводит к генерации "за кулисами" множества кода и его корректной привязке. Более того, синтаксис для использования операторов является гораздо более простым для изучения. Хорошее определение для итератора звучит так: итератор — это блок кода, который предоставляет все значения, подлежащие использованию в блоке f oreach, по очереди. Обычно в роли этого блока кода выступает метод, хотя в роли итератора также может использоваться и блок, отвечающий за доступ к свойству, или какой-нибудь другой блок кода. Чтобы не усложнять дело, здесь будут демонстрироваться только методы. Каким бы не был этот блок кода, количество возможных кандидатов на роль его возвращаемого типа ограничено. Пожалуй, вопреки ожиданиям, этот возвращаемый тип не может совпадать с типом того объекта, который перечисляется. Например, в классе, представляющем коллекцию объектов Animal, возвращаемым типом блока итератора не может быть Animal. Двумя допустимым кандидатами на роль возвращаемого значения являются типы упоминавшихся раньше интерфейсов IEnumerable или IEnumerator. Применяются они следующим образом. □ Для осуществления итерации по классу применяется метод GetEnumerator () с возвращаемым типом IEnumerator. □ Для осуществления итерации по члену класса, например, методу, применяется IEnumerable. Внутри блока итератора выбираются подлежащие использованию в цикле f oreach значения с помощью ключевого слова yield. Необходимый для этого синтаксис выглядит следующим образом: yield return значение;
306 Часть I. Язык С# Приведенной информации уже вполне достаточно для того, чтобы создать очень простой пример, вроде того, что показан ниже: public static IEnumerable SimpleListO { yield return "string 1"; yield return "string 2"; yield return "string 3"; } public static void Main(string [ ] args) { foreach (string item in SimpleListO) Console.WriteLine(item); Console.ReadKey(); } При желании самостоятельно протестировать данный код, не забудьте добавить оператор using для пространства имен System. Collections или, в противном случае, полностью квалифицируйте имя интерфейса System. Collections. IEnumerable. В качестве альтернативного варианта, можете просто воспользоваться загружаемым кодом для этой главы. Здесь в роли итератора выступает статический метод SimpleList (). Поскольку эту роль выполняет метод, в качестве возвращаемого типа применяется тип IEnumerable. Сам метод SimpleList () использует ключевое слово yield для предоставления использующему его блоку foreach трех значений, каждое из которых выводится на экран. Результат показан на рис. 11.3. Рис. 11.3. Метод-итератор SimpleList () в действии Очевидно, что данный итератор не является особо полезным, но зато действительно показывает, как все это работает на практике и насколько просто может выглядеть реализация. После просмотра кода может возникнуть вопрос, откуда он знает, что нужно возвращать элементы типа string? На самом деле он этого не знает; он возвращает объекты типа object. Как уже известно, object является базовым классом для всех типов, благодаря чему из операторов yield можно возвращать все что угодно. Однако компилятор является достаточно разумным для того, чтобы позволить интерпретировать возвращаемые значения как значения любого желаемого типа в контексте цикла foreach. Здесь код запрашивает значения типа string, поэтому они и предоставляются для работы. В случае изменения одной из строк yield таким образом, чтобы она возвращала, скажем, целое число, будет сгенерировано исключение, связанное с неправильным приведением в цикле foreach. Обратите внимание на еще один аспект, касающийся итераторов. Существует возможность прерывать возврат информации циклу foreach с использованием следующего оператора: yield break;
Глава 11. Коллекции, сравнения и преобразования 307 Когда в итераторе встречается такой оператор, обработка кода итератора тут же прекращается, равно как и выполнение использующего его цикла f oreach. Теперь пришла пора более сложного — и более полезного — примера. Поэтому в следующем разделе демонстрируется реализация итератора, получающего простые числа. Реализация итератора 1. Создайте новое консольное приложение по имени ChllEx03 и сохраните его в каталоге C:\BegVCSharp\Chapterll. 2. Добавьте новый класс Primes и измените его код следующим образом: using System; using System.Collections; using System.Collections .Generic- using System.Linq; using System.Text; namespace ChllEx03 { public class Primes { private long min; private long max; public Primes () : this B, 100) { } public Primes (long minimum, long maximum) { if (min < 2) min = 2; else min = minimum; max = maximum; ) public IEnumerator GetEnumerator () { for (long possiblePrime = min; possiblePrime <= max; possiblePrime++) { bool isPrime = true; for (long possibleFactor = 2; possibleFactor <= (long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++) { long remainderAfterDivision = possiblePrime % possibleFactor; if (remainderAfterDivision == 0) { isPrime = false; break; } } if (isPrime) { yield return possiblePrime; } } } } }
308 Часть I. Язык С# 3. Измените код в файле Program, cs следующим образом: static void Main(string [ ] args) { Primes primesFrom2Tol000 = new PrimesB, 1000); foreach (long i in primesFrom2Tol000) Console.Write("{0} ", i) ; Console.ReadKey(); } 4. Запустите приложение. На рис. 11.4 показан результат, который должен получиться. г— ■ П fi»e:///C:/BegVCSharpshapleM 1/СМ 1ЕхОЗ/СМ1ЕхОЗ/Ып/ОеЬид/СГ»11ЕхОЗЕХЕ 2 3 5' Э7 1И9 23 227 37 347 Ь7 461 93 599 19 727 57 859 97 ш f 11 11 ! !!29 149 4i i t.Hl 73 i М i 13 17 19 23 127 131 233 239 353 359 467 479 6Й7 613 739 743 «77 >1>м " .— 1 17 241 167 •1X7 1 1 7 7'. 1 И 1 29 31 37 41 43 47 5 139 149 151 157 163 251 257 263 269 271 373 379 383 389 397 491 499 503 5И9 521 619 631 641 643 647 757 761 769 773 787 887 9Й7 911 919 929 м,..-^г,.тт^.-;„1;г.7Г7^ 59 61 67 71 73 79 83 89 97 167 173 179 181 191 193 277 281 283 293 387 311 4И1 4И9 419 421 431 433 523 541 547 557 563 569 653 659 661 673 677 683 797 8И9 811 821 823 827 937 941 947 953 967 971 197 313 439 571 691 829 977 —_ 1 -1 '-1 там 1И1 ЮЗ 1 199 211 2 317 331 3 443 449 А 577 '.в7 ', 7И1 7Й9 7 839 853 Я 983 991 9 ., , „ | J Рис. 11.4. Приложение СЬНЕхОЗ в действии Описание полученных результатов Этот пример состоит из класса Primes, который позволяет проходить коллекцию простых чисел, находящихся между указанными верхним и нижним пределом. Этот класс инкапсулирует простые числа и использует для предоставления указанной функциональности итератор. Код класса Primes начинается с определения базовых деталей, а именно— двух полей для хранения максимального и минимального значений, между которыми должен выполняться поиск, и конструкторов — для установки этих значений. Обратите внимание на то, что на минимальное значение накладывается ограничение, не позволяющее, чтобы оно было меньше 2. Такое ограничение действительно имеет смысл, поскольку 2 является наименьшим простым числом. Весь самый интересный код находится в методе GetEnumerator (). Сигнатура этого метода отвечает правилам оформления блока итератора, поскольку предусматривает возврат типа I Enumerator: public IEnumerator GetEnumerator () { Для извлечения простых чисел в определенных пределах необходимо протестировать каждое число по очереди, поэтому сначала идет цикл for: for (long possiblePrime = min; possiblePrime <= max; possiblePrime++) Поскольку признак, является число простым или нет, не известен, сначала делается предположение о том, что оно является таковым и выполняется проверка на предмет того, так ли это. Это подразумевает выполнение проверки на предмет того, является ли число между 2 и тем числом, которое получается при вычислении корня квадратного из тестируемого числа, множителем (factor). Если да, тогда число не является простым и потому осуществляется переход к следующему числу. Если же число действительно является простым, тогда оно передается циклу foreach с помощью оператора yield:
Глава 11. Коллекции, сравнения и преобразования 309 bool isPrime = true; for (long possibleFactor = 2; possibleFactor <= (long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++) { long remainderAfterDivision = possiblePrime % possibleFactor; if (remainderAfterDivision == 0) { isPrime = false; break; } } if (isPrime) { yield return possiblePrime; } } } При указании слишком больших чисел для нижнего и верхнего пределов, в коде проявляется один интересный факт. Во время выполнения приложения результаты появляются по одному за раз, с паузами между ними, а не все одновременно. Это является свидетельством того, что код итератора возвращает результаты по одному, несмотря на тот факт, что никакого очевидного места завершения выполнения кода между вызовами yield нет. Однако "за кулисами" вызов yield все-таки действительно приводит к прерыванию выполнения кода, которое возобновляется при запрашивании следующего значения, т.е. тогда, когда у цикла f oreach, в котором используется итератор, начинается новый прогон. Итераторы и коллекции Ранее было обещано объяснить то, как итераторы могут применяться для итерации по объектам, хранящимся в коллекции словарного типа, без привлечения объектов Dictionary Item. Напомним, как выглядит класс коллекции Animals: public class Animals : DictionaryBase { public void Add(string newID, Animal newAnimal) Dictionary.Add(newID, newAnimal); public void Remove (string ammallD) Dictionary.Remove(animallD); public Animals() public Animal this[string animallD] { get { return (Animal)Dictionary[animallD]; } set { Dictionary[animallD] = value; } } }
310 Часть I. Язык С# Для получения желаемого поведения в этот код можно добавить следующий простой итератор: public new IEnumerator GetEnumerator () { foreach (object animal in Dictionary.Values) yield return (Animal)animal; } Теперь для прохождения по объектам Animal в коллекции можно использовать такой код: foreach (Animal myAnimal in animalCollection) { Console.WriteLine("New {0} object added to custom collection, " + "Name = {1}", myAnimal.ToString(), myAnimal.Name); } Обратитесь к проекту Dictionary Animals в загружаемом коде для данной главы. Глубокое копирование В главе 9 уже рассказывалось, как выполнять поверхностное копирование с помощью защищенного метода System.Object .MemberwiseClone (), реализуя метод, подобный GetCopy: public class Cloner { public int Val; public Cloner(int newVal) { Val = newVal; } public object GetCopy () { return MemberwiseClone (); } } Теперь давайте предположим, что имеющиеся поля представляют собой не типы- значения, а ссылочные типы (например, объекты): public class Content { public int Val; } public class Cloner { public Content MyContent = new Content () ; public Cloner (int newVal) { MyContent. Val = newVal; } public object GetCopy() { return MemberwiseClone (); } }
Глава 11. Коллекции, сравнения и преобразования 311 В таком случае при поверхностном копировании методом GetCopyO появится поле, ссылающееся на тот же объект, что и исходный. Следующий код, в котором используется данный класс С lone г, иллюстрирует последствия поверхностного копирования ссылочных типов: Cloner mySource = new ClonerE); Cloner myTarget = (Cloner)mySource.GetCopy(); Console.WriteLine("myTarget.MyContent.Val = {0}", myTarget.MyContent.Val); mySource.MyContent.Val = 2; Console.WriteLine("myTarget.MyContent.Val = {0}", myTarget.MyContent.Val); Здесь в четвертой строке, где присваивается значение полю mySource. MyContent. Val (т.е. общедоступному полю Val общедоступного поля MyContent исходного объекта), также изменяется и значение myTarget. MyContent .Val. Объясняется это тем, что mySource. MyContent ссылается на экземпляр того же самого объекта, что и myTarget .MyContent. Поэтому вывод показанного выше кода будет выглядеть следующим образом: myTarget.MyContent.Val = 5 myTarget.MyContent.Val = 2 Обойти эту проблему можно, выполнив глубокое копирование. Конечно, для этого можно соответствующим образом изменить применявшийся ранее метод GetCopy (), но все-таки лучше использовать стандартный подход .NET Framework, т.е. реализовать интерфейс ICloneable, который имеет единственный метод Clone (). Этот метод не принимает параметров и возвращает результат типа object, что делает его сигнатуру идентичной сигнатуре применяемого ранее метода GetCopy (). Чтобы изменить предыдущие классы, попробуйте воспользоваться следующим кодом глубокого копирования: public class Content { public int Val; } public class Cloner : ICloneable { public Content MyContent = new Content (); public Cloner(int newVal) { MyContent.Val = newVal; } public object Clone () { Cloner clonedCloner = new Cloner (MyContent.Val) ; return clonedCloner; } } Здесь был создан новый объект Cloner с использованием поля Val объекта Content, содержавшегося в исходном объекте Cloner (MyContent). Данное поле представляет собой тип-значение, поэтому необходимости в глубоком копировании нет. Использование кода, подобного только что показанному, для тестирования поверхностного копирования, но только с применением метода Clone () вместо GetCopy (), приведет к получению следующего результата: myTarget.MyContent.Val = 5 myTarget.MyContent.Val = 5 На этот раз содержащиеся объекты являются независимыми. Обратите внимание на то, что иногда, в частности, в более сложных системах объектов, вызовы метода
312 Часть I. Язык С# Clone () могут осуществляться рекурсивно. Например, если бы поле MyContent класса С lone r тоже нуждалось в глубоком копировании, тогда могло бы потребоваться использовать следующий код: public class Cloner : ICloneable { public Content MyContent = new Content (); public object Clone() { Cloner clonedCloner = new Cloner () ; clonedCloner.MyContent = MyContent.Clone() ; return clonedCloner; } } Здесь для упрощения синтаксиса создания нового объекта Cloner вызывается конструктор по умолчанию. Для функционирования этого кода еще потребовалось бы реализовать в классе Content интерфейс ICloneable. Добавление возможности глубокого копирования в CardLib Применить все это на практике можно, реализовав с использованием интерфейса ICloneable возможность для копирования объектов Card, Cards и Deck. Подобное может быть полезно в некоторых карточных играх, где не требуется, чтобы две колоды ссылались на один и тот же набор объектов Card, но, возможно, при этом нужно, чтобы порядок в одной колоде выглядел точно так же, как и в другой. Реализация функции клонирования для класса Card в ChllCardLib выглядит просто, поскольку для него вполне достаточно и только возможности поверхностного копирования (ведь в нем содержатся только данные типа значений, в виде полей). Поэтому просто внесите в определение этого класса следующие изменения: public class Card : ICloneable { public object Clone() { return MemberwiseClone () ; ) Обратите внимание, что данная реализация ICloneable подразумевает выполнение лишь поверхностного копирования. Никакого правила, которое бы определяло, что должно происходить в методе Clone (), нет, и для преследуемых здесь целей этого вполне достаточно. Далее реализуйте интерфейс ICloneable для класса коллекции Cards. Это будет немного сложнее, поскольку клонироваться должен каждый объект Card в исходной коллекции, что требует глубокого копирования: public class Cards : CollectionBase, ICloneable { public object Clone() { Cards newCards = new Cards () ; foreach (Card sourceCard in List) { newCards.Add(sourceCard.Clone() as Card); } return newCards; }
Глава 11. Коллекции, сравнения и преобразования 313 И, наконец, реализуйте интерфейс ICloneable для класса Deck. Обратите внимание на наличие здесь одной небольшой проблемы: этот класс не предусматривает никакой возможности для изменения содержащихся в нем карт, кроме их тасования. Например, не существует никакой возможности изменить экземпляр Deck так, чтобы он имел определенный порядок карт. Чтобы обойти эту проблему, определите для класса Deck новый приватный конструктор, позволяющий передавать определенную коллекцию Card при создании экземпляра объекта Deck. Ниже приведен код, необходимый для реализации в этом классе возможности клонирования: public class Deck : ICloneable { public object Clone() { {0} Deck newDeck = new Deck (cards. Clone () as Cards) ; return newDeck; } private Deck (Cards newCards) { cards = newCards; } Опять-таки, вы можете испробовать весь этот код в действии с помощью какого-нибудь простого клиентского кода (который, как и раньше, должны разместить внутри метода Main () соответствующего, предназначенного для тестирования, клиентского проекта): Deck deckl = new Deck() ; Deck deck2 = (Deck)deckl.Clone (); Console.WriteLine("The first card in the original deck is: // Первая карта в исходной колоде deckl.GetCard(O)); Console.WriteLine("The first card in the cloned deck is: {0} // Первая карта в клонированной колоде deck2.GetCard@)); deckl.Shuffle(); Console.WriteLine("Original deck shuffled."); // Исходная колода перемешана Console.WriteLine("The first card in the original deck is: deckl.GetCard(O)); Console.WriteLine("The first card in the cloned deck is: {0}", deck2.GetCard@)); Console.ReadKey(); Вывод будет выглядеть примерно так, как показано на рис. 11.5. {0} Рис. 11.5. Тестирование добавленной возможности клонирования
314 Часть I. Язык С# Сравнения В этом разделе рассматриваются два следующих возможных типа сравнений между объектами: □ сравнения типов; □ сравнения значений. Сравнения типов (т.е. определение того, что собой представляет объект или от чего он наследуется) важны во всех областях программирования на С#. Зачастую при передаче объекта (методу, например) то, что произойдет далее, зависит от того, объектом какого типа является данный объект. С этим уже не раз доводилось встречаться при передаче объектов как в этой, так и в предыдущих главах, но здесь будет продемонстрировано еще несколько полезных приемов. Сравнения значений тоже не раз было показано, по крайней мере, на примере простых типов. В случае сравнения значений объектов, однако, все выглядит немного сложнее. Для начала необходимо определить то, что именно подразумевается под сравнением, и что операции вроде > означают в контексте данных классов. Это особенно важно в случае коллекций, для которых может быть необходимо, чтобы объекты сортировались по какому-то условию, например, по алфавиту, или по какому-то более сложному алгоритму. Сравнение типов При сравнении объектов часто требуется знать их тип, поскольку это может позволить определять, является ли возможным сравнение значений. В главе 9 был представлен метод GetType (), который все классы наследуют от базового класса System.Object, и то, как он может использоваться вместе с операцией type of () для определения типов объектов (и выполнения в зависимости от этого того или иного действия): if (myObj.GetType() == typeof(MyComplexClass)) { // myObj является экземпляром класса MyComplexClass. } Еще там было показано и то, как с помощью стандартной реализации метода ToString (), также унаследованного от базового класса System.Object, можно получать строковое представление типа объекта. Эти строки тоже можно сравнивать, но такой подход является довольно громоздким. В данном разделе будет продемонстрирован более короткий удобный способ, предусматривающий применение операции is. Он позволяет создавать гораздо более удобный для восприятия код, а также просматривать базовые классы. Перед рассмотрением операции is, однако, необходимо ознакомиться с процессами, которые часто происходят "за кулисами" при работе с типами-значениями (в отличие от ссылочных типов), а именно— упаковкой и распаковкой Упаковка и распаковка В главе 8 рассказывалось об отличиях между ссылочными типами и типами-значениями, а в главе 9 эти отличия даже были проиллюстрированы на примере сравнения структур (которые представляют собой типы-значения) и классов (являющихся ссылочными типами). Упаковкой (boxing) называется процесс преобразования типа- значения в тип System.Object или в тип интерфейса, который реализуется данным типом-значением, а распаковкой (unboxing) — соответственно, процесс обратного преобразования.
Глава 11. Коллекции, сравнения и преобразования 315 Например, предположим, что имеется структура следующего типа: struct MyStruct { public int Val; } Упаковать структуру такого типа можно путем ее помещения в переменную типа object: MyStruct valTypel = new MyStruct(); valTypel.Val = 5; object refType = valTypel; Здесь сначала создается новая переменная (valTypel) типа MyStruct, затем ее члену Val присваивается новое значение, после чего она упаковывается в переменную типа object (refType). Объект, созданный путем упаковки переменной подобным образом, будет содержать ссылку на копию переменной valType, а не на исходную переменную valType. Удостовериться в этом можно, изменив содержимое исходной структуры, а затем распаковав содержащуюся в объекте структуру в новую переменную и просмотрев ее содержимое: valTypel.Val = 6; MyStruct valType2 = (MyStruct)refType; Console.WriteLine("valType2.Val = {0}", valType2.Val); Этот код приведет к получению следующего вывода: valType2.Val = 5 В случае присваивания ссылочного типа объекту, однако, поведение будет другим. Увидеть это можно, превратив MyStruct в класс (не обращая внимания на то, что тогда имя для этого класса является не совсем подходящим): class MyStruct { public int Val; } Если не вносить изменений в показанный ранее клиентский код (и, опять-таки, не обращать в нем внимания на неподходящие имена для переменных), это приведет к получению следующего вывода: valType2.Val = б Типы-значения еще можно упаковывать и в типы интерфейсов, разумеется, при условии, что они реализуют эти интерфейсы. Например, предположим, что тип MyStruct реализует интерфейс IMylnterfасе, как показано ниже: interface IMylnterface { } struct MyStruct : IMylnterface { public int Val; } Тогда упаковать его в тип IMylnterface можно следующим образом: MyStruct valTypel = new MyStruct(); IMylnterface refType = valTypel;
316 Часть I. Язык С# А для его распаковки подойдет обычный синтаксис приведения типов: MyStruct ValType2 = (MyStruct)refType; Как видно из этих примеров, процесс упаковки не требует вмешательства со стороны разработчика, т.е. писать какой-то код для обеспечения его выполнения не нужно. Что касается процесса распаковки значения, то тут требуется явное преобразование, т.е. нужно явным образом выполнять приведение типов (упаковка выполняется неявно и потому такого требования не имеет). Может возникнуть вопрос о том, зачем вообще делать подобное? На самом деле, существуют две очень веских причины, по которым упаковка является чрезвычайно полезной. Во-первых, она позволяет использовать типы-значения в коллекциях (вроде ArrayList), где элементы являются элементами типа object. Во-вторых, она представляет собой внутренний механизм, который обеспечивает возможность вызывать для типов-значений, подобных int и struct, методы object. Напоследок хотелось бы отметить, что выполнение распаковки является просто необходимым перед получением доступа к содержимому типов-значений. Операция is Несмотря на название, операция is вовсе не представляет собой способ для определения того, является ли объект объектом конкретного типа. Вместо этого она позволяет проверять, является ли объект объектом заданного типа, а также то, может ли он быть преобразован в объект такого типа. В случае удовлетворения этого условия операция is возвращает true. В более ранних примерах уже демонстрировались классы Cow и Chicken, унаследованные от класса Animal. В случае применения is для сравнения объектов с типом Animal значение true будет возвращаться для объектов всех трех этих типов, а не только Animal. Достичь подобного с помощью метода Get Type () и операции typeof (), которые показывались ранее, было бы очень трудно. Синтаксис операции is выглядит следующим образом: <операнду is <тип> Возможные результаты этого выражения перечислены ниже. □ В случае указания в <тип> типа класса,true будет возвращаться, если содержащийся в <операнд> операнд является объектом такого типа, если он унаследован от такого типа и если он может быть упакован в такой тип. □ В случае указания в <тип> типа интерфейса true будет возвращаться, если содержащийся в < операнд> операнд является объектом такого типа и если он является объектом типа, который реализует такой интерфейс. □ В случае указания в <тип> типа-значения true будет возвращаться, если содержащийся в <операнд> операнд является объектом такого типа и если он является объектом типа, который может быть распакован в такой тип. В следующем практическом занятии демонстрируется, как все это работает. практическое занятие Использование операции is 1. Создайте новое консольное приложение по имени ChllEx04 и сохраните его в каталоге C:\BegVCSharp\Chapterll. 2. Измените код в его файле Program, cs следующим образом:
Глава 11. Коллекции, сравнения и преобразования 317 namespace ChllEx04 { class Checker { public void Check(object paraml) { if (paraml is ClassA) Console.WriteLine("Variable can be converted to ClassA."); // Переменная может быть преобразована в ClassA else Console.WriteLine("Variable can't be converted to ClassA."); // Переменная не может быть преобразована в ClassA if (paraml is IMylnterface) Console.WriteLine("Variable can be converted to IMylnterface."); // Переменная может быть преобразована в IMylnterface else Console.WriteLine("Variable can't be converted to IMylnterface."); // Переменная не может быть преобразована в IMylnterface if (paraml is MyStruct) Console.WriteLine("Variable can be converted to MyStruct."); // Переменная может быть преобразована в MyStruct else Console.WriteLine("Variable can't be converted to MyStruct."); // Переменная не может быть преобразована в MyStruct interface IMylnterface class ClassA : IMylnterface class ClassB : IMylnterface class ClassC class ClassD : ClassA struct MyStruct : IMylnterface class Program { static void Main(string[] args) { Checker check = new Checker (); ClassA tryl = new ClassA (); ClassB try2 = new ClassB (), ClassC try3 = new ClassC () ClassD try4 = new ClassD () MyStruct try5 = new MyStruct (); object try6 = try5; Console.WriteLine("Analyzing ClassA type variable:"), II Анализ переменной типа ClassA
318 Часть I. Язык С# check.Check(tryl) Console.WriteLine check.Check(try2) Console.WriteLine check.Check(try3) Console.WriteLine check.Check(try4) Console.WriteLine check.Check(try5) Console.WriteLine check.Check(try6); Console.ReadKey() ; ("\nAnalyzing ClassB type variable:"); // Анализ переменной типа ClassB ("XnAnalyzing ClassC type variable:"); // Анализ переменной типа ClassC ("XnAnalyzing ClassD type variable:"); // Анализ переменной типа ClassD ("XnAnalyzing MyStruct type variable:"); // Анализ переменной типа MyStruct ("XnAnalyzing boxed MyStruct type variable:"); // Анализ упакованной переменной типа MyStruct 3. Запустите приложение. На рис. 11.6 показан результат, который должен получиться. Рис. 11.6. Приложение Chi 1Ех04 в действии Описание полученных результатов Этот пример иллюстрирует различные результаты, которые можно получать в случае применения операции is. Три класса, интерфейс и структура определяются и используются в качестве параметров для метода класса, который применяет операцию is для выяснения того, могут ли они быть преобразованы в тип ClassA, тип интерфейса и тип структуры. Только типы ClassA и ClassD (унаследованные от ClassA) являются совместимыми с ClassA. Типы, которые не наследуются ни от какого класса, не являются совместимыми с этим классом.
Глава 11. Коллекции, сравнения и преобразования 319 Типы ClassA, ClassB и MyStruct реализуют интерфейс IMylnterf асе и потому все они являются совместимыми с типом IMylnterf асе. Тип ClassD унаследован от ClassA и, следовательно, тоже является совместимым с этим типом. Таким образом, получается, что несовместимым с ним является только ClassC. И, наконец, только переменные самого типа MyStruct и упакованные переменные такого типа являются совместимыми с MyStruct, поскольку преобразовывать ссылочные типы в типы-значения нельзя (хотя упакованные ранее переменные, конечно, можно и распаковать). Сравнение значений Возьмем два представляющих людей объекта Person (Человек), каждый из которых имеет целочисленное свойство Age (Возраст). Может возникнуть желание сравнить их для того, чтобы узнать, кто из двух людей является старше. Чтобы сделать это, можно просто использовать и такой код: if (personl.Age > person2.Age) { } Такой подход хорош, но существуют и альтернативные варианты. Например, может оказаться предпочтительнее использовать такой синтаксис, как показан ниже: if (personl > person2) { } Подобное делает возможным применение приема перегрузки операций, о котором более подробно будет рассказываться в следующем разделе. Он является мощным, но применять его нужно рассудительно. В приведенном выше коде сразу и не понятно, что сравнивается именно возраст людей, а не рост, вес, коэффициент умственного развития или вообще общая "значимость". Вторым вариантом является применение интерфейсов IComparable и IComparer, которые позволяют определять то, как объекты будут сравниваться друг с другом, стандартным образом. Такой подход поддерживается многими классами коллекций в .NET Framework, что делает его прекрасным способом для сортировки объектов в коллекции. Перегрузка операций Перегрузка операций позволяет использовать стандартные операции, вроде +, > и т.д., с классами собственной разработки. "Перегрузкой" она называется потому, что подразумевает предоставление для этих операций своих собственных реализаций в случае использования параметров специфических типов, во многом подобно перегрузке методов, которая осуществляется путем предоставления разных параметров для методов с одинаковым именем. Перегрузка операций является полезным приемом, поскольку в реализации перегруженной операции можно выполнять какую угодно обработку, т.е. добиваться выполнения более сложных действий, чем, например, просто сложение двух операндов вместе, если брать операцию +. Чуть позже это будет показано более детально на примере дальнейшей модернизации библиотеки CardLib путем предоставления реализаций для операций сравнений, сравнивающих две карты для выяснения того, какая из них побьет другую в данной партии.
320 Часть I. Язык С# Поскольку выигрыш партии во многих карточных играх зависит от масти участвующих карт, простым сравнением чисел на картах здесь не обойтись. Если вторая выкладываемая карта по масти отличается от первой, тогда должна побеждать первая карта, независимо от ее достоинства. Реализовать такое поведение можно, взяв в расчет порядок двух операндов. Еще можно взять в расчет козырную масть, чтобы козырная карта била карты любых других мастей, даже если она не выкладывается первой. Это означает, что выяснение того, что выражение cardl > card2 является истинным (т.е. cardl бьет card2, если cardl выкладывается первой) вовсе не обязательно должно подразумевать, что выражение card2 > cardl является ложным. В случае если ни cardl, ни card2 не являются козырными картами и имеют разную масть, оба этих выражения должны возвращать true. Сначала, однако, необходимо ознакомиться с базовым синтаксисом перегрузки операций. Операции можно перегружать добавлением в класс членов соответствующего им типа (которые обязательно должны быть статическими — static). Некоторые операции предусматривают несколько способов применения (как, например, операция -, у которой имеется как унарная, так и бинарная версия), поэтому при перегрузке операций также требуется указывать и то, о каком количество операндов идет речь и к какому типу эти операнды относятся. Как правило, тип операндов совпадает с типом класса, в котором определяется операция, хотя операции, способные работать со смешанными типами, тоже допускается определять, как будет показано чуть позже. В качестве примера, давайте возьмем простой класс AddClassl, определенный следующим образом: public class AddClassl { public mt val; } Он представляет собой всего лишь оболочку вокруг значения int, но позволяет проиллюстрировать базовые принципы. С таким классом, показанный ниже код приведет во время компиляции к выдаче сообщения об ошибке: AddClassl opl = new AddClassl (); opl.val = 5; AddClassl op2 = new AddClassl (); op2.val = 5; AddClassl op3 = opl + op2; В сообщении об ошибке говорится о том, что операция + не может применяться к операндам типа AddClassl. Объясняется это тем, что подлежащая выполнению операция еще не была определена. Показанный ниже код будет работать, но не будет давать того результата, который, возможно, требуется: AddClassl opl = new AddClassl(); opl.val = 5; AddClassl op2 = new AddClassl(); op2.val = 5; bool op3 = opl == op2; Здесь opl и ор2 сравниваются с помощью бинарной операции == для выяснения того, ссылаются ли они на один и тот же объект, а не для выполнения проверки на предмет того, равны ли их значения. орЗ будет возвращать в предыдущем коде значение false, даже несмотря на то, что opl .val и ор2 .val идентичны. Для перегрузки операции + можно использовать такой код:
Глава 11. Коллекции, сравнения и преобразования 321 public class AddClassl { public int val; public static AddClassl operator + (AddClassl opl, AddClassl op2) { AddClassl returnVal = new AddClassl () ; returnVal. val = opl. val + op2.val; return returnVal; ) } Здесь видно, что перегрузки операций выглядят во многом подобно стандартным объявлениям статических методов, но только в них используется ключевое слово operator и сама операция, а не имя метода. Теперь операцию + можно успешно использовать с данным классом, как в предыдущем примере: AddClassl орЗ = opl + op2; Перегрузка всех бинарных операций выполняется по одной и той же схеме. Унарные операции выглядят похоже, но имеют только один параметр: public class AddClassl { public int val; public static AddClassl operator +(AddClassl opl, AddClassl op2) { AddClassl returnVal = new AddClassl (); returnVal.val = opl.val + op2.val; return returnVal; } public static AddClassl operator -(AddClassl opl) { AddClassl returnVal = new AddClassl () ; returnVal.val = -opl.val; return returnVal; } } Обе эти операции работают с операндами того же типа, что и у класса, и имеют возвращаемые значения, тип которых тоже совпадает с типом данного класса. Теперь давайте возьмем, однако, такие определения классов: public class AddClassl { public int val; public static AddClass3 operator +(AddClassl opl, AddClass2 op2) { AddClass3 returnVal = new AddClass3(); returnVal.val = opl.val + op2.val; return returnVal; } public class AddClass2 public int val; public class AddClass3 public int val;
322 Часть I. Язык С# Они позволят использовать такой код: AddClassl opl = new AddClassl(); opl.val = 5; AddClass2 op2 = new AddClass2 () ; op2.val = 5; AddClass3 op3 = opl + op2; Когда требуется, можно смешивать типы подобным образом. Обратите, однако, внимание на то, что в случае добавления в AddClass2 такой же операции, предыдущий код не работал бы, поскольку появилась бы неоднозначность по поводу того, какая именно из операций должна использоваться. Это означает, что следует соблюдать осторожность и не добавлять операции с одинаковой сигнатурой в более чем один класс. Помимо этого, в случае смешивания типов, операнды должны обязательно предоставляться перегруженной операции в том же самом порядке, что параметры. При попытке применить перегруженную операцию с операндами, идущими не в том порядке, операция работать не будет. Например, нельзя использовать такую операцию: AddClass3 орЗ = ор2 + opl; если только, конечно, не предоставить еще одну перегрузку с идущими в обратном порядке параметрами: public static AddClass3 operator +(AddClass2 opl, AddClassl op2) { AddClass3 returnVal = new AddClass3(); returnVal.val = opl.val + op2.val; return returnVal; } Ниже перечислены все те операции, которые можно перегружать. □ Унарные операции: +, -, !, ~, ++, — , true, false. □ Бинарные операции: +,-,*,/,%,&, |, Л, < <, > >. □ Операции сравнения: ==, !=,<,>, <=, >=. В случае перегрузки операций true и false классы можно использовать в булевских выражениях, таких как if (opl) {}. Перегружать операции присваивания вроде += нельзя, но у таких операций имеются свои простые аналоги, такие как +, так что волноваться об этом не стоит. Перегрузка + означает, что += будет функционировать, как и ожидалось. Перегружать операцию = тоже нельзя из-за ее фундаментального предназначения, но она связана с определяемыми пользователем операциями преобразования, о которых будет рассказываться в следующем разделе. Еще нельзя перегружать и такие операции, как && и | |, но они для выполнения своих вычислений используют операции & и |, перегрузки которых вполне достаточно. Некоторые операции, например, < и >, требуется перегружать парами. Другими словами, нельзя перегружать операцию < и не перегружать при этом >. Во многих случаях можно просто вызывать из этих операций другие операции для сокращения количества требуемого кода (и, соответственно, ошибок, которые могут возникнуть), как показано в следующем примере: public class AddClassl { public int val;
Глава 11. Коллекции, сравнения и преобразования 323 public static bool operator >= (AddClassl opl, AddClassl op2) { return (opl.val >= op2.val); } public static bool operator < (AddClassl opl, AddClassl op2) { return !(opl >= op2) ; > // Также необходимы реализации для операций <= и >. } В определениях более сложных операций такой подход действительно может сокращать количество подлежащих написанию строк кода, а также, следовательно, и объем изменяемого кода в случае, если позже понадобится изменить реализацию этих операций. То же самое касается операций == и ! =, но в их случае зачастую лучше переопределять методы Object .Equals () и Object .GetHashCode (), поскольку они оба тоже могут применяться для сравнения объектов. Переопределение этих методов обеспечивает гарантию того, что какой бы прием не использовали пользователи класса, результат они будут получать одинаковый. Существенным этот факт не является, но заслуживает упоминания для полноты изложения и требует применения следующих нестатических переопределенных методов: public class AddClassl { public int val; public static bool operator ==(AddClassl opl, AddClassl op2) return (opl.val == op2.val); public static bool operator !=(AddClassl opl, AddClassl op2) return !(opl == op2); public override bool Equals (object opl) return val = ((AddClassl)opl) .val; public override int GetHashCode() return val; } GetHashCode () применяется для получения уникального значения int для экземпляра объекта на основе его состояния. Здесь использование val является допустимым, потому что val тоже является значением типа int. Обратите внимание на то, что в Equals () применяется параметр типа object. Такая сигнатура является необходимой, иначе это будет перегрузка, а не переопределение данного метода, и тогда реализация по умолчанию все равно будет доступна пользователям класса. Вместо этого для получения требуемого результата необходимо применять приведение типов. Зачастую проверка типа объекта с помощью описанной ранее операции is является стоящим делом, как, например, в показанном ниже коде:
324 Часть I. Язык С# public override bool Equals(object opl) { if (opl is AddClassl) { return val == ( (AddClassl)opl).val; } else { throw new ArgumentException ( "Cannot compare AddClassl objects with objects of type " + opl. GetType () . ToString ()) ; } } В этом коде в случае, если переданный методу Equals операнд оказывается не того типа или не может быть приведен к правильному типу, генерируется исключение. Конечно, такое поведение может и не быть подходящим. Вместо него может быть необходимо, чтобы была возможность сравнивать объекты одного типа с объектами другого типа, в случае чего потребуется большее ветвление кода. Или же может быть нужно, чтобы сравнение выполнялось только между объектами абсолютно одинакового типа, в случае чего потребуется внести в первый оператор if следующее изменение: if (opl.GetType () == typeof(AddClassl)) Добавление перегруженных операций в CardLib Теперь можно снова обновить проект ChllCardLib, добавив в его класс Card перегрузку операций. Перед этим, однако, потребуется добавить в класс Card дополнительные поля, позволяющие определять козырные масти и назначать тузы старшей картой. Их нужно сделать статическими, поскольку в случае установки для них значений они будут применяться ко всем объектам Card: public class Card { // Флаг для применения козырных карт. При значении true козырные // карты должны цениться больше, чем карты других мастей. public static bool useTrumps = false; // Козырная масть, которая должна использоваться в случае, // если useTrumps равняется true, public static Suit trump = Suit.Club; // Флаг, определяющий то, являются ли тузы старше королей или младше двоек, public static bool isAceHigh = true; Обратите внимание, что эти правила распространяются на все объекты Card в каждом экземпляре Deck в приложении. Невозможно иметь две колоды, карты в каждой из которых подчиняются разным правилам. Для данной библиотеки, однако, подобное допустимо, потому что она позволяет спокойно предположить, что если одно приложение хочет использовать отдельные правила, тогда оно может обслуживать таковые самостоятельно, например, за счет установки значений для статических членов Card при каждой смене колоды. Сделав это, стоить добавить в класс Deck еще несколько конструкторов для инициализации колод с разными характеристиками:
Глава 11. Коллекции, сравнения и преобразования 325 public Deck() { for (int suitVal = 0; suitVal < 4; suitVal++) { for (int rankVal = 1; rankVal < 14; rankVal++) { cards.Add( new Card((Suit)suitVal/ (Rank)rankVal)); } } } // Конструктор не по умолчанию, позволяющий делать тузы старшими, public Deck(bool isAceHigh) : this() { Card.isAceHigh = isAceHigh; } // Конструктор не по умолчанию, позволяющий использовать козырную масть. public Deck(bool useTrumps, Suit trump) : this() { Card.useTrumps = useTrumps; Card.trump = trump; } // Конструктор не no умолчанию, позволяющий делать тузы старшими //и использовать козырную масть. public Deck(bool isAceHigh, bool useTrumps, Suit trump) : this() { Card.isAceHigh = isAceHigh; Card.useTrumps = useTrumps; Card.trump = trump; } Каждый из этих конструкторов определен с использованием продемонстрированного в главе 9 синтаксиса : this (), так что во всех случаях конструктор по умолчанию будет вызываться перед конструктором не по умолчанию, инициализирующим колоду. Теперь можно переходить и непосредственно к добавлению в класс Card перегруженных операций (а также предложенных переопределенных методов). public Card(Suit newSuit, Rank newRank) suit = newSuit; rank = newRank; public static bool operator =(Card cardl, Card card2) return (cardl.suit = card2.suit) && (cardl.rank = car62.rank) ; public static bool operator ! = (Card cardl, Card card2) return !(cardl = card2); public override bool Equals (object card) return this = (Card)card; public override int GetHashCode() return 13*(int)rank + (int)suit;
326 Часть I. Язык С# public static bool operator > (Card cardl, Card card2) { if (cardl.suit = card2.suit) { if (isAceHigh) { if (cardl.rank = Rank.Ace) { if (card2.rank = Rank.Ace) return false; else return true; } else { if (car62.rank = Rank.Ace) return false; else return (cardl. rank > card2. rank) ; } } else { return (cardl. rank > card2. rank) ; } } else { if (useTrumps && (card2 .suit = Card, trump)) return false; else return true; ) } public static bool operator < (Card cardl, Card card2) { return !(cardl >= card2); } public static bool operator >=(Card cardl, Card card2) { if (cardl.suit = card2.suit) { if (isAceHigh) { if (cardl.rank = Rank.Ace) { return true; } else { i f (card2. rank = Rank. Ace) return false; else return (cardl.rank >= card2.rank); } }
Глава 11. Коллекции, сравнения и преобразования 327 else { return (cardl.rank >= card2.rank); } } else { if (useTrumps && (card2.suit == Card, trump)) return false; else return true; } } public static bool operator <=(Card cardl, Card card2) { return !(cardl > card2); } Обратить внимание здесь стоит разве что на несколько длинноватый код для перегруженных операций > и >=. Пошаговое изучение кода для > позволит легко понять, как он работает и почему в нем необходимы такие шаги. Итак, здесь выполняется сравнение двух карт— cardl и card2, при котором предполагается, что карта cardl была выложена на стол первой. Как уже объяснялось ранее, это становится важным в случае использования козырных карт, поскольку козырная карта будет бить не козырную, даже если та является картой более высокого достоинства. Конечно, если масть обеих карт совпадает, тогда то, является масть козырной или нет, роли не играет, поэтому первым выполняется такое сравнение: public static bool operator > (Card cardl, Card card2) { if (cardl.suit == card2.suit) { Если статический флаг isAceHigh имеет значение true, тогда сравнивать достоинства карт напрямую по их значению в перечислении Rank нельзя, потому что достоинство туза соответствует в этом перечисление значению 1, что меньше значений карт всех остальных достоинств. Поэтому вместо этого используются следующие шаги. □ Если первой картой является туз, тогда выполняется проверка на предмет того, является ли вторая карта тоже тузом. Если да, тогда первая карта не бьет вторую, а если нет, тогда побеждает первая карта: if (isAceHigh) { if (cardl.rank == Rank.Асе) { if (card2.rank == Rank.Ace) return false; else return true; } □ Если первая карта отлична от туза, тогда все равно выполняется проверка на предмет того, не является ли таковым вторая карта. Если да, тогда побеждает вторая карта, а если нет, тогда может выполняться сравнение значений представляющих достоинство карт, поскольку известно, что тузов среди них нет:
328 Часть I. Язык С# else { if (card2.rank == Rank.Асе) return false; else return (cardl.rank > card2.rank); □ Если тузы не являются старшей картой, тогда может сразу же выполняться сравнение значений, представляющих достоинство карт: else { return (cardl.rank > card2.rank); } Остальной код заботится о ситуации, когда масть cardl и card2 не совпадает. Здесь важную роль играет статический флаг useTrumps. Если он имеет значение true и масть карты card2 является козырной, тогда точно можно сказать, что карта cardl козырем не является (поскольку масти этих двух карт не совпадают), а козыри всегда побеждают, так что карта card2 считается более высокой: else { if (useTrumps && (card2.suit == Card.trump)) return false; Если карта card2 не является козырем (или флаг useTrumps имеет значение false), тогда побеждает карта cardl, поскольку она была выложена на стол первой: else return true; } } Только в еще одной другой операции (а именно операции >=) используется подобный код; все остальные операции выглядят очень просто, поэтому рассказывать о них более подробно нет никакой необходимости. Следующий простой клиентский код позволяет протестировать все операции (чтобы испробовать его, поместите его в функцию Main () клиентского проекта, подобно тому, как делали это в клиентском коде, приведенном в предыдущих примерах CardLib): Card.isAceHigh = true; Console.WriteLine("Aces are high."); // Тузы являются старшей картой Card.useTrumps = true; Card.trump = Suit.Club; Console.WriteLine ("Clubs are trumps."); // Трефы являются козырной мастью Card cardl, card2, card3, card4, card5; cardl = new Card(Suit.Club, Rank.Five); card2 = new Card(Suit.Club, Rank.Five); card3 = new Card(Suit.Club, Rank.Ace); card4 = new Card(Suit.Heart, Rank.Ten); card5 = new Card(Suit.Diamond, Rank.Ace); Console.WriteLine("{0} == {1} ? {2}", cardl.ToString(), card2.ToString(), cardl == card2);
Глава 11. Коллекции, сравнения и преобразования 329 Console.WriteLine("{0} != {1} ? {2}", cardl.ToStnngO , card3.ToString(), cardl !=card3); Console.WriteLine("{0}.Equals({l}) ? {2}", cardl.ToStnngO, card4 .ToString () , cardl .Equals (card4) ) ; Console.WriteLine(MCard.Equals({0}, {l}) ? {2}", card3.ToString(), card4.ToString(), Card.Equals(card3, card4)), Console.WriteLine("{0} > {1} ? {2}", cardl.ToString (), card2.ToString (), cardl > card2); Console.WriteLine("{0} <= {1} ? {2}", cardl.ToString (), card3.ToString (), cardl <= card3); Console.WriteLineCMO} > {1} ? {2}", cardl.ToString (), card4.ToString(), cardl > card4); Console.WriteLine(M{0} > {1} ? {2}", card4.ToString(), cardl.ToString (), card4 > cardl); Console.WriteLineCMO} > {1} ? {2}", card5.ToString (), card4.ToString (), card5 > card4); Console.WriteLineCMO} > {1} ? {2}", card4.ToString (), card5.ToString (), card4 > card5); Console.ReadKey(); Результаты показаны на рис. 11.7. Рис. 11.7. Тестирование перегруженных операций в CardLib В каждом случае операции применяются с учетом заданных правил. Это особенно заметно в четырех последних строках вывода, демонстрирующих, что козырные карты всегда бьют не козырные. Интерфейсы IComparable и IComparer Интерфейсы IComparable и IComparer являются стандартным способом для сравнения объектов в .NET Framework. Разница между ними описана ниже. □ Интерфейс IComparable реализуется в классе подлежащего сравнению объекта и потому позволяет выполнять сравнения только между этим и еще каким-то объектом. □ Интерфейс IComparer реализуется в отдельном классе и потому позволяет выполнять сравнения между любыми двумя объектами. Обычно код сравнения, который должен использоваться в классе по умолчанию, предоставляется с помощью интерфейса IComparable, а код сравнения, который не должен использоваться по умолчанию — с помощью других классов. Интерфейс IComparable поддерживает один единственный метод CompareTo (), который принимает объект. Его можно, например, реализовать таким образом, чтобы он позволял передавать ему объект Person и определять, является ли данный человек старше или младше текущего. На самом деле, этот метод возвращает значение int,
330 Часть I. Язык С# так что еще можно сделать и так, чтобы он позволял определять, насколько именно старше или моложе является второй человек: if (personl.CompareTo(person2) == 0) Console.WriteLine("Same age"); // Того же возраста else if (personl.CompareTo(person2) > 0) Console.WriteLine("person 1 is Older"); // Первый человек старше else Console.WriteLine("personl is Younger"); // Первый человек моложе Интерфейс IComparer тоже предоставляет единственный метод Compare (), который принимает два объекта и возвращает целочисленный результат, точно так же, как метод CompareTo О. При наличии объекта, поддерживающего IComparer, можно применять такой код: if (personComparer.Compare(personl, person2) == 0) Console.WriteLine("Same age"); // Того же возраста else if (personComparer.Compare(personl, person2) > 0) Console.WriteLine("person 1 is Older"); // Первый человек старше else Console.WriteLine("personl is Younger"); // Первый человек моложе В обоих случаях параметры, предоставляемые методам, относятся к типу System. Object. Это означает, что сравниваться могут объекты любого типа, а это обычно требует выполнения перед возвратом результата какой-то операции по сравнению типов и, возможно даже, выдачи исключений в случае обнаружения того, что используемые типы являются неправильными. В состав .NET Framework входит используемая по умолчанию реализация интерфейса IComparer, предназначенная для класса по имени Comparer, который находится в пространстве имен System.Collections. Этот класс способен выполнять специфические для каждой культуры операции сравнения между простыми типами, а также любым типом, который поддерживает интерфейс I Comparable. Его можно использовать, например, со следующим кодом: string firstString = "First String"; string secondString = "Second Strings- Console .WriteLine ("Comparing '{0}' and '{l}1, result: {2}", // Результат сравнения строк firstString, secondString, Comparer.Default.Compare(firstString, secondString));
Глава 11. Коллекции, сравнения и преобразования 331 int firstNumber = 35; int secondNumber =23; Console.WriteLine("Comparing '{0}' and 41} \ result: {2}", // Результат сравнения чисел firstNumber, secondNumber, Comparer.Default.Compare(firstNumber, secondNumber)); Здесь сначала используется статический член Comparer .Default для получения экземпляра класса Comparer, а затем— метод Compare () для сравнения первых двух строк и потом еще двух чисел. Результат получается следующий: Comparing 'First String' and 'Second String', result: -1 Comparing '35' and '23', result: 1 Поскольку в алфавите буква F идет перед буквой S, считается, что F "меньше, чем" S, поэтому результат первого сравнения равен -1. Аналогично, число 35 является больше, чем 23 и потому результат в данном случае равен 1. Обратите внимание, что результаты не отражают величину разницы (т.е. на сколько меньше и больше один объект другого). В случае применения класса Comparer нужно обязательно использовать только такие типы, которые могут сравниваться. Попытка сравнить firstString с firstNumber, например, завершится генерацией исключения. Ниже перечислены моменты, касающиеся поведения этого класса, на которые стоит обратить внимание. □ Объекты, передаваемые Comparer .Compare () проверяются на предмет того, не поддерживают ли они интерфейс I Comparable. Если поддерживают, тогда используется та реализация. □ Нулевые значения являются допустимыми и считаются "меньшими" любого другого объекта. □ Строки обрабатываются в соответствии с текущей культурой. При желании, чтобы строки обрабатывались в соответствии с какой-то другой культурой (или языком), экземпляр класса Comparer нужно создавать с помощью его собственного конструктора, который позволяет передавать объект System. Globalization.Culturelnfо, указывающий на применяемую культуру. □ Строки обрабатываются с учетом регистра. При желании, чтобы они обрабатывались без учета регистра, нужно использовать класс CaselnsensitiveComparer, который во всем остальном работает совершенно таким же образом. Сортировка коллекций с использованием интерфейсов IComparable и IComparer Многие классы коллекций допускают возможность выполнения сортировки, либо с использованием стандартных операций сравнения между объектами, либо посредством специальных методов. Одним из примеров является класс коллекции ArrayList. Он содержит метод Sort (), который может применяться как без параметров, в случае чего используются стандартные операции сравнения, так и принимать в качестве параметра интерфейс IComparer, который он должен использовать для сравнения пар объектов. В случае класса коллекции ArrayList, заполняемого простыми типами вроде целых чисел или строк, стандартный метод сравнения вполне подходит. В случае же своих собственных классов нужно обязательно либо реализовать в определении класса интерфейс IComparable, либо создавать отдельный класс, поддерживающий интерфейс IComparer, и применять его для выполнения сравнений.
332 Часть I. Язык С# Обратите внимание на то, что некоторые классы в пространстве имен System. Collection, включая CollectionBase, не предоставляют вообще никакого метода для сортировки. При желании реализовать сортировку в коллекции, унаследованной от какого-то из этих классов, придется прикладывать немного больше усилий и обеспечивать сортировку внутренней коллекции List самостоятельно. В следующем практическом занятии демонстрируется пример использования стандартного и нестандартного метода сравнения для сортировки списка. ^^■г11*! Сортировка списка 1. Создайте новое консольное приложение по имени ChllEx05 и сохраните его в каталоге C:\BegVCSharp\Chapterll. 2. Добавьте новый класс Person и измените код в его файле следующим образом: namespace ChllEx05 { class Person : IComparable { public string Name; public int Age; public Person (string name, int age) { Name = name; Age = age; > public int CompareTo (object obj) { if (obj is Person) { Person otherPerson = obj as Person; return this.Age - otherPerson.Age; } else { throw new ArgumentException ( "Object to compare to is not a Person object.") ; // Сравниваемый объект не является объектом Person } } 3. Добавьте еще один новый класс по имени PersonComparerName и измените код в его файле следующим образом: using System; using System. Collections ; using System.Collections.Generic; using System.Linq; using System.Text; namespace ChllEx05 { public class PersonComparerName : IComparer { public static IComparer Default = new PersonComparerName () ;
Глава 11. Коллекции, сравнения и преобразования 333 public int Compare (object x, object y) { if (x is Person && у is Person) { return Comparer. Default. Compare ( ((Person)x).Name, ((Person)у).Name); } else { throw new ArgumentException ( "One or both objects to compare are not Person objects.") // Один или оба из сравниваемых объектов //не являются объектами Person } } } } 4. Измените код в файле Program, cs, как показано ниже: using System; using System. Collections ; using System.Collections.Generic; using System.Linq; using System.Text; namespace ChllEx05 { class Program { static void Main(string[] args) { ArrayList list = new ArrayList() ; list.Add(new Person("Jim", 30)); list.Add(new Person("Bob", 25)); list.Add(new Person("Bert", 27)); list.Add(new Person("Ernie", 22)); Console. WriteLine ("Unsorted people: ") ; // Несортированный список людей for (int i = 0; i < list.Count; i++) { Console.WriteLine("{0} ({I})", (list[i] as Person).Name, (list[i] as Person).Age); } Console.WriteLine(); Console.WriteLine( "People sorted with default comparer (by age) : ") ; // Список людей, отсортированный (по возрасту) //с помощью метода сравнения по умолчанию list.Sort(); for (int i = 0; i < list.Count; i++) { Console.WriteLine("{0} ({I})", (list[i] as Person).Name, (list[i] as Person).Age); > Console.WriteLine(); Console.WriteLine( "People sorted with nondefault comparer (by name) :") ;
334 Часть I. Язык С# } // Список людей, отсортированный (по имени) с помощью // метода сравнения не по умолчанию list.Sort(PersonComparerName.Default); for (int i = 0; i < list.Count; i++) { Console.WriteLine("{0} ({I})", (list[i] as Person).Name, (list[i] as Person).Age) , } Console.ReadKey(); 5. Запустите приложение. На рис. 11.8 показан результат, который должен получиться. I ni«:///C:/B^iVCSh«rprt;haptor11/Ch11Ex05«:M1Ex05A<rVDel)oe«:M1Ex05.EXE l'»MI|ll«- nrlr.l Ernie <ГА> II., I. <•/',> lie rl </.7> ■III! < (И) 1-сэ<»- ••ь <?.:,> rnia <?.?.> Рис. 11.8. Приложение ChllEx05 в действии Описание полученных результатов Здесь класс коллекции ArrayList, содержащий объекты Person, сортируются двумя разными способами. За счет вызова метода ArrayList. Sort () применяется стандартный метод сравнения, каковым является CompareTo () из класса Person (поскольку этот класс реализует интерфейс I Comparable): public int CompareTo(object obj) { if (obj is Person) { Person otherPerson = obj as Person; return this.Age - otherPerson.Age; } else { throw new ArgumentException( "Object to compare to is not a Person object."); Этот метод сначала проверяет, может ли его аргумент быть подвергнут сравнению с объектом Person, т.е. может ли данный объект быть преобразован в объект Person. Если нет, генерируется исключение. Если да, выполняется сравнение свойств Age этих двух объектов Person.
Глава 11. Коллекции, сравнения и преобразования 335 Далее осуществляется сортировка с применением нестандартного метода сравнения, заключающегося в использовании класса PersonComparerName, который реализует интерфейс IComparer. У этого класса для упрощения его использования имеется общедоступное статическое поле: public static IComparer Default = new PersonComparerName(); Оно позволяет получать экземпляр этого класса с помощью PersonComparerName. Default, точно так же и показывавшийся ранее класс Comparer. Метод CompareTo () этого класса выглядит следующим образом: public int Compare(object x, object y) { if (x is Person && у is Person) { return Comparer.Default.Compare ( ((Person)x).Name, ((Person)y).Name); } else { throw new ArgumentException( "One or both objects to compare are not Person objects."); } } Опять-таки, сначала выполняется проверка аргументов на предмет того, являются ли они объектами Person. Если нет, генерируется исключение. А если да, то тогда используется стандартный объект Comparer для сравнения строковых полей Name этих двух объектов Person. Преобразования Пока что операция преобразования применялась только тогда, когда было необходимо преобразовать один тип в другой, но это не является единственно возможным вариантом. Подобно тому, как тип int может преобразовываться в тип long или double неявным образом в виде части операции вычисления, классы тоже могут определяться так, чтобы они преобразовывались в другие классы (неявно или явно). Делается это путем перегрузки операций преобразования, во многом подобно перегрузке других операций, что демонстрировалось ранее в главе. О том, как именно это делается, будет рассказано в первой части этого раздела, а второй части рассматривается еще одна полезная операция as, которую обычно рекомендуется применять для выполнения приведения между ссылочными типами. Перегрузка операций преобразования Помимо перегрузки математических операций, как показывалось ранее в этой главе, можно определять как неявные, так и явные операции преобразования между типами. Подобное может быть необходимо для выполнения преобразования между типами, которые не связаны между собой, т.е., например, не имеют ни связывающих их между собой отношений наследования, ни общих интерфейсов. Предположим, что требуется определить операцию неявного преобразования между ConvClassl и ConvClass2. Это означает, что можно использовать такой код: ConvClassl opl = new ConvClassl (); ConvClass2 op2 = opl;
336 Часть I. Язык С# Для определения операции явного преобразования можно применять следующий код: ConvClassl opl = new ConvClassl (); ConvClass2 op2 = (ConvClass2)opl; Например, давайте рассмотрим показанный ниже код: public class ConvClassl { public int val; public static implicit operator ConvClass2(ConvClassl opl) { ConvClass2 returnVal = new ConvClass2(); returnVal.val = opl.val; return returnVal; } } public class ConvClass2 { public double val; public static explicit operator ConvClassl(ConvClass2 opl) { ConvClassl returnVal = new ConvClassl (); checked {returnVal.val = (int)opl.val;}; return returnVal; } } Здесь ConvClassl содержит значение типа int, a ConvClass2 — значение типа double. Поскольку значения int могут преобразовываться в значения double неявным образом, операцию неявного преобразования между ConvClassl и ConvClass2 определить можно. Обратное, однако, невозможно, и потому операция преобразования между ConvClass2 и ConvClassl должна быть определена как явная. Это было указано с помощью ключевых слов implicit и explicit. To есть в случае данных классов, следующий код будет работать нормально: ConvClassl opl = new ConvClassl (); opl.val = 3; ConvClass2 op2 = opl; Для преобразования в обратном направлении потребуется выполнение операции явного приведения типов: ConvClass2 opl = new ConvClass2 () ; opl.val = 3el5; ConvClassl op2 = (ConvClassl)opl; Поскольку в операции явного преобразования было использовано слово checked, показанный выше код приведет к генерации исключения, потому что значение val объекта opl является слишком большим для того, чтобы уместиться в свойстве val объекта ор2. Операция as Операция as позволяет преобразовывать тип в определенный ссылочный тип с применением следующего синтаксиса: <операнду as <тип>
Глава 11. Коллекции, сравнения и преобразования 337 Выполнение подобного преобразования возможно только в перечисленных ниже случаях. □ Если операнд, указанный в <операнд>, имеет тип, заданный в <тип>. □ Если операнд, указанный в <операнд>, может быть неявно преобразован в тип, заданный в <тип>. □ Если операнд, указанный в <операнд>, может быть упакован в тип, заданный в <тип>. Если операнд, указанный в <операнду, не может быть приведен к типу, заданному в <тип>, результатом выражения будет null. Обратите внимание, что выполнять преобразование из базового класса в производный с применением операции явного преобразования можно, но такой подход работает не всегда. Рассмотрим классы ClassA и ClassD из приведенного ранее примера, где ClassD унаследован от ClassA: class ClassA : IMylnterface { } class ClassD : ClassA { } В следующем коде операция as используется для выполнения преобразования хранящегося в obj 1 экземпляра ClassA в тип ClassD: ClassA objl = new ClassA (); ClassD obj2 = objl as ClassD; Выполнение этого кода приведет к получению в obj 2 значения null. Однако с помощью полиморфизма можно сделать так, чтобы экземпляры ClassD сохранялись в переменных типа ClassA. В следующем коде иллюстрируется этот подход, и операция as используется для преобразования в тип ClassD переменной типа ClassA, содержащей экземпляр типа ClassD: ClassD objl = new ClassD () ; ClassA obj2 = objl; ClassD obj3 = ob]2 as ClassD; На этот раз выполнение кода приведет к тому, что в obj 3 будет содержаться ссылка на тот же самый объект, а не значение null. Такая функциональность делает операцию as очень полезной, поскольку следующий код (в котором применяется простая операция приведения типов), например, будет приводить к выдаче исключения: ClassA objl = new ClassA (); ClassD obj2 = (ClassD)objl; В случае использования в этом коде операции as он будет приводить просто к присваиванию obj2 значения null — исключение генерироваться не будет. Поэтому код вроде того, что приведен ниже (и в котором участвуют два разработанных ранее в главе класса, а именно — Animal и производный от него класс Cow), очень часто и применяется в приложениях на С#: public void MilkCow(Animal myAnimal) { Cow myCow = myAnimal as Cow;
338 Часть I. Язык С# if (myCow != null) { myCow.Milk(); } else { Console.WriteLine("{0} isn't a cow, and so can't be milked.", myAnimal.Name); } } Гораздо легче применять такой код, чем обеспечивать проверку на предмет выдачи исключений. Резюме В этой главе были рассмотрены многие из тех приемов, которыми можно пользоваться для того, чтобы делать ООП-приложения гораздо более мощными и интересными. Эти приемы не требуют особых усилий, но могут делать классы гораздо более удобными для работы и, следовательно, упрощать написание всего остального необходимого кода. Каждая из рассмотренных тем имеет много сфер применения. С коллекциями наверняка придется иметь дело в той или иной форме практически в любом приложении, а создание строго типизированных коллекций может существенно облегчить жизнь в ситуациях, когда требуется работать с группой объектов одинакового типа. Еще здесь было рассказано и о том, как добавлять индексаторы и итераторы, которые позволяют легко получать доступ к объектам внутри коллекции. Сравнения и преобразования тоже являются темой, которая постоянно попадается на глаза. В этой главе было показано, как выполняются различные операции сравнения, а также как выглядят базовые процессы упаковки и распаковки. Здесь было продемонстрировано, как перегружать операции, причем как операции сравнения, так и операции преобразования, а также как применять это все вместе для сортировки списка. В следующей главе речь пойдет о совершенно другом — об обобщениях. Они позволяют создавать классы, способные автоматически подстраиваться под работу с динамически выбираемыми типами. Это особенно удобно в случае коллекций, и там будет показано, как большую часть приведенного в настоящей главе кода можно значительно упростить за счет использования обобщенных коллекций. Упражнения 1. Создайте класс типа коллекции по имени People, представляющий собой коллекцию показанного ниже класса Person. Элементы в этой коллекции должны быть доступны через строковый индексатор, в роли которого должно выступать имя человека, идентичное хранящемуся в свойстве Person.Name: public class Person { private string name; private int age; public string Name { get { return name; }
Глава 11. Коллекции, сравнения и преобразования 339 set { name = value; } public int Age get return age; set age = value; } } 2. Расширьте класс Person из предыдущего упражнения так, чтобы в нем перегружались операции >,<,>= и <= и выполнялось сравнение свойства Age со свойством Age других экземпляров Person. 3. Добавьте в класс People метод GetOldest, возвращающий массив объектов Person с наибольшим значением в свойстве Age (их может быть как один, так и больше, поскольку многие элементы могут иметь в этом свойстве одинаковое значение) с использованием перегруженных версий операций, которые определили ранее. 4. Реализуйте в классе People интерфейс ICloneable для обеспечения возможности глубокого копирования. 5. Добавьте в класс People итератор, позволяющий получать значения Age всех членов в цикле f о reach показанным ниже образом: foreach (int age in myPeople.Ages) { // Отображение значений возраста. }
Обобщения Одна из немногочисленных жалоб в отношении первой версии языка С# была связана с отсутствием поддержки обобщений (generics). Обобщения в языке C++ (называемые там шаблонами) давно считаются замечательным приемом, поскольку позволяют одному единственному определению типа разрастаться во множество специализированных типов во время компиляции и тем самым экономить огромное количество времени и усилий. По какой-то причине обобщения так и не вошли в первую версию языка С#, и он из-за этого пострадал. Возможно, так случилось потому, что обобщения часто находят довольно сложными для изучения, или может потому, что было решено, что они не столь уж необходимы. К счастью, начиная с версии С# 2.0, они стали доступными. Даже еще лучше то, что теперь их не очень трудно использовать, хотя для этого, конечно, все равно требуется взглянуть на вещи по-другому. В этой главе рассматриваются следующие темы. □ Что собой представляют обобщения. Сначала обобщения рассматриваются в относительно общих чертах, потому что изучение основных понятий является критически важным для эффективного использования обобщений. □ Как выглядят в действии некоторые из общедоступных типов, предлагаемых .NET Framework. Это поможет увидеть их функциональные возможности и мощь, а также новый синтаксис, который для них требуется использовать в коде. □ Как определять свои собственные обобщенные типы — обобщенные классы, интерфейсы, методы и делегаты, — и какие дополнительные приемы существуют для дальнейшей настройки обобщенных типов, вроде ключевого слова default и ограничениях типов. Что собой представляют обобщения Чтобы наилучшим образом проиллюстрировать, что собой представляют обобщения и почему они являются настолько полезными, вспомним классы коллекций из предыдущей главы. Там показывалось, как базовые коллекции могут содержаться
Глава 12. Обобщения 341 в классах, вроде ArrayList, а также то, что такие коллекции страдают от отсутствия контроля типа, из-за чего требуется приводить элементы object к типу объектов, которые в действительности хранятся в коллекции. Поскольку в ArrayList могут храниться любые объекты, которые наследуются от System.Object (т.е. практически какие угодно), необходимо соблюдать осторожность. Предположение о том, что в коллекции содержатся объекты только определенных типов может заканчиваться генерацией исключений и разбиением логики кода. В предыдущей главе были показаны некоторые приемы, с помощью которых можно обходить подобные проблемы, включая и код, который нужно использовать для проверки типа объекта. Однако там же и выяснилось, что гораздо более эффективным решением является применение строго типизированной коллекции с самого начала. Путем наследования от класса CollectionBase и предоставления своих собственных методов для добавления, удаления и получения доступа к членам коллекции можно ограничивать члены коллекции только теми, которые наследуются от определенного базового типа или поддерживают определенный интерфейс. Однако тут как раз и начинаются проблемы. При каждом создании нового класса, который должен содержаться в коллекции, требуется делать одно из описанных ниже действий. □ Использовать какой-нибудь созданный класс коллекции, который может содержать элементы нового типа. □ Создавать новый класс коллекции, способный хранить элементы нового типа и реализующий все требуемые методы. Обычно вместе с новым типом требуется и дополнительная функциональность, поэтому чаще всего нужно все равно создавать новый класс коллекции. Из-за всего этого подготовка классов коллекций может отнимать приличное время. Обобщенные классы, напротив, гораздо упрощают дело. Обобщенным классом называется такой класс, который создается вокруг типа или типов, каковые предоставляются во время создания экземпляра, и тем самым позволяет делать объект строго типизированным практически безо всяких усилий. В контексте коллекций процесс создания "коллекции объектов типа Т" выглядит так же просто, как звучит, и может осуществляться с помощью единственной строки кода. То есть вместо такого кода: CollectionClass col = new CollectionClass(); col.Add(new ItemClass()); можно использовать следующий код: CollectionClass<ItemClass> col = new CollectionClass<ItemClass>() ; col.Add(new ItemClass()); Квадратные скобки в синтаксисе являются способом, которым обобщенным типам передаются типы переменных. В предыдущем коде, следовательно, CollectionClass<ItemClass> нужно читать как CollectionClass из ItemClass. Конечно, этот синтаксис еще будет более подробно рассматриваться позже в этой главе. Обобщения охватывают не только коллекции, но просто для них они подходят больше всего, как можно будет увидеть позже в этой главе при рассмотрении пространства имен System.Collections .Generic. За счет создания обобщенного класса можно генерировать методы с сигнатурой, позволяющей им становиться строго типизированными на базе любого типа, даже с учетом того, что этим типом может быть как тип-значение, так и ссылочный тип, и что придется иметь дело с особыми случаями. Можно даже делать допустимым только определенный набор типов путем ограничения типов, применяемых для создания экземпляра обобщенного класса, только теми, которые поддерживают конкретный интерфейс или наследуются от конкрет-
342 Часть I. Язык С# ного типа. Более того, обобщенными классами дело не заканчивается: также можно создавать обобщенные интерфейсы, обобщенные методы (которые допускается определять и в не обобщенных классах) и даже обобщенные делегаты. Все это прибавляет коду гибкости, а раз так, разумное применение обобщений может экономить многие часы при его разработке. Наверняка у вас возник вопрос о том, как все это возможно. Обычно при создании класса он компилируется в тип, который затем можно использовать в своем коде. Вы можете подумать, что после создания обобщенный класс должен компилироваться в целую кучу типов, чтобы затем можно было создавать его экземпляры. К счастью, все происходит не так, а с учетом возможности создания в .NET практически бесконечного количества классов, и очень хорошо, что это не так. "За кулисами" исполняющая среда .NET позволяет обобщенным классам генерироваться динамически только тогда, когда в них возникает необходимость. Никакой обобщенный класс А для класса В не будет существовать до тех пор, пока вы не запросите его путем создания его экземпляра. Те кто, знаком с языком C++ или интересуется им, должны обратить внимание, что в этом как раз и состоит одно из отличий между шаблонами C++ и обобщенными классами С#. В C++ компилятор определяет, где в коде был использован шаблон специфического типа, например, шаблон А для класса В, и компилирует код, необходимый для создания такого типа. В С# все происходит во время выполнения. В целом, обобщения позволяют создавать гибкие типы, способные обрабатывать объекты одного и более специфических типов, определяются которые лишь при создании экземпляра обобщения или использовании его каким-то другим образом. Теперь пришла пора увидеть, как обобщения выглядят в действии. Использование обобщений Прежде чем рассказывать о том, как создавать свои собственные обобщения, не помешает ознакомиться с теми, которые поставляются в составе .NET Framework. Таковыми являются типы, доступные в пространстве имен System.Collections .Generic, которое уже несколько раз встречалось в коде, поскольку оно включается в состав консольных приложений по умолчанию. Использовать какие-либо из предлагаемых в этом пространстве имен типов пока не приходилось, но это скоро изменится. В настоящем разделе речь пойдет именно о типах в этом пространстве имен, а также о том, как их применять для создания строго типизированных коллекций и улучшения функциональных возможностей тех, что уже существуют. Первый делом, однако, будут рассмотрены другие, более простые обобщенные типы, позволяющие обходить уже упоминавшуюся ранее небольшую проблему с типами-значениями, а именно — нулевые типы (mailable types). Нулевые типы В предыдущих главах показывалось, что одним из отличий между типами-значениями (к числу которых относится большинство базовых типов, вроде int и double, а также структуры) и ссылочными типами (вроде string и любого класса) является то, что типы-значения должны обязательно содержать какое-то значение. Они могут существовать в состоянии без присвоенного значения в промежутке сразу же после их объявления и непосредственно перед присваиванием им значения, но пользоваться этим никак нельзя. В отличие от них, ссылочные типы могут быть нулевыми (т.е. принимать значение null).
Глава 12. Обобщения 343 Бывают ситуации, и возникают они гораздо чаще, чем может показаться (особенно во время работы с базами данных), в которых полезно иметь тип-значение, способный быть нулевым. Обобщения предлагают способ для получения такого типа, и заключается он в использовании типа System.Nullable<T>, как показано в следующем примере: System.Nullable<int> nullablelnt; В этой строке кода объявляется переменная по имени nullablelnt, которая может иметь любое такое же значение, как и переменная int, плюс значение null. Это позволяет использовать код, подобный показанному ниже: nullablelnt = null; Если бы nullablelnt не была переменной типа int, тогда скомпилировать предыдущий код было бы невозможно. Предыдущая операция присваивания эквивалента приведенной ниже: nullablelnt = new System.Nullable<int> (); Как и любую другую переменную, данную переменную нельзя использовать безо всякой предварительной инициализации, будь то присваивание значения null (с применением показанного выше синтаксиса) или какого-то другого значения. Нулевые типы можно проверять на предмет того, действительно ли в них содержится значение null, точно так же, как и ссылочные типы: if (nullablelnt == null) { } В качестве альтернативного варианта можно использовать свойство HasValue: if (nullablelnt.HasValue) { } В случае ссылочных типов такой код работать не будет, даже для типов, у которых имеется свое собственное свойство HasValue, поскольку наличие ссылочного типа со значением null означает отсутствие объекта, посредством которого можно получать доступ к этому свойству, и потому будет генерироваться исключение. Еще значение нулевого типа можно узнавать с помощью свойства Value. Если HasValue возвращает true, это означает, что значением свойства Value точно не является null. Но если HasValue возвращает false, тогда это означает, что переменной было присвоено значение null и поэтому получение доступа к Value закончится генерацией исключения System. InvalidOperationException. Обратите внимание на то, что нулевые типы являются настолько полезными, что привели даже к изменению синтаксиса С#. Вместо показанного выше синтаксиса для объявления переменной нулевого типа также можно использовать и такой синтаксис: int? nullablelnt; Здесь синтаксис int? представляет собой простой сокращенный вариант синтаксиса System.Nullable<int>, но является гораздо более удобным для восприятия, и потому в последующих разделах будет применяться именно он.
344 Часть I. Язык С# Операции и нулевые типы В случае простых типов вроде int для работы со значениями могут использоваться операции, подобные +, - и т.д. В случае эквивалентных им нулевых типов дело не меняется: значения, содержащиеся в нулевых типах, неявно преобразовываются в требуемый тип, и далее для работы с ними спокойно используются подходящие операции. То же самое касается и структур с предоставленными операциями: int? opl = 5; int? result = opl * 2; Обратите внимание, что здесь переменная result тоже относится к типу int?. Поэтому следующий код не будет компилироваться: int? opl = 5; int result = opl * 2; Чтобы исправить это, необходимо выполнить явное преобразование: int? opl = 5; int result = (int) opl * 2; Данный код будет работать, но только в случае присваивания opl какого-нибудь числового значения: в случае же присваивания opl значения null будет генерироваться исключение System.InvalidOperationException. Это вызывает вполне естественный вопрос о том, что же происходит, когда какое- то одно или оба значения в процессе вычисления операции оказываются равными null, как значение opl в предыдущем коде? Ответ таков: в случае всех простых нулевых типов, кроме bool?, вычисление операции завершается возвратом значения null, что на обычном языке можно выразить словами "не получается вычислить". В случае конструкций для обработки подобных ситуаций можно определять свои собственные операции (как будет показано позже в этой главе), а в случае типа bool? существуют предопределенные операции & и |, которые способны возвращать не null значения и показаны в табл. 12.1. Таблица 12.1. Возможные результаты операций & и | для операндов типа bool? opl true true true false false false null null null op2 true false null true false null true false null opl & op2 true false null false false false null false null opl | op2 true true true true false null true null null Результаты, приведенные в этой таблице, вполне понятны с логической точки зрения: если имеется достаточно информации для выяснения ответа в ходе вычисления и без знания значения одного из операндов, тогда нет никакой разницы, является значением этого операнда null или нет.
Глава 12. Обобщения 345 Операция ?? С целью еще большего сокращения количества кода, необходимого для работы с нулевыми типами, и упрощения кода, требуемого для работы с переменными, которые допускают присваивание им значения null, предусмотрена операция ??. Эта операция, еще также называемая операцией объединения с null, представляет собой бинарную операцию и позволяет предоставлять альтернативное значение для использования в тех выражениях, которые могут при вычислении возвращать null. Если значение первого операнда не равно null, она возвращает значение этого операнда, а если оно равно null, тогда она возвращает значение второго операнда. То есть два следующих выражения являются с функциональной точки зрения эквивалентными: opl ?? ор2 opl == null ? ор2 : opl В этом коде opl может представлять собой выражение любого типа, в том числе и ссылочного, а также, что более важно, нулевого типа. Это означает, что операцию ?? можно использовать для предоставления тех значений, которые должны использоваться по умолчанию в случае, если нулевой тип равен null, как показано ниже: int? opl = null; int result = opl * 2 ?? 5; Поскольку в этом примере opl равно null, opl * 2 тоже будет равно null. Однако операция ?? обнаруживает это и присваивает result значение 5. Очень важно обратить здесь внимание на то, что выполнять никакое явное преобразование для помещения результата в переменную result, являющуюся переменной целочисленного типа, не требуется. Операция ?? выполняет это преобразование сама. В качестве альтернативного варианта результат вычисления ?? можно без проблем поместить и в int?: int? result = opl * 2 ?? 5; Это делает операцию ? ? универсальной операцией для применения при работе с нулевыми переменными, а также удобным способом для предоставления значений по умолчанию, который позволяет не прибегать ни к добавлению какого-то блока кода в структуру if, ни к использованию тернарной операции, столь часто вызывающей путаницу. В следующем практическом занятии предоставляется возможность поэкспериментировать с нулевыми типами на примере типа Vector. актическое занятие Нулевые ТИПЫ 1. Создайте новый проект типа консольного приложения по имени Chi2Ех01 и сохраните его в каталоге С: \BegVCSharp\Chapterl2. 2. Добавьте в него новый класс Vector в файле Vector. cs. 3. Измените код в файле Vector. cs следующим образом: public class Vector { public double? R = null; public double? Theta = null; public double? ThetaRadians { get { // Преобразование градусов в радианы, return (Theta * Math.PI / 180.0); } }
346 Часть I. Язык С# public Vector(double? r, double? theta) { // Нормализация. if (r < 0) { r = -r; theta += 180; } theta = theta % 360; // Установка полей. R = r; Theta = theta; } public static Vector operator +(Vector opl, Vector op2) { try { // Получение координат (х, у) для нового вектора. double newX = opl.R.Value * Math.Sin(opl.ThetaRadians.Value) + op2.R.Value * Math.Sin(op2.ThetaRadians.Value); double newY = opl.R.Value * Math.Cos(opl.ThetaRadians.Value) + op2.R.Value * Math.Cos(op2.ThetaRadians.Value); // Выполнение преобразования в (г, theta). double newR = Math.Sqrt (newX * newX + newY * newY) ; double newTheta = Math.Atan2(newX, newY) * 180.0 / Math.PI; // Возврат результата. return new Vector(newR, newTheta); } catch { // Возврат "нулевого" вектора. return new Vector(null, null); } } public static Vector operator -(Vector opl) { return new Vector(-opl .R, opl.Theta); } public static Vector operator -(Vector opl, Vector op2) { return opl + (-op2); } public override string ToStringO { // Получение строкового представления координат. string rString = R.HasValue ? R.ToStringO : "null"; string thetaString = Theta.HasValue ? Theta.ToString() : "null"; // Возврап строки (г, theta). return string.Format (" ({0}, {1})", rString, thetaString); } }
Глава 12. Обобщения 347 4. Измените код в файле Program.cs, как показано ниже: class Program { static void Main(string[] args) { Vector vl = GetVector("vector1"); Vector v2 = GetVector("vectorl") ; Console.WriteLine("{0} + {1} = {2}", vl, v2, vl + v2) ; Console.WriteLine("{0} - {1} = {2}", vl, v2, vl - v2) ; Console.ReadKey() ; } static Vector GetVector(string name) { Console.WriteLine("Input {0} magnitude:", name); // Ввод модуля вектора double? г = GetNullableDoubleO; Console.WriteLine ("Input {0} angle (in degrees) : ", name); // Ввод угла вектора (в градусах) double? theta = GetNullableDoubleO; return new Vector (r, theta); } static double? GetNullableDoubleO { double? result; string userlnput = Console.ReadLine (); try { result = double.Parse(userlnput); } catch { result = null; } return result; } } 5. Запустите приложение и введите значения модуля и угла для двух векторов. На рис. 12.1 показан пример результата, который должен получиться. Рис. 12.1. Приложение Chl2Ex01 в действии
348 Часть I. Язык С# 6. Запустите приложение снова, но на этот раз пропустите ввод какого-то из четырех требуемых значений. На рис. 12.2 показан пример результата, который может получиться в таком случае. ■ m*^//C:/B^|VCSh«r|VChapter12K:h12Ex01/Ch12Ex01/btiVDeb*nyCh12Ex01.EXE Рис. 12.2. Пропуск ввода одного из значений в приложении СЫ2Ех01 Описание полученных результатов В этом примере создавался класс по имени Vector, представляющий вектор с полярными координатами (модулем и углом), как показано на рис. 12.3. Координаты г и О представлены в коде общедоступными полями R и Theta, причем значение поля Theta выражается в градусах. Для получения значения Theta в радианах предоставляется ThetaRad, а необходимо это потому, что в классе Math в его статических методах используются радианы. И R, и Theta являются значениями типа double?, а это значит, что они могут быть равны null: public class Vector { public double? R = null; public double? Theta = null; public double? ThetaRadians { get { // Преобразование градусов в радианы, return (Theta * Math.PI / 180.0); } } Конструктор класса Vector сначала нормализует первоначальные значения R и Theta, а затем устанавливает соответствующие общедоступные поля: public Vector(double? r, double? theta) { // Нормализация. if (г < 0) { г = -г; theta += 180; } theta = theta % 360; // Установка полей. R = г; Theta = theta; } .у ; Рис. 12.3. Вектор с полярными координатами
Глава 12. Обобщения 349 Основная функциональная возможность класса Vector состоит в сложении и вычитании векторов с помощью перегруженных операций, для которых здесь требуются лишь некоторые базовые познания в тригонометрии. Важным аспектом в этом коде является то, что в случае выдачи исключения при получении значения свойства Value поля R или ThetaRadians, т.е. в ситуации, когда значением какого-то из них оказывается null, возвращается "нулевой" вектор: public static Vector operator +(Vector opl, Vector op2) { try { // Получение координат (х, у) для нового вектора. } catch { // Возвращение "нулевого" вектора. return new Vector(null, null); } } Если какая-то из образующих вектор координат равна null, тогда вектор является недействительным, что здесь выражается в возврате Vector со значениями null в обоих полях (R и Theta). В остальной части кода в классе Vector переопределяются другие операции, требуемые для расширения функции сложения так, чтобы она могла выполнять и вычитание, а также переопределяется метод ToString () для получения строкового представления объекта Vector. Код в Program, cs тестирует класс Vector, предлагая пользователю инициализировать два вектора и затем складывая и вычитая их. В случае не указания пользователем какого-то из значений, оно интерпретируется как равное null, и тогда в силу вступают упоминавшиеся ранее правила. Пространство имен System.Collections.Generics Практически в каждом из демонстрировавшихся до сих пор приложений встречались следующие пространства имен: using System; using System.Collections .Generic- using System.Linq; using System.Text; В пространстве имен System содержится большинство из базовых типов, используемых в приложениях .NET. В пространстве имен System.Text находятся типы, касающиеся обработки и кодирования строк. О пространстве имен System.Linq будет рассказываться чуть позже в этой книге, начиная с главы 26. А какие типы предлагает пространство имен System.Collections.Generic и почему оно включается в состав консольных приложений по умолчанию? Ответ на этот вопрос выглядит так: в этом пространстве имен содержатся обобщенные типы, предназначенные для работы с коллекциями, и оно наверняка будет применяться настолько часто, уже заранее сконфигурировано с оператором using, чтобы его можно было использовать без явного указания его имени. Как и обещалось ранее в главе, сейчас будут рассмотрены типы из этого пространства имен, которые, несомненно, облегчают жизнь разработчика. В частности, они позволяют создавать строго типизированные классы коллекций практически без всяких усилий. В табл. 12.2 приведено краткое описание двух типов из пространст-
350 Часть I. Язык С# ва имен System.Collections .Generics, о которых пойдет речь в данном разделе. Другие типы из этого пространства имен будут рассматриваться позже в этой главе. Таблица 12.2. Два типа из пространства имен System. Collections. Generics, рассматриваемые в настоящем разделе Тип Описание List<T> Коллекция объектов типа т Dictionary<K, v> Коллекция элементов типа V, ассоциируемых с ключами типа к Еще в разделе будут описаны различные интерфейсы и делегаты, которые могут использоваться с этими классами. Тип List<T> Вместо того чтобы наследовать класс от CollectionBase и реализовывать необходимые методы, как это делалось в предыдущей главе, гораздо быстрее и легче воспользоваться обобщенным типом коллекции List<T>. Дополнительное преимущество здесь заключается в том, что многие из методов, которые обычно требуется реализовать самостоятельно, вроде Add (), при таком подходе реализуются автоматически. Для создания коллекции объектов типа Т применяется такой код: List<T> myCollection = new List<T>(); И все! Ни определять какие-либо классы, ни реализовать какие-либо методы, ни делать еще что-нибудь в подобном роде больше не требуется. Еще можно только задавать начальный список элементов в коллекции, передавая его конструктору List<T>. Объект, экземпляр которого создается с помощью такого синтаксиса, поддерживает методы и свойства, перечисленные в табл. 12.3 (где подразумевается, что типом, предоставляемым обобщению List<T>, является Т). Таблица 12.3. Методы и свойства List<T> Член Описание int Count Свойство, предоставляющее информацию о количестве элементов в коллекции void Add (T item) Добавляет элемент в коллекцию void AddRange (iEnumerable<T>) Добавляет несколько элементов в коллекцию iList<T> AsReadOnly () Возвращает доступный только для чтения интерфейс к коллекции int Capacity Получает или устанавливает количество элементов, которое может содержаться в коллекции void Clear () Удаляет все элементы из коллекции bool Contains (T item) Определяет, содержится ли элемент в коллекции void СоруТо (Т [ ] array, int index) Копирует элементы коллекции в массив array, начиная в массиве с индекса index IEnumerator<T> GetEnumerator () Получает экземпляр IEnumerator<T> ДЛЯ итерации по коллекции. Обратите внимание, что возвращаемый интерфейс является строго типизированным и относится к типу т, поэтому выполнять приведение в циклах f oreach не требуется
Глава 12. Обобщения 351 Окончание табл. 12.3 Член Описание int IndexOf (T item) void Insert (int index, T item) bool Remove (T item) void RemoveAt(int index) Получает индекс элемента или значение -1, если данный элемент не содержится в коллекции Вставляет элемент в коллекцию, используя указанный индекс Удаляет первое вхождение элемента из коллекции и возвращает true. Если элемент не содержится в коллекции, тогда возвращает false Удаляет из коллекции тот элемент, индекс которого соответствует указанному в index Еще у List<T> есть свойство Item, обеспечивающее подобный массивам доступ: Т itemAtIndex2 = myCollectionOfT [2] ; Этот класс поддерживает еще несколько других методов, но для начала работы это уже слишком много. В следующем практическом занятии демонстрируется применение типа List<T> на примере класса Collection<T>. J1**8*^ Применение типа List<T> 1. Создайте новое консольное приложение по имени Chl2Ex02 и сохраните его в каталоге С: \BegVCSharp\Chapterl2. 2. Щелкните правой кнопкой мыши на имени этого проекта в окне Solution Explorer и выберите в контекстном меню пункт Add"=>Add Existing Item (Добавить1^ Добавить существующий элемент). 3. Выберите из каталога C:\BegVCSharp\Chapterll\ChllEx01\ChllEx01 файлы Animal. cs, Cow. cs и Chicken. cs и щелкните на кнопке Add (Добавить). 4. Измените объявление пространства имен в каждом из трех добавленных файлов следующим образом: namespace Chl2Ex02 5. Измените код в файле Program, cs, как показано ниже: static void Main(string[] args) { List<Animal> animalCollection = new List<Animal>() ; animalCollection.Add(new Cow ("Jack")) ; animalCollection. Add (new Chicken ("Vera")) ; foreach (Animal myAnimal in animalCollection) { myAnimal.Feed(); } Console.ReadKey(); 6. Запустите приложение. Полученный результат должен выглядеть точно так же, как он выглядел в рассмотренном в предыдущей главе приложении ChllEx02.
352 Часть I. Язык С# Описание полученных результатов Между этим примером и примером ChllEx02 есть только два отличия. Первое состоит в том, что строка кода Animals animalCollection = new Animals(); была заменена такой строкой: List<Animal> animalCollection = new List<Ammal> () ; Второе и более важное отличие состоит в том, что в проекте больше нет класса коллекции Animals. Вся трудная работа, которая в предыдущем примере проводилась для создания этого класса, здесь была выполнена с помощью единственной строки кода за счет использования обобщенного класса коллекции. В качестве альтернативного варианта, такого же результата можно было бы добиться, сохранив код в Program, cs неизменным и воспользовавшись следующим определением для класса Animals: public class Animals : List<Animal> { } Такой подход имел бы свои преимущества, а именно— сделал бы код в файле Program, cs несколько более удобным для восприятия и плюс позволил бы добавлять в класс Animals дополнительные члены там, где это необходимо. Конечно, может возникнуть вопрос, а зачем вообще может потребоваться наследовать классы от CollectionBase, и это действительно хороший вопрос. На самом деле, ситуаций, в которых подобное может оказаться необходимым, существует немного. Естественно, знать о том, как класс CollectionBase работает внутри, полезно, потому что List<T> работает во многом аналогично, но в основном класс CollectionBase все-таки существует для обратной совместимости. Единственной реальной ситуацией, когда действительно может возникать желание применять класс CollectionBase, является та, когда требуется иметь гораздо больший контроль над членами, которые отображаются пользователям класса. Например, при желании создать класс коллекции с модификатором доступа internal в методе Add() использование CollectionBase может оказаться наилучшим вариантом. Еще можно передавать конструктору List<T> значение первоначальной вместимости списка (в виде int) или первоначальный список элементов с помощью интерфейса IEnumerable<T>. Класс List<T> входит в число классов, которые поддерживают этот интерфейс. Сортировка и поиск в обобщенных списках Сортировка обобщенного списка выглядит во многом так же, как и сортировка любого другого списка. В предыдущей главе уже рассказывалось о том, как использовать интерфейсы IComparer и IComparable для сравнения двух объектов и тем самым сортировать список объектов такого типа. Единственное отличие здесь состоит в том, что применять нужно обобщенные интерфейсы IComparer<T> и IComparable<T>, которые предоставляют немного другие, специфические для каждого типа методы. Все отличия этих методов кратко описаны в табл. 12.4.
Глава 12. Обобщения 353 Таблица 12.4. Отличия между обобщенными и не обобщенными методами Обобщенный метод Не обобщенный метод Отличие int IComparable<T>.CompareTo( Т otherObj) bool IComparable<T>.Equals( T otherObj) int IComparer<T>.Compare( T objectA, T objectB) bool IComparer<T>.Equals ( T objectA, T objectB) object.Equals() instead int IComparer<T>.GetHashCode( T objectA) int IComparable.CompareTo( object otherObj) int IComparer.Compare( object objectA, object objectB) В обобщенных версиях является строго типизированным. Не существует в не обобщенном интерфейсе; вместо него может использоваться унаследованный метод object.Equals() В обобщенных версиях является строго типизированным Не существует в не обобщенном интерфейсе; вместо него может использоваться унаследованный метод obj ect.Equals() Не существует в не обобщенном интерфейсе; вместо него может использоваться унаследованный метод object. GetHashCodeO Для сортировки List<T> может предоставляться или интерфейс IComparable<T> прямо в сортируемом типе, или интерфейс IComparer<T>. В качестве альтернативного варианта, методом сортировки может служить и обобщенный делегат. Данный подход является гораздо более интересным для рассмотрения, поскольку реализация упомянутых выше интерфейсов действительно ничем не отличается от реализации их не обобщенных аналогов. В двух словах, все, что необходимо для сортировки списка — это метод, сравнивающий два объекта типа Т, а для выполнения поиска— метод, проверяющий объект типа Т на предмет того, отвечает ли конкретным критериям. То есть все сводится к определению таких методов, и для оказания помощи в этом предусмотрены два соответствующих обобщенных типа-делегата, которые можно использовать. □ Comparison<T> — делегат для метода, применяемого для сортировки, со следующим возвращаемым типом и параметрами: int метод{Т objectA, T objectB) □ Predicate<T> — делегат для метода, применяемого для поиска, со следующим возвращаемым типом и параметрами: bool метод(Т targetObject) Можно определять любое количество таких методов и использовать их для "оснастки" методов сортировки и поиска в List<T>. В следующем практическом занятии представлен пример организации сортировки и поиска.
354 Часть I. Язык С# практическое замятие Выполнение сортировки и поиска в List<T> 1. Создайте новое консольное приложение по имени СЫ2ЕхОЗ и сохраните его в каталоге С: \BegVCSharp\Chapterl2. 2. Щелкните правой кнопкой мыши на имени этого проекта в окне Solution Explorer и выберите в контекстном меню пункт Add^Add Existing Item (Добавить1^ Добавить существующий элемент). 3. Выберите из каталога C:\BegVCSharp\Chapterl2\Chl2Ex01\Chl2Ex01 файл Vector. cs и щелкните на кнопке Add (Добавить). 4. Измените объявление пространства имен в добавленном файле следующим образом: namespace Chl2Ex03 5. Добавьте новый класс с именем Vectors. 6. Измените код в файле Vectors. cs, как показано ниже: public class Vectors : List<Vector> { public Vectors () { } public Vectors(IEnumerable<Vector> initialltems) { foreach (Vector vector in initialltems) { Add(vector); } } public string Sum() { StringBuilder sb = new StringBuilder(); Vector currentPoint = new Vector@.0, 0.0); sb.Append("origin"); foreach (Vector vector in this) { sb.AppendFormat(" + {0}", vector); currentPoint += vector; } sb.AppendFormat(" = {0}", currentPoint); return sb.ToString(); } } 7. Добавьте новый класс по имени VectorDelegates. 8. Измените код в файле VectorDelegates. cs следующим образом: public static class VectorDelegates { public static int Compare(Vector x, Vector y) { if (x.R > y.R) { return 1;
Глава 12. Обобщения 355 else if (x.R < y.R) { return -1; } return 0; } public static bool TopRightQuadrant(Vector target) { if (target.Theta >= 0.0 && target.Theta <= 90.0) { return true; } else { return false; } } } 9. Измените код в файле Program.cs, как показано ниже: static void Main(string [ ] args) { Vectors route = new Vectors (); route.Add(new Vector B.0, 90.0)); route.Add(new Vector A.0, 180.0)) ; route.Add(new Vector @.5, 45.0)); route.Add(new Vector B.5, 315.0)); Console.WriteLine(route.Sum()); Comparison<Vector> sorter = new Comparison<Vector>(VectorDelegates.Compare); route.Sort(sorter); Console.WriteLine(route.Sum ()); Predicate<Vector> searcher = new Predicate<Vector>(VectorDelegates.TopRightQuadrant); Vectors topRightQuadrantRoute = new Vectors(route.FindAll(searcher)); Console.WriteLine(topRightQuadrantRoute.Sum()); Console.ReadKey(); } 10. Запустите приложение. На рис. 12.4 показан результат, который должен получиться. ■ Шв^АС:УВвдУС5Ь*Г|УСИ1р1вг12Л:М2ЕхОЗЛ:М2ЕхОЗЛ)тД)вЬид/СМ2ЕхОЗ.ЕХЕ Г -mod irlfin • <2. 4SSM62J 1 > iriain • <8.' М'.,.И4Г.2П > > г if in • <И.« . 45 . 4'. \ 1 <1 • 2 НИ) ♦ 18И> УИ> <И.'. » <А <2. . 4S> • <2.'.. . ЧЮ • <2.S. 11 31 !»> HI .Л < 1 <l .t.ti- 511 1 «ЙГ.9214«М,> L1M9214S45 МИ77> 17И 17 И 12 НА 1 | Рис. 12.4. Приложение Chl2Ex03 в действии Описание полученных результатов В этом примере создается класс коллекции Vectors для класса Vector, созданного в СЫ2Ех01. Конечно, можно было бы и просто использовать переменную типа List<Vector>, но поскольку требуется дополнительная функциональность, используется новый класс — Vectors, унаследованный от List<Vector> и позволяющий добавлять какие угодно члены.
356 Часть I. Язык С# Одним из таких членов является метод Sum (). Он возвращает строку, в которой по очереди перечисляется каждый вектор, а также результат сложения этих всех векторов вместе (с помощью перегруженной операции из исходного класса Vector). Поскольку каждый вектор может пониматься как направление и расстояние, это, по сути, приводит к образованию маршрута с конечной точкой: public string Sum() { StringBuilder sb = new StringBuilder(); Vector currentPoint = new Vector @.0, 0.0); sb.Append("origin"); foreach (Vector vector in this) { sb.AppendFormat(" + {0}", vector); currentPoint += vector; } sb.AppendFormat(" = {0}", currentPoint); return sb.ToString(); } В этом методе для построения строки ответа применяется удобный класс StringBuilder, содержащийся в пространстве имен System. Text. У этого класса есть члены, вроде используемых здесь методов Append () и AppendFormat (), которые упрощают процесс сборки строки, обеспечивая более высокую производительность, чем может достигаться конкатенацией отдельных строк. Для получения результирующей строки тоже применяется метод этого класса, а именно — ToString (). Еще создаются два новых метода, подлежащие использованию в качестве делегатов, т.е. в качестве статических членов VectorDelegates. Метод Compare () предназначен для выполнения сравнения (сортировки), а метод TopRightQuadrant () — для поиска. О них более подробно будет рассказываться при рассмотрении кода в Program, cs. Код в Main () начинается с инициализации коллекции Vectors, в которую добавляется несколько объектов: Vectors route = new Vectors (); route.Add(new Vector B.0, 90.0)); route.Add(new Vector A.0, 180.0)); route.Add(new Vector @.5, 45.0)); route.Add(new Vector B.5, 315.0)); Метод Vectors. Sum () применяется для вывода элементов в коллекции, как уже отмечалось ранее, но на этот раз в их первоначальном порядке: Console.WriteLine(route.Sum()); Далее создается первый из делегатов— sorter. Этот делегат является делегатом типа Comparison<Vector> и потому ему может быть назначен метод со следующим возвращаемым типом и параметрами: int метод (Vector objectA, Vector objectB) Эта сигнатура соответствует сигнатуре метода VectorDelegates. Compare (), который потому далее и назначается этому делегату: Comparison<Vector> sorter = new Comparison<Vector>(VectorDelegates.Compare); Метод Compare () сравнивает модули двух векторов следующим образом: public static int Compare(Vector x, Vector y) { if (x.R > y.R)
Глава 12. Обобщения 357 { return l; } else if (x.R < y.R) { return -1; } return 0; } Это позволяет упорядочивать векторы по модулю: route.Sort(sorter) ; Console.WriteLine(route.Sum()); Вывод приложения дает результат, который и следовало ожидать — результат суммирования выглядит точно так же, потому что конечная точка следующего "векторного маршрута" остается той же, независимо от того порядка, в котором выполняются отдельные шаги. Далее посредством поиска получается подмножество векторов в коллекции. Это подразумевает использование метода VectorDelegates. TopRightQuadrant (): public static bool TopRightQuadrant(Vector target) { if (target.Theta >= 0.0 && target.Theta <= 90.0) { return true; } else { return false; } } Этот метод возвращает true, если в его аргументе Vector содержится значение Theta в диапазоне от 0 до 90 градусов, т.е. если вектор в диаграмме (вроде той, что показывалась ранее) указывает вверх и/или вправо. В методе Main () данный метод используется через делегат типа Predicate<Vector>, как показано ниже: Predicate<Vector> searcher = new Predicate<Vector>(VectorDelegates.TopRightQuadrant); Vectors topRightQuadrantRoute = new Vectors(route.FindAll(searcher)); Console.WriteLine(topRightQuadrantRoute.Sum()); Это требует определения в Vectors соответствующего конструктора: public Vectors(IEnumerable<Vector> initialltems) { foreach (Vector vector in initialltems) { Add(vector); } } Здесь новая коллекция инициализируется с использованием интерфейса IEnumerable<Vector>, что необходимо потому, что List<Vector>. FindAll () возвращает экземпляр List<Vector>, а не экземпляр Vectors. В результате выполнения поиска возвращается только подмножество объектов Vector, так что (как и следовало ожидать) результат суммирования выглядит по-дру-
358 Часть I. Язык С# гому. На привыкание к применению таких обобщенных типов делегатов для сортировки и поиска в обобщенных коллекциях может потребоваться некоторое время, но при таком подходе код получается более простым и эффективным с очень логичной структурой. Поэтому потратить время на изучения описанных в этом разделе приемов очень даже стоит. Тип Dictionary<K, V> Тип Dictionary<K, V> позволяет определять коллекцию пар "ключ-значение". В отличие от других обобщенных типов-коллекций, о которых рассказывалось в этой главе, данный класс требует создания экземпляров двух типов: одного для того ключа и одного для значения, представляющего каждый элемент в коллекции. После создания экземпляра объекта Dictionary<K, V> в нем можно выполнять во многом все те же операции, что и в любом классе, который наследуется от DictionaryBase, но только с помощью безопасным к типам методов и свойств. Можно, например, добавлять пары ключей и значений с помощью строго типизированного метода Add (): Dictionary<string, int> things = new Dictionary<string, int>(); things.Add("Green Things", 29); things.Add("Blue Things", 94); things.Add("Yellow Things", 34); things.Add("Red Things", 52); things.Add("Brown Things", 27); Можно проходить по ключам и значениям в коллекции с помощью свойств Keys и Values: foreach (string key in things.Keys) { Console.WriteLine(key) ; } foreach (int value in things.Values) { Console.WriteLine(value); } Помимо этого по элементам в коллекции еще можно проходить и получением каждого из них в виде экземпляра KeyValuePair<K, V>, во многом подобно тому, как это можно делать с объектами DictionaryEntry, которые демонстрировались в предыдущей главе: foreach (KeyValuePair<string, int> thing in things) { Console.WriteLine("{0} = {1}", thing.Key, thing.Value); } О Dictionary<K, V> важно запомнить одну вещь: ключ для каждого элемента должен обязательно быть уникальным. Попытки добавить элемент с ключом, идентичным ключу уже добавленного элемента, будут приводить к генерации исключения ArgumentException. Из-за этого Dictionary<K, V> позволяет передавать своему конструктору в качестве аргумента интерфейс I Compare r <К>. Подобное может быть необходимо в случае использования в качестве ключей своих собственных классов, которые не поддерживают ни интерфейса IComparable, ни интерфейса IComparable<K>, или при желании сделать так, чтобы объекты сравнивались посредством какого-то нестандартного процесса.
Глава 12. Обобщения 359 Например, в приведенном выше коде для сравнения строковых ключей можно было бы использовать метод, не учитывающий регистр: Dictionary<string, int> things = new Dictionary<string, int>(StringComparer.CurrentCulturelgnoreCase); Тогда исключение генерировалось бы в случае применения таких ключей, как показаны ниже: things.Add("Green Things", 29); things.Add("Green things", 94); Еще конструктору Dictionary^, V> можно также передавать начальную вместимость (с помощью int) и набор элементов (с помощью интерфейса IDictionary<K, V>) Изменение проекта Саг<1ЫЬдля использования обобщенной коллекции Теперь в проект CardLib, который разрабатывался на протяжения нескольких последних глав, можно внести одно простое изменение, заключающееся в модификации класса Cards таким образом, чтобы в нем использовался обобщенный класс коллекции, и тем самым сократить количество требуемых строк кода. Необходимое изменение, внести которое нужно в определение класса Cards, выглядит следующим образом: public class Cards : List<Card>, ICloneable { } Еще можно удалить все методы класса Cards, кроме Clone (), который требуется для ICloneable, и метода СоруТо (), потому что версия СоруТо (), которую предоставляет List<Card>, работает с массивом объектов Card, а не коллекцией Cards. В метод Clone () необходимо внести одну небольшую модификацию, поскольку в классе List<T> не определено свойство List, которое должно использоваться: public object Clone () { Cards newCards = new Cards (); foreach (Card sourceCard in this) { newCards.Add(sourceCard.Clone() as Card); } return newCards; } Полный код можно найти в загружаемом коде для этой главы (в проекте Chl2CardLib). Определение обобщений К этому моменту об обобщениях было рассказано вполне достаточно для того, чтобы можно было перейти к рассмотрению способов создания своих собственных обобщений: было показано и много кода с участием обобщенных типов, и много примеров с применением их синтаксиса на практике. Поэтому в настоящем разделе речь пойдет о способах определения обобщений, а именно об определении: □ обобщенных классов; □ обобщенных интерфейсов; □ обобщенных методов; □ обобщенных делегатов.
360 Часть I. Язык С# Здесь будут рассмотрены следующие усовершенствованные приемы для решения проблем, встречающихся при определении обобщенных типов: □ использование ключевого слова default; □ ограничение типов; □ наследование от обобщенных классов; □ применение обобщенных операций. Определение обобщенных классов Для создания обобщенного класса достаточно применить в определении класса синтаксис в виде угловых скобок: class MyGenericClass<T> { } Здесь на месте Т можно указывать какой угодно идентификатор, главное соблюдать стандартные правила именования языка С#, т.е. не начинать имя с числа и т.п. Обычно, однако, достаточно использовать и просто Т. Обобщенный класс может содержать в своем определении любое количество типов, отделенных друг от друга запятой: class MyGenericClass<Tl, Т2, Т3> { } После определения эти типы можно применять в определении класса точно так же, как и любые другие типы. Например, их можно использовать и в качестве типов для переменных членов, и в качестве возвращаемых типов для членов, вроде свойств или методов, и в качестве типов параметров для аргументов методов: class MyGenericClass<Tl, Т2, Т3> { private Tl innerTlObject; public MyGenericClass (Tl item) { innerTlObject = item; } public Tl InnerTlObject { get { return innerTlObject; } } } Здесь объект типа Tl может передаваться конструктору, и к нему разрешено получать доступ только для чтения через свойство InnerTlObject. Обратите внимание, что практически никаких предположений касательно того, какими являются предоставляемые классу типы, делать нельзя. Следующий код, например, компилироваться не будет:
Глава 12. Обобщения 361 class MyGenencClass<Tl, Т2, Т3> { private Tl innerTlObject; public MyGenericClass () { innerTlObject = new Tl() ; } public Tl InnerTlObject { get { return innerTlObject; } } } Поскольку не известно, каким является тип Т1, ни один из его конструкторов использовать нельзя — возможно, у него даже нет ни одного, а, может быть, у него нет общедоступного конструктора по умолчанию. Без сложного кода, подразумевающего применение приемов, которые будут показаны позже в этом разделе, по сути, о типе Т1 можно делать только предположение, что это тип, который либо унаследован от System.Object, либо может быть упакован в System.Object. Очевидно, это значит, что ничего особого интересного с экземплярами данного типа и любого из тех других типов, которые предоставляются обобщенному классу MyGenericClass, делать нельзя. Без применения рефлексии, которая представляет собой усовершенствованный прием, применяемый для изучения типов во время выполнения (и не рассматриваемый в этой главе), использовать, по сути, можно только сложный код, подобный показанному ниже: public string GetAllTypesAsString () { return "Tl = " + typeof (Tl) .ToStringO + ", T2 = " + typeof (T2) .ToStringO + ", T3 = " + typeof (T3) .ToStringO; } Помимо этого, разумеется, существует еще кое-что, что можно делать, особенно в случае коллекций, поскольку работа с группами объектов является довольно простым процессом и не требует никаких предположений о типах объектов, что и является одной из веских причин существования обобщенных классов коллекций, которые уже демонстрировались в этой главе. Другое ограничение, о котором тоже нужно знать, состоит в том, что при сравнении значения типа, указанного в обобщенном классе, со значением null, разрешено использовать только операцию == или ! =. То есть следующий код будет работать нормально: , public bool Compare (Tl opl, Tl op2) { if (opl != null && op2 != null) { return true; } else { return false; } }
362 Часть I. Язык С# Если Т1 представляет собой тип-значение, тогда всегда предполагается, что он не может быть нулевым, поэтому в предыдущем коде Compare будет всегда возвращать true. Однако при попытке сравнить два аргумента opl и ор2 компилятор будет выдавать ошибку: public bool Compare (Tl opl, Tl op2) { if (opl = op2) return true; else return false; } Объясняется это тем, что в данном коде предполагается, что Т1 поддерживает операцию ==. Короче говоря, для того чтобы делать нечто по-настоящему интересное с обобщениями, необходимо знать немного больше о типах, которые используются в классе. Ключевое слово default Одной из наиболее главных деталей, которые необходимо знать о типах, используемых для создания экземпляров обобщенных классов, является то, представляют они собой ссылочные типы или типы-значения. Не зная этого, нельзя даже присваивать значения null с помощью кода, показанного ниже: public MyGenericClass() { innerTlObject = null; } Если Tl представляет собой тип-значение, тогда innerTlObject не может иметь значение null, и этот код компилироваться не будет. К счастью, эта проблема была решена, и привело это к появлению нового способа использования для ключевого слова default (применение которого уже демонстрировалось в структурах switch ранее в главе). Выглядит этот способ так: public MyGenericClass() { innerTlObject = default (Tl) ; } Теперь в случае, если тип Т1 представляет собой ссылочный тип, innerTlObject будет присваиваться значение null, а в случае, если он представляет собой тип-значение — значение по умолчанию. Таким значением по умолчанию для числовых типов является 0, а у структур каждый из их членов аналогичным образом инициализируется в О или null. Ключевое слово default позволяет делать с назначаемыми обязательными типами немного больше, но для того, чтобы действительно пойти еще дальше, необходимо научиться ограничивать поставляемые типы. Ограничение типов Типы, которые использовались с обобщенными классами до сих пор, называются неограниченными (unbounded), поскольку никакие ограничения касательно того, какими именно они могут быть, на них не накладывались. Ограничения можно накладывать на те типы, которые могут использоваться для создания экземпляра обобщенного класса.
Глава 12. Обобщения 363 Для этого существует несколько способов. Например, можно ограничивать тип только тем, который наследуется от определенного типа. Если взять приведенные ранее классы Animal, Cow и Chicken, то в их случае можно было бы ограничить тип только унаследованным от класса Animal; тогда следующий код работал бы нормально: MyGenericClass<Cow>= new MyGenericClass<Cow>(); А приведенный ниже код, однако, приводил бы к выдаче во время компиляции сообщения об ошибке: MyGenericClass<string>= new MyGenericClass<string> () ; Достигается подобное поведение путем использования в определениях классов ключевого слово where: class MyGenericClass<T> where T : ограничение { } Здесь за то, что собой представляет накладываемое ограничение, отвечает синтаксис constraint. Подобным образом можно предоставлять и несколько ограничений, отделяя их друг от друга запятыми: class MyGenericClass<T> where T : ограничение1, ограничение! { } Определять ограничения можно как для одного любого, так и для всех требуемых обобщенным классом ограничений с использованием нескольких операторов where: class MyGenericClass<Tl, T2> where Tl : ограничение1 where T2 : ограничение! { } Любое из применяемых ограничений должно обязательно идти после указателей наследования: class MyGenericClass<Tl, T2> : MyBaseClass, IMylnterface where Tl : ограничение! where T2 : ограничение! { } Доступные варианты ограничений перечислены в табл. 12.5. В случае применения ограничения new (), оно должно обязательно указываться для типа последним. Посредством ограничения base-class можно применять параметр одного типа в качестве ограничения для другого, как показано ниже: class MyGenericClass<Tl, T2> where T2 : Tl { } Здесь Т2 должен обязательно представлять собой тот же тип, что и Т1, или наследоваться от Т1. Такое ограничение называется простым ограничением типа и означает, что параметр одного обобщенного типа применяется в качестве ограничения для другого.
364 Часть I. Язык С# Таблица 12.5. Доступные ограничения Ограничение Определение Пример использования struct class base-class interface new () Тип должен обязательно представлять собой тип-значение Тип должен обязательно представлять собой ссылочный тип Тип должен обязательно либо представлять собой, либо наследоваться от base-class. В таком ограничении разрешается предоставлять любое имя класса Тип должен обязательно либо представлять собой, либо реализовать interface Тип должен обладать общедоступным конструктором без всяких параметров В классе, которому для работы требуются типы-значения, например, в классе, где значение 0 у переменной экземпляра типа т имеет некий смысл В классе, которому для работы требуются ссылочные типы, например в классе, где значение null у переменной типа т имеет некий смысл В классе, которому для работы требуются определенные базовые функциональные возможности, унаследованные ОТ base-class В классе, которому для работы требуются определенные базовые функциональные возможности, обеспечиваемые interface В классе, где необходимо иметь возможность создавать переменные экземпляра типа т, к примеру, в конструкторе Циклические ограничения типов, вроде тех, что показаны ниже, запрещены: class MyGenericClass<Tl, Т2> where Т2 : Tl where Т1 : Т2 { } Этот код компилироваться не будет. В следующем практическом занятии демонстрируется пример определения и работы обобщенного класса с использованием семейства классов Animal, которые показывались в предыдущих главах. Практическое занятие Определение обобщенного класса 1. Создайте новое консольное приложение по имени Chi2Ex04 и сохраните его в каталоге С:\BegVCSharp\Chapterl2. 2. Щелкните правой кнопкой мыши на имени этого проекта в окне Solution Explorer и выберите в контекстном меню, которое появится после этого, пункт Add^Add Existing Item (Добавить1^Добавить существующий элемент). 3. Выберите файлы Animal, cs, Cow.cs, Chicken, cs, SuperCow.cs и Farm.cs из каталога C:\BegVCSharp\Chapterl2\Chl2Ex02\Chl2Ex02 и щелкните на кнопке Add (Добавить). 4. Измените объявление пространства имен в каждом из добавленных файлов следующим образом: namespace Chl2Ex04
Глава 12. Обобщения 365 5. Измените код в файле Animal. cs, как показано ниже: public abstract class Animal { public abstract void MakeANoise () ; } 6. Измените код в файле Chicken. cs следующим образом: public class Chicken : Animal { public override void MakeANoise () { Console.WriteLine("{0) says 'cluck!'", name); ) } 7. Измените код в файле Cow.cs следующим образом: public class Cow : Animal { public override void MakeANoise () { Console.WriteLine("{0} says 'moo!'", name); } } 8. Добавьте новый класс с именем SuperCow и измените код в файле SuperCow.cs следующим образом: public class SuperCow : Cow { public void Fly() Console.WriteLine ("{0} is flying!", name); public SuperCow(string newName) : base(newName) public override void MakeANoise () Console.WriteLine ("{0} says 'here I come to save the day!1", name); 9. Добавьте новый класс с именем Farm и измените код в файле Farm, cs, как показано ниже: using System; using System.Collections ; using System.Collections .Generic- using System.Linq; using System.Text;
366 Часть I. Язык С# namespace Chl2Ex04 { public class FarnKT> : IEnumerable<T> where T : Animal { private List<T> animals = new List<T>() ; public List<T> Animals { get { return animals; } } public IEnumerator<T> GetEnumerator () { return animals. GetEnumerator () ; } IEnumerator IEnumerable.GetEnumerator() { return animals. GetEnumerator () ; } public void MakeNoises () { foreach (T animal in animals) { animal.MakeANoise(); } } public void FeedTheAnimals () { foreach (T animal in animals) { animal.Feed(); } > public FarnKCow> GetCows() { Farm<Cow> cowFarm = new Farm<Cow>() ; foreach (T animal in animals) { if (animal is Cow) { cowFarm. Animals. Add (animal as Cow) ; > } return cowFarm; ) } } 10. Измените код в файле Program, cs следующим образом: static void Main(string[] args) { Farm<Animal> farm = new Farm<Animal>(); farm.Animals.Add(new Cow("Jack")); farm.Animals.Add(new Chicken("Vera")); farm.Animals.Add(new Chicken("Sally")); farm.Animals.Add(new SuperCow("Kevin"));
Глава 12. Обобщения 367 farm.MakeNoises(); Farm<Cow> dairyFarm = farm.GetCows(); dairyFarm.FeedTheAnimals(); foreach (Cow cow in dairyFarm) { if (cow is SuperCow) { (cow as SuperCow).Fly(); } } Console.ReadKey(); } 11. Запустите приложение. На рис. 12.5 показан результат, который должен получиться. Рис. 12.5. Приложение Chl2Ex04 в действии Описание полученных результатов В этом примере был создан обобщенный класс Farm<T>, который вместо того, чтобы наследоваться от обобщенного класса списка, предоставляет таковой в виде общедоступного свойства. Тип этого списка определяет параметр типа Т, который передается классу Farm<T> и ограничивается только типом, который либо сам является, либо унаследован от Animal: public class Farm<T> : IEnumerable<T> where T : Animal { private List<T> animals = new List<T>(); public List<T> Animals { get { return animals; } } Еще Farm<T> реализует интерфейс IEnumerable<T>, тоже с передачей ему типа Т, который, следовательно, имеет те же ограничения. Реализуется этот интерфейс для обеспечения возможности итерации по содержащимся в Farm<T> элементами без явного прохода по Farm<T>.Animals. Достигается это очень легко, а именно— возвратом перечислителя, предоставляемого Animals, каковым является класс List<L>, который тоже реализует интерфейс IEnumerable<T>: public IEnumerator<T> GetEnumerator () { return animals.GetEnumerator(); }
368 Часть I. Язык С# Поскольку IEnumerable<T> наследуется от IEnumerable, также необходимо реализовать и IEnumerable.GetEnumerator(): IEnumerator IEnumerable.GetEnumerator() { return animals.GetEnumerator(); } В состав Farm<T> входит два метода, которые пользуются методами абстрактного класса Animal: public void MakeNoisesO { foreach (T animal in animals) { animal.MakeANoise(); } } public void FeedTheAnimals() { foreach (T animal in animals) { animal.Feed(); } } Поскольку Т ограничен типом Animal, данный код компилируется нормально — доступ к этим методам гарантирован, каким бы на самом деле не был тип Т. Следующий метод — Get Cows () — более интересен. Он просто извлекает из коллекции все элементы, которые относятся к типу Cow (или наследуются от Cow, как, например, новый класс SuperCow): public Farm<Cow> GetCowsO { Farm<Cow> cowFarm = new Farm<Cow>(); foreach (T animal in animals) { if (animal is Cow) { cowFarm.Animals.Add(animal as Cow); } } return cowFarm; } Интересно здесь то, что данный метод кажется немного неэкономным. При желании иметь другие методы подобного рода, например, GetChickens () и т.п., их тоже нужно было бы реализовать явно. В системе с большим количеством типов потребовалось бы реализовать подобным образом еще больше методов. Поэтому гораздо более эффективным решением здесь было бы применение обобщенного метода, пример реализации которого еще будет демонстрироваться позже в главе. Клиентский код в Program.cs просто тестирует различные методы Farm и на самом деле не содержит ничего такого, о чем бы еще не рассказывалось, поэтому рассматривать этот код более детально не имеет смысла. Наследование от обобщенных классов Класс Farm<T> в предыдущем примере, а также несколько других классов, которые демонстрировались ранее в этой главе, наследовались от обобщенного типа. В слу-
Глава 12. Обобщения 369 чае класса Farm<T> этим типом был интерфейс IEnumerable<T>. Там ограничение, наложенное на тип Т в Farm<T>, привело к накладыванию соответствующего дополнительного ограничения и на тип Т, используемый в IEnumerable<T>. Подобный подход может быть удобным приемом для установки ограничений на иначе никак неограниченные типы, но, конечно, требует соблюдения кое-каких правил. Во-первых, нельзя "снимать" с типов ограничения, накладываемые в том типе, от которого выполняется наследование. Другими словами, тип Т, используемый в том типе, от которого выполняется наследование, должен обязательно ограничиваться как минимум настолько же, насколько и этот тип. Например, следующий код является допустимым: class SuperFarm<T> : Farm<T> where T : SuperCow { } Он будет работать, потому что Т ограничивается типом Animal в классе Farm<T>, a ограничение этого класса до класса SuperCow означает ограничение Т до подмножества именно таких значений. Следующий код, однако, компилироваться не будет: class SuperFarm<T> : Farm<T> where T : struct { } Здесь точно понятно, что тип Т, предоставляемый SuperFarm<T>, не может быть преобразован в тип Т, пригодный для использования в Farm<T>, поэтому код и не компилируется. Эта же проблема присутствует даже в ситуациях, когда ограничение является надмножеством: class SuperFarm<T> : Farm<T> where T : class { } Даже если типы, вроде Animal, и будут разрешены в Super Fa rm<T>, другие типы, удовлетворяющие ограничению class, все равно не будут допустимы в Farm<T>. To есть этот код, опять-таки, компилироваться не будет. Данное правило распространяется на абсолютно все типы ограничений, которые демонстрировались ранее в главе. Также важно обратить внимание на то, что в случае наследования от обобщенного типа нужно обязательно предоставлять всю необходимую информацию о типах либо в виде параметров других обобщенных типов, как показывалось выше, либо явно. То же самое касается и необобщенных классов, унаследованных от обобщенных типов, которые демонстрировались в других местах. Например: public class Cards : List<Card>, ICloneable { } Этот код будет компилироваться, а вот следующий — нет: public class Cards : List<T>, ICloneable { } Здесь не предоставляется никакой информации для Т, поэтому компиляция невозможна.
370 Часть I. Язык С# В случае предоставления параметра обобщенному типу, как было в List<Card> выгие, он может называться закрытым (closed) обобщенным типом, так же как и наследование от List<T> может аналогичным образом называться наследованием от открытого (open) обобщенного типа. Обобщенные операции Переопределенные операции реализуются в С# точно так же, как и другие методы, и, следовательно, могут реализоваться и в обобщенных классах. Например, в Farm<T> можно определить следующую операцию неявного преобразования: public static implicit operator List<Animal>(Farm<T> farm) { List<Animal> result = new List<Animal>(); foreach (T animal in farm) { result.Add(animal); } return result; } Это позволит при необходимости получать доступ к объектам Animal в Farm<T> напрямую как к List<Animal>. Подобное может оказаться удобным для сложения двух экземпляров Farm<T> с помощью, например, показанных ниже операций: public static Farm<T> operator +(Farm<T> farml, List<T> farm2) { Farm<T> result = new Farm<T>(); foreach (T animal in farml) { result.Animals.Add(animal); } foreach (T animal in farm2) { if (!result.Animals.Contains(animal)) { result.Animals.Add(animal); } } return result; } public static Farm<T> operator +(List<T> farml, Farm<T> farm2) { return farm2 + farml; } Это позволит складывать вместе экземпляры Farm<Animal> и Farm<Cow> следующим образом: Farm<Animal> newFarm = farm + dairyFarm; В этом коде dairyFarm (экземпляр Farm<Cow>) неявно преобразуется в экземпляр List<Animal>, который пригоден для использования в перегруженной операции + в Farm<T>. Может показаться, что подобного легко добиться и путем использования следующего кода:
Глава 12. Обобщения 371 public static FarnKT> operator +(FarnKT> farml, FamKT> £arm2) { Farm<T> result = new Farm<T>(); foreach (T animal in farml) { result.Animals.Add(animal); } foreach (T animal in farm2) { if (!result.Animals.Contains(animal)) { result.Animals.Add(animal); } } return result; } Однако поскольку Farm<Cow> не может преобразовываться Farm<Animal>, операция суммирования будет заканчиваться ошибкой. Чтобы устранить эту проблему, можно воспользоваться следующей операцией преобразования: public static implicit operator Farm<Animal>(Farm<T> farm) { Farm<Animal> result = new Farm<Animal>(); foreach (T animal in farm) { result.Animals.Add(animal); } return result; } При наличии такой операции экземпляры Farm<T>, вроде Farm<Cow> смогут преобразовываться в экземпляры Farm<Animal>, что устранит проблему. Применять можно любой из продемонстрированных методов, но последний все-таки более предпочтителен по причине простоты. Обобщенные структуры В предыдущих главах уже рассказывалось о том, что структуры выглядят, по сути, точно так же, как классы, и отличаются от них лишь незначительными деталями и тем, что представляют собой не ссылочные типы, а типы-значения. Благодаря этому, создавать обобщенные структуры можно аналогично обобщенным классам: public struct MyStruct<Tl, T2> { public Tl iteml; public T2 item2; } Определение обобщенных интерфейсов В этой главе уже встречалось несколько обобщенных интерфейсов: в частности, это были интерфейсы из пространства имен Systems .Collections .Generic вроде IEnumerable<T>, который использовался в последнем примере. Для определения обобщенных интерфейсов применяются те же самые приемы, что и для определения обобщенных классов:
372 Часть I. Язык С# interface MyFarmingInterface<T> where T : Animal { bool AttemptToBreed(T animall, T animal2); T OldestlnHerd { get; } } Здесь обобщенный параметр Т используется для указания типа в двух аргументах метода AttemptToBreed О и в свойстве OldestlnHerd. Правила наследования выглядят для обобщенных интерфейсов так же, как и для обобщенных классов. При выполнении наследования от базового обобщенного интерфейса, например, нужно не забывать о таком правиле, как сохранение ограничений параметров, представляющих обобщенный тип базового интерфейса. Определение обобщенных методов В последнем практическом занятии был использован метод Get Cows (), в пояснениях к которому было сказано, что можно было бы создать и более универсальную версию данного метода, применив обобщенный метод. В этом разделе как раз и будет показано, как подобное возможно. Обобщенный метод — это такой метод, в котором возвращаемый тип и/или типы параметров определяются параметром или параметрами обобщенного типа: public T GetDefault<T>() { return default (T); } В этом типичном примере, чтобы вернуть значение по умолчанию для типа Т используется рассматривавшееся ранее в главе ключевое слово default. Вызывается этот метод следующим образом: int myDefaultlnt = GetDefault<T>(); Отвечающий за тип параметр Т предоставляется во время вызова метода. Этот тип Т идет отдельно от типов, используемых для предоставления классам параметров обобщенного типа. На самом деле обобщенные методы могут реализоваться и не обобщенными классами: public class Defaulter { public T GetDefault<T>() { return default(T); } } Если класс является обобщенным, однако, тогда для типов обобщенного метода нужно обязательно использовать разные идентификаторы. Например, следующий код компилироваться не будет: public class De£aulter<T> { public T GetDefault<T>() t return default(T) ; 1 }
Глава 12. Обобщения 373 Здесь необходимо переименовать тип Т либо в методе, либо в классе. Ограничения могут использоваться параметрами обобщенного метода тем же образом, что и в классе, и применять в таком случае можно любые параметры типа класса: public class Defaulter<Tl> { public T2 GetDefault<T2>() where T2 : Tl { return default (T2); } } Здесь тип Т2, предоставляемый методу, должен обязательно либо совпадать, либо наследоваться от типа Т1, предоставляемого классу. Такой прием является наиболее типичным способом установки ограничений на обобщенные методы. Таким образом, получается, что в класс Farm<T>, который демонстрировался ранее, можно включить следующий метод (который присутствует в загружаемом коде для СЫ2Ех04, но в закомментированном виде): public Farm<U> GetSpecies<U> () where U : T { Farm<U> speciesFarm = new Farm<U>(); foreach (T animal in animals) { if (animal is U) { speciesFarm.Animals.Add(animal as U) ; } } return speciesFarm; } Это приведет к замене метода GetCows () и любых других методов такого же типа. На используемый здесь параметр обобщенного типа — U — накладывает ограничения параметр Т, на который, в свою очередь накладывает ограничения класс Farm<T>, который разрешает использовать для него только тип Animal. Это открывает возможность воспринимать экземпляры Т как экземпляры Animal, если в том вдруг возникнет необходимость. Применение такового нового метода требует внесения в содержащийся в Program. cs клиентский код для Chi2Ex04 одного изменения: Farm<Cow> dairyFarm = farm.GetSpecies<Cow>(); Точно также можно написать и следующее: Farm<Chicken> dairyFarm = farm.GetSpecies<Chicken>() ; или использовать любой другой класс, унаследованный от Animal. Здесь важно обратить внимание на то, что наличие параметров обобщенного типа в методе приводит к изменению его сигнатуры. Это означает, что может существовать и несколько перегрузок метода, отличающихся друг от друга только параметрами обобщенного типа, как показано в следующем примере: public void ProcessT<T> (T opl) { }
374 Часть I. Язык С# public void ProcessKT, U>(T opl) { } To, какой из методов должен использоваться, будет определять количество указываемых при вызове метода параметров обобщенного типа. Определение обобщенных делегатов Последним обобщенным типом, который осталось рассмотреть, является обобщенный делегат. Как обобщенные делегаты выглядят в действии, уже показывалось ранее в этой главе при рассмотрении способов сортировки и поиска в обобщенных списках, где для того и другого применялись, соответственно, делегаты Comparison<T> и Predicate<T>. В главе 7 рассказывалось, что делегаты определяются путем указания параметров и возвращаемого типа метода, а также ключевого слова delegate и имени делегата: public delegate int MyDelegate(int opl, int op2); Обобщенные делегаты определяются объявлением и использованием одного или более параметров обобщенного типа: public delegate Tl MyDelegate<Tl, T2>(T2 opl, T2 op2) where Tl : T2; Здесь видно, что ограничения могут накладываться и в случае обобщенных делегатов тоже и требуют соблюдения всех тех же правил. Более подробно о делегатах будет рассказываться в следующей главе, где помимо прочего еще будет демонстрироваться и то, как их можно использовать в таком распространенном в программировании на С# механизме, как события. Резюме В настоящей главе было показано, как использовать обобщенные типы в С# и каким образом создавать свои собственные обобщенные типы — обобщенные классы, интерфейсы, методы и делегаты. Здесь также было продемонстрировано и то, как использовать структуры, включая создание нулевых типов и работу с классами из пространства имен System.Collecitons.Generic. Обобщения, как можно было увидеть в этой главе, являются чрезвычайно мощным новым механизмом в С#. С их помощью можно создавать классы, способные служить сразу нескольким целям и подходящие для применения сразу в целом ряде разнообразных ситуаций. Даже если создавать свои собственные обобщенные типы нет причин, использовать обобщенные классы коллекций наверняка доведется постоянно. В следующей главе мы продолжим изучение базовых аспектов языка С#, а именно — уточним некоторые детали и рассмотрим события. Упражнения 1. Какие из следующих элементов могут быть обобщенными? • Классы • Методы • Свойства • Перегруженные версии операций • Структуры • Перечисления
Глава 12. Обобщения 375 2. Расширьте класс Vector в СЫ2Ех01 так, чтобы операция * возвращала скалярное произведение двух векторов. Под скалярным произведением двух векторов понимается результат перемножения их модулей, помноженный на косинус угла между ними. 3. Что не так в следующем коде? Исправьте ошибку. public class Instantiator<T> { public T instance; public Instantiator () { instance = new T () ; } } 4. Что не так в следующем коде? Исправьте ошибку. public class StringGetter<T> { public string GetString<T> (T item) { return item.ToString (); } } 5. Создайте обобщенный класс по имени ShortCollection<T>, реализующий IList<T> и состоящий из коллекции элементов с максимальным размером. Этот максимальный размер должен иметь вид целого числа, которое могло бы предоставляться конструктору ShortCollection<T> или по умолчанию устанавливаться в 10. Конструктор должен также быть способным принимать первоначальный список элементов через параметр List<T>. Сам класс должен функционировать точно так же, как Collection<T>, но генерировать исключение типа IndexOutOf RangeException при попытке добавить в коллекцию слишком много элементов или при наличии слишком большого количества элементов в переданном конструктору списке List<T>.
Дополнительные приемы объектно-ориентированного программирования В этой главе продолжается изучение языка С# и рассматриваются несколько механизмов и приемов, которые в принципе больше никуда особо не вписывались. Это вовсе не означает, что данные приемы не являются полезными, просто они не подпадают ни под какую из тех тем, которые рассматривались до этого. В частности, в настоящей главе рассматриваются следующие основные темы. □ Операция : : и спецификатор глобального пространства имен. □ Специальные исключения и связанные с ними рекомендации. □ События. □ Анонимные методы. Помимо этого здесь еще будут внесены окончательные изменения в проект CardLib, который разрабатывался в последних нескольких главах, и даже показано как его можно использовать для создания карточной игры. Операция :: и спецификатор глобального пространства имен Операция : : предоставляет альтернативный способ для получения доступа к типам в пространствах имен. Таковой может быть необходим при желании использовать псевдоним пространства имен и существовании неоднозначности между этим псевдонимом и фактической иерархией пространства имен. В таком случае иерархия пространства получает преимущество над псевдонимом. Чтобы увидеть, что это означает, рассмотрим следующий код:
Глава 13. Дополнительные приемы объектно-ориентированного... 377 using MyNamespaceAlias = MyRootNamespace.MyNestedNamespace; namespace MyRootNamespace { namespace MyNamespaceAlias { public class MyClass { } } namespace MyNestedNamespace { public class MyClass { } } } В коде в MyRootNamespace для ссылки на класс мог бы применяться такой синтаксис: MyNamespaceAlias.MyClass Классом, на который ссылается данный код, является MyRootNamespace. MyNamespace Alias .MyClass, а не MyRootNamespace.MyNestedNamespace.MyClass. To есть пространство имен MyRootNamespace.MyNamespaceAlias скрывает псевдоним, определенный в операторе using и ссылающийся на MyRootNamespace .MyNestedNamespace. Получать доступ к пространству имен MyRootNamespace .MyNestedNamespace и содержащемуся внутри него классу по-прежнему можно, но для этого требуется использовать другой синтаксис: MyNestedNamespace.MyClass В качестве альтернативного варианта можно применять операцию : :, как показано ниже: MyNamespaceAlias::MyClass Применение этой операции вынудит компилятор использовать псевдоним, определенный в операторе using и тогда код станет ссылаться на MyRootNamespace. MyNestedNamespace.MyClass. Вместе с операцией : : еще можно использовать и ключевое слово global, которое превращает ее, по сути, в псевдоним, ссылающийся на корневое пространство имен наивысшего уровня. Делать подобное может быть удобно для того, чтобы было еще яснее, о каком именно пространстве имен идет речь: global::System.Collections.Generic.List<int> Здесь конечным классом является именно тот, который и требовался — обобщенный класс коллекции List<T>. Но классом, определенным с помощью приведенного ниже кода, он точно не является: namespace MyRootNamespace { namespace System { namespace Collections { namespace Generic { class List<T> { }
378 Часть I. Язык С# Разумеется, нужно стараться не присваивать собственным пространствам имен такие имена, которые уже есть у пространств имен .NET, но такая проблема все равно может возникать в больших проектах, особенно при работе над ними в составе очень многочисленной команды. В подобных случаях применение операции : : и ключевого слова global может быть единственным способом получения доступа к требуемым типам. Специальные исключения В главе 7 рассказывалось об исключения и объяснялось, как для работы с ними можно использовать блоки try. . .catch. . . finally. Еще там говорилось о нескольких стандартных исключениях .NET, а также базовом классе для всех исключений — System.Exception. Однако иногда вместо стандартных исключений бывает удобнее использовать в приложениях свои собственные классы исключений, наследуя от базового класса. Такой подход позволяет более точно задавать информацию, которая должна передаваться отвечающему за перехват исключений коду, и те исключения, которые он должен обрабатывать. Например, он позволяет добавлять в класс исключения новое свойство, обеспечивающее доступ к какой-то базовой информации, и тем самым предоставлять получателю исключения возможность вносить необходимые изменения или просто получать больше информации о причине возникновения данного исключения. После определения класса исключения его можно добавить в список распознаваемых VS исключений, выбрав в меню Debug (Отладка) пункт Exceptions (Исключения) и выполнив в появившемся после этого окне щелчок на кнопке Add (Добавить), и затем конфигурировать его поведение так, как показывалось в главе 7. Базовые классы исключений Хотя классы специальных исключений и можно наследовать от базового класса System.Exception, как описывалось в предыдущем разделе, все-таки согласно наилучшим практическим методикам делать это не рекомендуется. Вместо этого лучше наследовать их от класса System.ApplicationException. В пространстве имен System существуют два фундаментальных и унаследованных от Exception класса исключений — ApplicationException и SystemException. Класс SystemException используется в качестве базового класса для предопределенных исключений, которые предлагаются в .NET Framework. Класс ApplicationException предназначен специально для того, чтобы разработчики могли наследовать от него свои собственные классы исключений. Если вы будете следовать этой модели, то всегда сможете отличать предопределенные и специальные исключения при перехвате исключений, унаследованных от одного из этих двух классов. Ни класс ApplicationException, ни класс SystemException никоим образом не расширяют функциональные возможности класса Exception. Они существуют исключительно для того, чтобы позволять разработчиками различать исключения описанным выше образом. Добавление специальных исключений в CardLib Легче всего проиллюстрировать способы применения специальных исключений, опять-таки, на примере модернизации проекта CardLib. В настоящее время метод Deck.GetCardO в этом проекте предусматривает генерацию стандартного исключения .NET при попытке получения доступа к карте с индексом меньше 0 и больше 51; предлагаем изменить его теперь так, чтобы в нем использовалось специальное исключение.
Глава 13. Дополнительные приемы объектно-ориентированного... 379 Чтобы сделать это, первым делом создайте новый проект типа библиотеки классов по имени Chl3CardLib и, как и раньше, скопируйте в него классы из Chl2CardLib, заменив, где требуется, пространство имен Chl2CardLib на Chl3CardLib. Далее определите исключение с помощью нового класса внутри нового файла CardOutOfRangeException.cs, который можете добавить в проект Chl3CardLib, выбрав в меню Project (Проект) пункт Add Class (Добавить класс), как показано ниже: public class CardOutOfRangeException : ApplicationException { private Cards deckContents; public Cards DeckContents { get { return deckContents; } } public CardOutOfRangeException (Cards sourceDeckContents) : base ("There are only 52 cards in the deck.") // В колоде имеется лишь 52 карты { deckContents = sourceDeckContents; } } Экземпляр класса Cards является обязательным для конструктора данного класса. Он обеспечивает доступ к данному объекту Cards через свойство DeckContents и предоставляет подходящее сообщение об ошибке конструктору базового класса Exception, чтобы оно было доступно через его свойство Message. Далее добавьте код для генерации специального исключения в Deck.cs (заменив им прежний код, который предназначался для выдачи стандартного исключения): public Card GetCard(int cardNum) { if (cardNum >= 0 && cardNum <= 51) return cards[cardNum]; else throw new CardOutOfRangeException(cards.Clone() as Cards) ; } Свойство DeckContents инициализируется после выполнения глубокого копирования текущего содержимого объекта Deck, которое имеет вид объекта Cards. Это дает возможность видеть, каким было это содержимое на момент генерации исключения, чтобы при последующем внесении изменений в содержимое колоды эта информация "не утрачивалась". Протестировать все это можно с помощью следующего клиентского кода (который также доступен и загружаемом коде для этого главы, в проекте Chl3CardClient): Deck deckl = new Deck() ; try { Card myCard = deckl.GetCardF0); } catch (CardOutOfRangeException e) { Console.WriteLine(e.Message); Console.WriteLine(e.DeckContents[0]); ' } Console.ReadKey ();
380 Часть I. Язык С# Выполнение этого код приведет к получению вывода, показанного на рис. 13.1. ^1ЦВЦДЛГ№1Д1х1ЖД1-1--13И1ИИИ1^^ИИ - Рис. 13.1. Генерация специального исключения в проекте CardLib Здесь перехватывающий код вывел на экран значение свойства Message объекта исключения. Еще была отображена первая карта в объекте Cards, полученном через свойство DeckContents, просто для подтверждения того, что к коллекции Cards можно получать доступ через объект, представляющий специальное исключение. События В этом разделе речь пойдет об одном из наиболее часто используемых механизмов ООП в .NET, а именно — о событиях (events). Сначала, как обычно, будет приведен краткий обзор того, что собой представляют события. Далее будут показаны некоторые простые события в действии и рассказано о том, что с ними можно делать. Напоследок вы узнаете о том, как создавать и использовать свои собственные события. В самом конце главы будет предоставлена возможность завершить разработку проекта библиотеки классов CardLib путем добавления в него события, а также, в качестве последнего этапа перед переходом к рассмотрению более сложных тем, немного поразвлечься и создать использующее эту библиотеку приложение для игры в карты. Что собой представляет событие События похожи на исключения тем, что они тоже генерируются, т.е. выдаются объектами, и тем, что для них тоже можно предоставлять реагирующий на них выполнением какого-нибудь действия код. Однако существует и несколько отличий, наиболее важное из которых состоит в отсутствии для обработки событий структуры, эквивалентной try. . . catch. Вместо применения этой структуры на события нужно подписываться (subscribe). Под подпиской на событие подразумевается предоставление кода, который должен выполняться при генерации данного события, в виде обработчика событий (event handler). На событие можно подписывать несколько обработчиков, которые тогда все будут вызываться при генерации этого события. Эти обработчики могут являться как частью того класса объекта, который генерирует данное событие, так и частью других классов. Сами обработчики событий представляют собой просто функции. Единственным ограничением для такой функции является то, что ее возвращаемый тип и параметры должны обязательно соответствовать тем, которых требует событие. Это ограничение входит в состав определения события и задается делегатом. Тот факт, что делегаты используются в событиях, как раз и делает их такими полезными. Именно поэтому им и было уделено некоторое внимание в главе 6. При необходимости можете перечитать приведенный там материал. Базовая последовательность обработки выглядит следующим образом: сначала приложение создает объект, который может генерировать событие. Например, рассмотрим приложение для мгновенного обмена сообщениями. Оно могло бы создавать объект, представляющий соединение с удаленным пользователем, и тогда этот объект мог бы генерировать событие при поступлении сообщения от удаленного пользователя по эгому соединению (рис. 13.2).
Глава 13. Дополнительные приемы объектно-ориентированного... 381 Приложение Создает Объект, представляющий соединение Рис. 13.2. Объект, генерирующий события Далее приложение подписывается на событие. Приложение для мгновенного обмена сообщениями могло бы делать это путем определения функции, пригодной для использования с указанным в событии типом делегата, и передачи ссылки на эту функцию событию. Эта функция-обработчик событий могла бы представлять собой метод в другом объекте, например, объекте дисплея, и отображать мгновенные сообщения на экране по мере их поступления (рис. 13.3). Приложение Создает Объект, представляющий соединение Объект, представляющий дисплей Подписывается на Рис. 13.3. Подписка на событие И, наконец, последнее: при генерации события подписчику отправляется соответствующее уведомление. Во взятом для примера приложении это означает, что при поступлении мгновенного сообщения через объект соединения в объекте, представляющем дисплей, вызывался бы метод обработчика событий. Поскольку используется стандартный метод, объект, генерирующий событие, может передавать любую имеющую отношение к делу информацию через параметры и тем самым делать события очень разнообразными. В случае взятого в качестве примера приложения одним из таких параметров мог бы быть текст мгновенного сообщения, который обработчик событий тогда бы мог отображать в объекте, представляющем дисплей, как показано на рис. 13.4. Приложение Объект, представляющий дисплей | Добрый день! J Объект, представляющий соединение Генерирует событие / / Вызывает Рис. 13.4. Генерация события
382 Часть I. Язык С# Обработка событий Как уже рассказывалось выше, для обработки события на него нужно подписываться, предоставляя функцию — обработчик событий, возвращаемый тип и параметры которой должны совпадать с возвращаемым типом и параметрами делегата, закрепленного для применения с этим событием. В следующем практическом занятии демонстрируется пример, в котором простой объект таймера используется для генерации событий, что приводит к вызову функции-обработчика. J^^^l^^ Обработка событий 1. Создайте новое консольное приложение по имени СЫЗЕх01 и сохраните его в каталоге С:\BegVCSharp\Chapterl3. 2. Измените код в его файле Program, cs следующим образом: using System; using System.Collections .Generic- using System.Linq; using System.Text; using System.Timers; namespace Chl3Ex01 { class Program { static int counter = 0; static string displaystring = "This string will appear one letter at a time. "; // Строка, которая будет отображаться по одной букве за раз static void Main(string[] args) { Timer myTimer = new Timer A00) ; myTimer. Elapsed += new ElapsedEventHandler (WriteChar) ; myTimer.Start() ; Console.ReadKey(); } static void WriteChar (object source, ElapsedEventArgs e) { Console.Write(displaystring[counter++ % displaystring.Length]); } } } 3. Запустите приложение (после запуска нажатие любой клавиши приводит к завершению приложения). Через некоторое время на экране появится результат, показанный на рис. 13.5. Рис. 13.5. Приложение Chl3Ex 01 в действии
Глава 13. Дополнительные приемы объектно-ориентированного... 383 Описание полученных результатов Объект, который используется для генерации событий, представляет собой экземпляр класса System. Timers. Timer. Инициализируется он с указанием временного интервала (в миллисекундах). Когда он запускается с помощью метода Start (), начинают генерироваться события через соответствующие заданному промежутки времени. В Main () объект Timer инициализируется с временным промежутком, составляющим 100 миллисекунд, а это означает, что при запуске он будет генерировать события 10 раз в секунду: static void Main(string[] args) { Timer myTimer = new Timer(lOO); Объект Timer инициирует событие Elapsed» и обработчик для этого события должен обязательно иметь такой же возвращаемый тип и параметры, как тип-делегат System.Timers.ElapsedEventHandler, который является одним из стандартных делегатов, поставляемых с .NET Framework. Возвращаемый тип и параметры этого делегата выглядят следующим образом: void functionName(object source, ElapsedEventArgs e); Объект Timer отправляет ссылку на самого себя в первом параметре и экземпляр объекта ElapsedEventArgs во втором параметре. Эти параметры вполне безопасно проигнорировать; более подробно они будут рассматриваться позже в главе. Для этого в коде присутствует подходящий метод: static void WriteChar(object source, ElapsedEventArgs e) { Console.Write(displayString[counter++ % displayString.Length]); } Этот метод использует два статических поля Classl — counter и displayString — для отображения одного символа. При каждом вызове этого метода отображается очередной символ. Л Следующим шагом является подключение этого обработчика к событию, т.е. подписку на него. Выполняется оно путем применения операции += для добавления обработчика в событие в виде нового экземпляра делегата, инициализируемого с методом обработки событий WriteChar: static void Main(string [ ] args) { Timer myTimer = new TimerA00); myTimer.Elapsed += new ElapsedEventHandler(WriteChar); Данная команда (в которой используется немного странно выглядящий синтаксис, являющийся специфическим синтаксисом делегатов) добавляет обработчик в список, который будет вызываться при генерации события Elapsed. В этот список можно добавлять сколько угодно обработчиков, главное, чтобы все они отвечали требуемым критериям. Каждый из этих обработчиков будет вызываться при генерации события по очереди. Все, что остается для функции Main () — это запуск таймера: myTimer.Start (); Нельзя, чтобы работа приложения завершалась до того, как будут обработаны те или иные события, поэтому функция Main () помещается в режим ожидания. Простейшим способом для этого является запрос ввода пользователя, поскольку такая
384 Часть I. Язык С# команда не будет завершать процесс обработки до тех пор, пока пользователь не нажмет какую-нибудь клавишу: Console.ReadKey(); Хотя процесс обработки в Main () на этом этапе фактически прекращен, процесс обработки в объекте Timer продолжается. Когда он генерирует события, он вызывает метод WriteCharO, который работает параллельно с оператором Console. ReadLine (). Определение событий Теперь пришла пора научиться определять и использовать свои собственные события. В следующем практическом занятии демонстрируется пример реализации сценария мгновенного обмена сообщениями, приведенного ранее в этой главе, с созданием объекта Connection, генерирующего события, за обработку которых отвечает объект Display. практическое занятие Определение событий 1. Создайте новое консольное приложение по имени СЫЗЕх02 и сохраните его в каталоге С: \BegVCSharp\Chapterl3. 2. Добавьте в него новый класс Connection и измените код в файле Connection. cs следующим образом: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Timers; namespace Chl3Ex02 { public delegate void MessageHandler (string messageText) ; public class Connection { public event Me ssageHandler MessageAr rived; private Timer pollTimer; public Connection () { pollTimer = new Timer A00) ; pollTimer. Elapsed += new ElapsedEventHandler (CheckForMessage) ; public void Connect () { pollTimer.Start(); } public void Disconnect () { pollTimer.Stop(); } private static Random random = new Random () ; private void CheckForMessage (object source, ElapsedEventArgs e) { Console.WriteLine("Checking for new messages.") ; // Проверка поступления новых сообщений
Глава 13. Дополнительные приемы объектно-ориентированного... 385 if ((random.Next(9) = 0) && (MessageArrived != null)) { MessageArrived("Hello Mum! ") ; 3. Добавьте новый класс Display и измените его код в файле Display, cs следующим образом: namespace Chl3Ex02 { public class Display { public void DisplayMessage (string message) { Console.WriteLine("Message arrived: {0}", message); // Поступило новое сообщение 4. Измените код в файле Program, cs показанным ниже образом: static void Main(string [ ] args) { Connection myConnection = new Connection (); Display myDisplay = new Display(); myConnection.MessageArrived += new MessageHandler (myDisplay.DisplayMessage); myConnection.Connect (); Console.ReadKey(); } 5. Запустите приложение. На рис. 13.6 показан результат, который должен получиться. -- w*3wj" | We:///C:/BegVCSharpyChipter13yCh13Ex02y... ЯД fill ш 1 зай Du 1 ]Ш№ щшш i J \ >| Рис. 13.6. Приложение Chl3Ex02 в действии
386 Часть I. Язык С# Описание полученных результатов Большую часть работы в этом приложении выполняет класс Connection. Экземпляры этого класса применяют объект Timer, во многом подобный тому, что показывался в первом примере этой главы, инициализируя его в конструкторе класса и предоставляя доступ к его состоянию (информации о том, включен он или выключен) через методы Connect () и Disconnect (): public class Connection { private Timer pollTimer; public Connection () pollTimer = new TimerA00); pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage); public void Connect() pollTimer.Start(); public void Disconnect () pollTimer.Stop (); } Еще в конструкторе регистрируется обработчик для события Elapsed, точно так же, как это делалось в первом примере. Метод этого обработчика — CheckForMessage () — предусматривает генерацию события в среднем один раз для каждых 10 раз его вызова. Его код еще будет поясняться, но сначала не помешает рассмотреть определение самого события. Перед определением события требуется обязательно определить подлежащий использованию с ним тип делегата, т.е. тип делегата, возвращаемому типу и параметрам которого должен соответствовать метод обработки событий. Для выполнения этого используется стандартный синтаксис делегатов, с помощью которого необходимый делегат определяется как общедоступный (pulbic) внутри пространства имен Chl3Ex02, чтобы он был доступен внешнему коду, как показано ниже: namespace Chl3Ex02 { public delegate void MessageHandler(string messageText); Данный делегат, имеющий здесь имя MessageHandler, представляет собой функцию void с одним единственным параметром string. Этот параметр можно использовать для передачи мгновенного сообщения, получаемого объектом Connection, объекту Display. После определения делегата (или установления месторасположения подходящего существующего делегата) можно переходить к определению самого события, в виде члена класса Connection: public class Connection { public event MessageHandler MessageArrived; Делается это просто выбором имени для события (каковым здесь является MessageAarived) и его объявлением с использованием ключевого слова event и указанием применяемого с ним типа-делегата (в данном случае — определенного ранее
Глава 13. Дополнительные приемы объектно-ориентированного... 387 типа-делегата MessageHandler). После объявления события подобным образом, его можно генерировать обращением к нему по имени так, будто бы оно является методом с возвращаемым типом и параметрами, заданными в делегате. Например, данное событие можно было бы сгенерировать следующим образом: MessageArrivedC'This is a message."); Если бы делегат был определен безо всяких параметров, тогда это можно было бы сделать так: MessageArrivedO ; В качестве альтернативного варианта, делегат мог еще быть определен и с большим числом параметров, что тогда бы потребовало написания большего количества кода для генерации события. Что касается метода CheckForMessage О , то его код выглядит так: private static Random random = new Random () ; private void CheckForMessage(object source, ElapsedEventArgs e) { Console.WriteLine("Checking for new messages."); if ( (random.Next(9) == 0) && (MessageArrived != null)) { MessageArrived("Hello Mum!"); } } Здесь применяется уже демонстрировавшийся в предыдущих главах класс Random для генерации случайного числа в диапазоне от 0 до 9 и, когда сгенерированным числом оказывается 0, что должно происходить в 10 процентах случаев, генерируется событие. Это имитирует опрос подключения для выяснения того, поступило ли новое сообщения, чего при каждой проверке происходить не будет. Для отделения таймера от экземпляра Connection используется приватный статический экземпляр класса Random. Обратите внимание на предоставляемую дополнительную логику, согласно которой событие должно генерироваться только в случае, если выражение MessageArrived ! = null в результате дает true. Это выражение, в котором снова используется несколько необычный синтаксис делегатов, по сути, задает следующий вопрос: "Имеются ли у события какие-то подписчики?". Если подписчиков нет, тогда MessageArrived вычисляется в null, и в генерации события нет никакого смысла. Класс, который будет подписываться на событие, называется Display и содержит единственный метод DisplayMessage (), определенный следующим образом: public class Display { public void DisplayMessage(string message) { Console.WriteLine("Message arrived: {0}", message); } } Этот метод соответствует типу делегата (и является общедоступным, что представляет собой требование для всех обработчиков событий, которые используются в классах, отличных от класса, генерирующего событие), поэтому его можно использовать для реагирования на событие MessageArrived. Теперь осталось только сделать так, чтобы код в Main() инициализировал экземпляры классов Connection и Display, подключал их и запускал весь процесс. Требуемый для этого код похож на тот, что был в первом примере:
388 Часть I. Язык С# static void Main(string[] args) { Connection myConnection = new Connection (); Display myDisplay = new Display (); myConnection.MessageAmved += new MessageHandler(myDisplay.DisplayMessage); myConnection.Connect (); Console.ReadKey(); } Здесь после запуска процесса с помощью метода Connect () объекта Connection вызывается метод Console . ReadKey () для приостановки процесса обработки в Main (). Универсальные обработчики событий Делегат, который демонстрировался ранее для события Timer.Elapsed, содержал два параметра, которые являются параметрами очень часто встречающегося в обработчиках событий типа, а именно: □ object source — ссылка на объект, который сгенерировал событие; □ ElapsedEventArgs e — параметры, отправленные событием. Причина, по которой тип object используется в данном событии и на самом деле во многих других событиях, состоит в том, что очень часто требуется использовать один обработчик событий для нескольких идентичных событий, генерируемых разными объектами, и при этом все равно знать, какой конкретно объект сгенерировал данное событие. Чтобы лучше объяснить и проиллюстрировать это, в следующем практическом занятии предлагается немного расширить предыдущий пример. Применение универсального обработчика событий 1. Создайте новое консольное приложение по имени СЫЗЕхОЗ и сохраните его в каталоге С: \BegVCSharp\Chapterl3. 2. Скопируйте код из файлов Program.cs, Connection.cs и Display.cs проекта Сп13Ех02 и замените названия пространств имен в каждом из них с СЫЗЕх02 на СЫЗЕхОЗ. 3. Добавьте новый класс MessageArrivedEventArgs и измените код в представляющем его файле MessageArrivedEventArgs . cs следующим образом: namespace СЫЗЕхОЗ { public class MessageArrivedEventArgs : EventArgs { private string message; public string Message { get { return message; }
Глава 13. Дополнительные приемы объектно-ориентированного... 389 public MessageAmvedEventArgs () { message = "No message sent."; // Сообщения не отправлялись } public MessageArrivedEventArgs(string newMessage) { message = newMessage; } } } 4. Измените код в файле Connection. cs следующим образом: namespace СЫЗЕхОЗ { public delegate void MessageHandler (Connection source, MessageArrivedEventArgs e) ; public class Connection { public event MessageHandler MessageArrived; private string name; public string Name { get { return name; } set { name = value; } } private void CheckForMessage(object source, EventArgs e) { Console.WriteLine("Checking for new messages."); // Проверка поступления новых сообщений if ( (random.Next(9) == 0) && (MessageArrived != null)) { MessageArrived (this, new MessageArrivedEventArgs ("Hello Mum! ")) ; } } } } 5. Измените содержимое файла Display. cs, как показано ниже: public void DisplayMessage(Connection source, MessageArrivedEventArgs e) { Console.WriteLine("Message arrived from: {0}", source.Name); // Поступило новое сообщение Console.WriteLine("Message Text: {0}", e.Message); // Вывод текста сообщения } 6. В завершение измените код в файле Program, cs следующим образом:
390 Часть I. Язык С# static void Main(string[] args) { Connection myConnectionl = new Connection (); myConnectionl .Name = "First connection."; // Первое подключение Connection myConnection2 = new Connection(); myConnection2.Name = "Second connection."; // Второе подключение Display myDisplay = new Display(); myConnectionl.MessageArrived += new MessageHandler(myDisplay.DisplayMessage); myConnection2.MessageArrived += new MessageHandler (myDisplay. DisplayMessage) ; myConnectionl.Connect(); myConnection2.Connect(); Console.ReadKey(); } 7. Запустите приложение. На рис. 13.7 показан результат, который должен получиться. Рис. 13.7. Приложение СЫЗЕхОЗ в действии Описание полученных результатов Путем отправки ссылки на генерирующий событие объект в виде одного из параметров обработчика событий можно настраивать ответ этого обработчика отдельным объектам. Эта ссылка открывает доступ к исходному объекту и его свойствам включительно. Отправкой параметров, содержащихся в классе, который наследуется от System. Event Args (как это делает класс ElapsedEventArgs), можно предоставлять любую необходимую информацию (вроде параметра Message в классе MessageArrivedEventArgs). Вдобавок эти параметры будут выигрывать от полиморфизма. Например, обработчик для события MessageArrived можно было бы определить так: public void DisplayMessage(object source, EventArgs e) { Console.WriteLine("Message arrived from: {0}", ( (Connection)source).Name); Console.WriteLine("Message Text: {0}", ((MessageArrivedEventArgs)e).Message); }
Глава 13. Дополнительные приемы объектно-ориентированного... 391 Тогда определение делегата в Connection. cs можно было бы изменить следующим образом: public delegate void MessageHandler(object source, EventArgs e); Данное приложение работало бы в точности так же, как и раньше, но его функция DisplayMessage () стала бы более разносторонней (по крайней мере, теоретически, поскольку на практике для достижения более высокого качества потребовалось бы больше реализации). Тот же самый обработчик смог бы работать и с другими событиями, такими как Timer .Elapsed, хотя пришлось бы изменить внутренние детали обработчика еще немного, чтобы параметры, отправляемые при генерации данного события, обрабатывались надлежащим образом (приведение их к типу объектов Connection и MessageArrivedEventArgs показанным способом приведет к генерации исключения; вместо этого должна использоваться операция as и выполняться проверка на предмет наличия в них значения null). Возвращаемые значение и обработчики событий У всех демонстрировавшихся до сих пор обработчиков событий возвращаемым типом был void. Предоставлять для события возвращаемый тип в принципе допускается, но это чревато возникновением проблем, поскольку каждое взятое событие может приводить к вызову нескольких обработчиков. Если все они возвращают значение, тогда может быть не ясно, какое именно значение было возвращено. Система справляется с этой проблемой, позволяя получать доступ только к последнему значению, которое было возвращено обработчиком событий. Таковым всегда будет значение, возвращенное обработчиком, который последним подписался на данное событие. Хотя эта функциональность и может оказаться полезной в некоторых ситуациях, все-таки рекомендуется применять обработчики событий с возвращаемым типом void и избегать параметров out. Анонимные методы Вместо того чтобы определять методы обработки событий, можно выбирать и другой вариант, а именно — использовать анонимные методы. Анонимным методом называется такой, который фактически не существует как метод в традиционном смысле, т.е. не является методом какого-либо определенного класса. Вместо этого анонимный метод создается исключительно для применения в качестве целевого метода для делегата. Синтаксис, необходимый для создания анонимного метода, выглядит следующим образом: delegate (параметры) { II Код анонимного метода. }; На месте параметры указывается перечень параметров, соответствующих параметрам делегата, экземпляр которого создается, в том виде, в котором они должны использоваться в коде анонимного метода: delegate(Connection source, MessageArrivedEventArgs e) { // Код анонимного метода, соответствующего событию MessageHandler в Chl3Ex03. }; Например, с помощью следующего кода можно было бы полностью пропустить метод Display.DisplayMessage () в СЫЗЕхОЗ:
392 Часть I. Язык С# myConnectionl.MessageArrived += delegate(Connection source, MessageArrivedEventArgs e) { Console.WriteLine("Message arrived from: {0}", source.Name); Console.WriteLine("Message Text: {0}", e.Message); }; Интересное свойство анонимных методов состоит в том, что они, по сути, являются локальными для того блока кода, в котором содержатся, и имеют доступ ко всем локальным переменным в этой области действия. В случае использования такой переменной она становится внешней (outer). Внешние переменные не уничтожаются при выходе за пределы области действия, как это происходит с другими локальными переменными; вместо этого они продолжают существовать до тех пор, пока не будут уничтожены методы, в которых они используются, что может случаться позже, чем ожидается, и потому обязательно требует внимательного отношения. Расширение и использование CardLib Теперь, когда было показано, как определять и использовать события, пришла пора попробовать применить полученные знания на практике в проекте Chl3CardLib. Событие, которое предлагается добавить, будет генерироваться при получении последнего объекта Card в объекте Deck с использованием GetCard и называться LastCardDrawn (Последняя вытянутая карта). Оно будет позволять подписчикам перемешивать колоду автоматически и тем самым сокращать объем подлежащей выполнению клиентом обработки. Делегат, определяемый для этого события (LastCardDrawnHandler), должен поставлять ссылку на объект Deck так, чтобы метод Shuffle () был доступен отовсюду, где бы ни находился обработчик. Это требует добавления в Deck.сs следующего кода: namespace Chl3CardLib { public delegate void LastCardDrawnHandler (Deck currentDeck) ; Код для определения описанного события и его генерации выглядит просто: public event LastCardDrawnHandler LastCardDrawn; public Card GetCard(int cardNum) { if (cardNum >= 0 && cardNum <= 51) { if ((cardNum = 51) && (LastCardDrawn !=null)) LastCardDrawn(this); return cards[cardNum]; else throw new CardOutOfRangeException((Cards)cards.Clone()); } Это и есть весь код, необходимый для добавления события в определение класса Deck. Клиентское приложение для игры в карты, использующее CardLib Потратив столько времени на разработку библиотеки CardLib, было бы нелепо не воспользоваться ею. Поэтому напоследок в этом разделе, посвященном дополнительным механизмам ООП в С# и .NET Framework, предлагается немного развлечься
Глава 13. Дополнительные приемы объектно-ориентированного... 393 и написать базовый код клиентского приложения для игры в карты, основанного на использовании уже знакомых классов игральных карт. Как и в предыдущих главах, сначала необходимо добавить в решение Chl3CardLib клиентское консольное приложение, назвать его Chl3CardClient, добавить в него ссылку на проект Chl3CardLib и сделать его стартовым проектом. Потом нужно создать в нем новый класс по имени Player, размещаемый в новом файле Player. cs. Этот класс должен содержать приватное поле типа Cards по имени Hand, приватное строковое поле с именем name и два доступных только для чтения свойства Name и PlayHand. Эти свойства будут просто предоставлять доступ к соответствующим приватным полям. Хотя свойство PlayHand и должно быть доступным только для чтения, к возвращаемой им ссылке на поле hand доступ должен быть не только для чтения, но и для записи, чтобы с ее помощью можно было изменять имеющиеся на руках у игрока карты. Еще необходимо скрыть конструктор по умолчанию, сделав его приватным, и предоставить общедоступный конструктор не по умолчанию, принимающий в качестве параметра первоначальное значение для свойства Name экземпляров Player. И, наконец, последнее, что потребуется сделать — это предоставить метод типа bool с именем HasWon (), возвращающий значение true только в том случае, если все карты на руках игрока относятся к одной масти (что является простым условием для победы). Ниже приведен код, который нужно добавить в Player. cs: using System; using System. Col lections. Generic- using System.Linq; using System.Text; using Chl3CardLib; namespace Chl3CardClient { public class Player { private Cards hand; private string name; public string Name { gat { return name; > ) public Cards PlayHand { get { return hand; } } private Player() { ) public Player (string newName) { name = newName; hand = new Cards () ; }
394 Часть I. Язык С# public bool HasWon() { bool won = true; Suit match = hand[0] . suit; for (int i = 1; i < hand.Count; i++) { won & = hand[i] .suit = match; > return won; } } } Далее необходимо определить класс, который будет отвечать за саму карточную игру. Этот класс называется Game и должен содержаться в проекте Chl3CardClient внутри файла Game. cs. У него должно быть четыре следующих приватных поля: □ playDeck — переменная типа Deck, содержащая используемую колоду карт; □ currentCard — значение типа int, применяемое в качестве указателя на следующую карту в используемой колоде; □ players — массив объектов Player, представляющих игроков игры; □ discardedCards — коллекция Cards, предназначенная для тех карт, которые были отброшены игроками, но не были перетасованы и помещены обратно в колоду. Стандартный конструктор этого класса должен выполнять следующие действия: инициализировать и перетасовывать объект Deck, хранящийся в playDeck, устанавливать для переменной-указателя currentCard значение 0 (соответствующее первой карте в playDeck) и подключать обработчик событий по имени Reshuffle () к событию playDeck.LastCardDrawn. Этот обработчик должен просто перетасовывать колоду, инициализировать коллекцию discardedCards и снова устанавливать для currentCard значение 0 в знак готовности к считыванию карт из новой колоды. Еще в классе Game должно содержаться два служебных метода: SetPlayersO для установки игроков для игры (в виде массива объектов Player) и DealHands () для раздачи карт на руки игрокам (по семь карт каждому). Допустимое количество игроков должно ограничиваться от 2 до 7, что гарантирует наличие достаточного числа карт для хождения по кругу. И, наконец, еще в классе Game должен присутствовать метод PlayGame (), содержащий логику самой игры. Мы вернемся к нему чуть позже, после рассмотрения кода в файле Program, cs. В файле же Game. cs получается, что вся остальная часть кода должна выглядеть следующим образом: using System; using System.Collections.Generic; using System.Linq; using System.Text; using Chl3CardLib; namespace Chl3CardClient { public class Game { private int currentCard; private Deck playDeck; private Player[] players; private Cards discardedCards;
Глава 13. Дополнительные приемы объектно-ориентированного... 395 public Game() { currentCard = 0; playDeck = new Deck(true); playDeck.LastCardDrawn += new LastCardDrawnHandler(Reshuffle); playDeck.Shuffle() ; discardedCards = new Cards(); } private void Reshuffle(Deck currentDeck) { Console.WriteLine("Discarded cards reshuffled into deck."); // Отброшенные карты перетасованы и помещены в колоду currentDeck.Shuffle(); discardedCards.Clear(); currentCard = 0; } public void SetPlayers(Player[] newPlayers) { if (newPlayers.Length > 7) throw new ArgumentException("A maximum of 7 players may play this" + " game."); // В эту игру может играть не более 7 игроков if (newPlayers.Length < 2) throw new ArgumentException("A minimum of 2 players may play this" + " game."); // В эту игру может играть не менее 2 игроков players = newPlayers; } private void DealHandsO { for (int p = 0; p < players.Length; p++) { for (int с = 0; с < 7; C++) { players[p].PlayHand.Add(playDeck.GetCard(currentCard++)); } } } public int PlayGameO { // Код, который должен выполняться дальше. } } } В файле Program, cs должна содержаться функция Main (), отвечающая за инициализацию и проведение игры. В частности, она должна выполнять перечисленные ниже шаги. □ Отображать вводную информацию. □ Приглашать пользователя указать количество игроков, каковых может быть не меньше двух, но и не больше семи. □ Создавать массив объектов Player соответствующим образом. □ Отображать каждому игроку приглашение ввести имя и использовать его для инициализации одного соответствующего объекта Player в массиве. □ Создавать объект Game и назначать игроков с помощью метода SetPlayers().
396 Часть I. Язык С# □ Запускать игру с помощью метода PlayGame (). □ Использовать значение int, возвращенное PlayGame (), для отображения сообщения о победителе (этим значением является индекс победившего игрока в массиве объектов Player). Код, необходимый для выполнения всех этих шагов, выглядит следующим образом (и для удобства снабжен комментариями): static void Main(string[] args) { // Отображение вводной информации. Console.WriteLine("KarliCards: a new and exciting card game."); // KarliCards: новая и увлекательная карточная игра Console .WriteLine ("To win you must have 7 cards of the same suit in" + "your hand."); // Для выигрыша необходимо, чтобы на руках оказалось 1/1 карт одной масти Console.WriteLine (); // Отображение приглашения указать количество игроков, bool inputOK = false; int choice = -1; do { Console.WriteLine("How many players B-7)?"); // Ввод количества игроков B-7) string input = Console.ReadLine(); try { // Попытка преобразовать введенные данные //в допустимое число игроков, choice = Convert.ToInt32(input); if ((choice >= 2) && (choice <= 7)) inputOK = true; } catch { // Игнорирование неудачных попыток преобразования //и продолжение отображения приглашения. } } while (inputOK == false); // Инициализация массива объектов Player. Player[] players = new Player[choice]; // Получение имен игроков. for (int p = 0; p < players.Length; p++) . { Console.WriteLine("Player {0}, enter your name:", p + 1); // Ввод имен игроков string playerName = Console.ReadLine (); players[p] = new Player(playerName); } // Запуск игры. Game newGame = new Game () ; newGame.SetPlayers(players); int whoWon = newGame.PlayGame(); // Отображение сообщения о победившем игроке. Console.WriteLine ("{0} has won the game!", players[whoWon].Name); }
Глава 13. Дополнительные приемы объектно-ориентированного... 397 Теперь дошло дело и до кода метода PlayGame (), представляющего собой основное тело всего приложения. Большинство деталей этого метода можно прояснить по комментариям в коде. Ничего сложного в нем нет; просто метод довольно велик. Игра продолжается просмотром каждым игроком карт на своих и руках и перевернутой карты на столе. Они могут либо брать эту карту, либо вытаскивать новую из колоды. После вытаскивания карты каждый игрок должен отбрасывать одну карту, либо заменяя карту на столе другой, если та была взята, либо помещая отбрасываемую карту поверх той, что лежит на столе (и тем самым также добавляя ее в коллекцию discardedCards). При просмотре этого кода, который, кстати, приведен ниже, нужно помнить о том, каким образом осуществляется манипулирование объектами Card. Причина того, почему эти объекты определяются как ссылочные типы, а не типы-значения (с помощью структуры), к этому моменту должна стать совершенно ясной. Любой взятый объект Card может существовать одновременно в нескольких местах, поскольку ссылка не него может присутствовать и в объекте Deck, и в полях hand объектов Player, и в коллекции discardedCards, и даже в объекте playCard (представляющем карту, выложенную на стол в текущий момент). Это упрощает отслеживание карт и, в частности, применяется в коде, отвечающем за вытаскивание новой карты из колоды. Карта принимается только в том случае, если ее нет ни на руках у игроков, ни в коллекции discardedCards. Выглядит весь необходимый для этого код следующим образом: public int PlayGame () { // Проводить игру только в том случае, если существуют игроки. if (players == null) return -1; // Выполнение первой раздачи карт на руки игрокам. DealHands(); // Инициализация имеющих отношение к игре переменных вместе с переменной, // представляющей первую карту, которая должна выкладываться // на стол, то есть переменной playCard. bool GameWon = false; int currentPlayer; Card playCard = playDeck.GetCard(currentCard++); discardedCards.Add(playCard); // Главный цикл игры, который должен продолжать выполняться до тех пор, // пока не будет соблюдено условие GameWon == true. do { // Проход по игрокам в каждом раунде игры. for (currentPlayer = 0; currentPlayer < players.Length; currentPlayer++) { // Считывание информации о текущем игроке, о том, какие карты имеются // у него на руках, и том, какая карта в текущей момент выложена на столе. Console.WriteLine("{0}'s turn.", players[currentPlayer].Name); Console.WriteLine("Current hand:"); foreach (Card card in players[currentPlayer].PlayHand) { Console.WriteLine(card); } Console.WriteLine("Card in play: {0}", playCard); // Вывод игроку приглашения взять карту со стола или вытащить новую, bool inputOK = false;
398 Часть I. Язык С# do { Console.WriteLine ("Press T to take card in play or D to " + "draw:"); // Нажмите Т, чтобы взять разыгранную карту, // или D, чтобы вытащить новую string input = Console.ReadLine(); if (input.ToLowerO == "t") { // Добавление карты со стола на руки пользователю. Console.WriteLine("Drawn: {0}", playCard); // Избавление от отброшенных карт, если возможно (в случае // перемешивания колоды их там больше быть не должно) . if (discardedCards.Contains(playCard)) { discardedCards.Remove(playCard); } players[currentPlayer].PlayHand.Add(playCard); inputOK = true; } if (input.ToLowerO == "d") { // Добавление на руки пользователю новой карты из колоды. Card newCard; // Добавление карты только в том случае, если ее нет // ни на руках у игроков, ни в стопке отброшенных карт. bool cardlsAvailable; do { newCard = playDeck.GetCard(currentCard++); f // Выполнение проверки на предмет того, не находится //ли данная карта в стопке отброшенных карт. cardlsAvailable = !discardedCards.Contains(newCard); if (cardlsAvailable) { // Просмотр карт на руках у всех игроков для выяснения того, //не находится ли карта newCard у кого-нибудь из них. foreach (Player testPlayer in players) { if (testPlayer.PlayHand.Contains(newCard)) { cardlsAvailable = false; break; } } } } while (!cardlsAvailable); // Выдача вытащенной карты на руки игроку. Console.WriteLine("Drawn: {0}", newCard); players[currentPlayer].PlayHand.Add(newCard); inputOK = true; } } while (inputOK == false);
Глава 13. Дополнительные приемы объектно-ориентированного... 399 // Отображение новой раскладки на руках у игрока с нумерацией карт. Console.WriteLine("New hand:"); for (int i = 0; i < players [currentPlayer] .PlayHand.Count; i++) { Console.WriteLine("{0}: {1}", i + 1, players[currentPlayer].PlayHand[i]); } // Отображение игроку приглашения отбросить какую-нибудь карту. inputOK = false; int choice = -1; do { Console.WriteLine("Choose card to discard:"); // Выберите карту для отбрасывания string input = Console.ReadLine(); try { // Попытка преобразовать введенные // данные в допустимый номер карты, choice = Convert.ToInt32(input); if ((choice > 0) && (choice <= 8)) inputOK = true; } catch { // Игнорирование неудачных попыток преобразования //и продолжение вывода приглашения. } } while (inputOK == false); // Помещение ссылки на удаляемую карту в playCard (выкладывание // карты на стол), затем изъятие карты из рук игрока //и добавление ее в стопку отброшенных карт. playCard = players[currentPlayer].PlayHand[choice - 1]; players[currentPlayer].PlayHand.RemoveAt(choice - 1); discardedCards.Add(playCard); Console.WriteLine("Discarding: {0}", playCard); // Отбрасывание карты // Вывод пустой строки для удобства. Console.WriteLine (); // Выполнение проверки на предмет того, выиграл ли игрок //в этой игре, и если да, осуществление выхода из цикла. GameWon = players [currentPlayer] . HasWonO; if (GameWon == true) break; } } while (GameWon == false); // Завершение игры с указанием выигравшего игрока, return currentPlayer; } На рис. 13.8 показана эта игра в действии.
400 Часть I. Язык С# Рис. 13.8. Карточная игра в действии Поиграйте в эту игру и не поленитесь детально изучить ее. Попробуйте разместить в методе Reshuffle () точку останова и сыграть в игру с участием семи игроков. Вытаскивание и отбрасывание вытянутых карт будет быстро приводить к перетасовке колоды, потому что при наличии семи игроков будет оставаться только три карты для избавления. Так вы сможете удостовериться в том, что все работает правильно, просто обращая внимание на то, когда эти три карты будут появляться снова. Резюме В этой главе были описаны некоторые более сложные приемы, расширяющие знания по языку С#. Далее перечислены ключевые моменты, с которыми вы ознакомились в этой главе. □ Квалификация наименований типов в пространствах имен (с более детальным описанием, чем приводилось в предыдущих главах). □ Применение операции : : и ключевого слова global для гарантии того, что ссылки на типы являются ссылками именно на те типы, которые требуются. □ Реализация собственных объектов исключений и передача более детальной информации обработчикам исключений.
Глава 13. Дополнительные приемы объектно-ориентированного... 401 □ Использование специального исключения в коде для CardLib— библиотеки карточной игры, которая разрабатывалась в последних нескольких главах. □ События и обработка событий, что является очень важной темой. Требуемый код, хотя и довольно замысловат и непрост для изучения в начале, на самом деле является достаточно простым; к тому же, с обработчиками событий в настоящей книге придется встретиться еще не раз. □ Некоторые наглядные примеры событий и способы их обработки. В главе снова были внесены изменения в проект CardLib, после чего он был использован для создания простого приложения карточной игры. Это приложение позволило проиллюстрировать практически все из тех приемов, которые рассматривались в этой книге до сих пор. Вместе с этой главой подошло к концу не только описание всех приемов ООП, применяемых в программировании на С#, но и описание всей версии языка С# 2.0. В следующей главе речь пойдет о новых функциональных возможностях С#, которые появились в версии С# 3.0. Упражнения 1. Напишите с использованием универсального синтаксиса (object sender, EventArgs e) код обработчика событий, способного принимать от приведенного ранее в этой главе кода либо событие Timer .Elapsed, либо событие Connection.MessageArrived. Этот обработчик должен выводить на экран строку, сообщающую о том, событие какого типа было получено, вместе со свойством Message параметра MessageArrivedEventArgs или свойством SignalTime параметра ElapsedEventArgs в зависимости от того, какое событие произошло. 2. Модифицируйте пример приложения карточной игры так, чтобы в нем выполнялась проверка на предмет соблюдения такого же более интересного условия выигрыша, как и в популярной карточной игре "рамми". В соответствие с правилами, выигравшим считается игрок, у которого на руках окажется два "набора" карт — один, состоящий из трех карт, и второй, состоящий из четырех карт. Под набором подразумевается либо последовательность карт одной масти (например, 3 червей, 4 червей, 5 червей, 6 червей), либо несколько карт одного достоинства (например, 2 червей, 2 пик, 2 бубен).
Расширения в языке С# 3.0 Язык С# не стоит на месте. Андерс Хейлсберг (Anders Hejlsberg), автор языка С#, и другие разработчики в Microsoft продолжают обновлять и шлифовать его. На момент написания настоящей книги самые последние изменения были представлены в версии С# 3.0, вышедшей в составе линейки продуктов Visual Studio 2008. К этому этапу у вас может возникнуть вопрос, что же еще могло потребоваться; и действительно в плане функциональных возможностей в предыдущих версиях С# мало чего не достает. Это, однако, вовсе не означает, что разработчики языка С# не могут упрощать некоторые аспекты программирования на С# или модернизировать отношения между С# и другими технологиями. Возможно, легче всего объяснить это, вспомнив о дополнении, которое появилось между версиями 1.0 и 2.0— обобщения. Хотя обобщения и являются чрезвычайно полезными, никаких функциональных возможностей, которых нельзя было бы получить ранее, они на самом деле не предоставляют. Несомненно, они значительно упрощают дело, и без них пришлось бы писать гораздо больше кода. Вряд ли кто-то захотел бы вернуться обратно в те времена, когда еще не было обобщенных классов коллекций. И, тем не менее, существенной частью языка С# обобщения не являются. Но зато улучшением в этом языке их, несомненно, назвать можно. Расширения в версии С# 3.0 выглядят во многом так же. Они предлагают новые способы для получения тех или иных результатов, которых раньше было трудно достигать без применения длинных и/или сложных приемов программирования. О двух таких новых средствах в версии С# 3.0, как автоматические свойства и частичные методы, уже рассказывалось в этой книге (см. главу 10). В обоих случаях можно было заметить, что подобные изменения скорее влияют на компиляцию кода С#, чем делают нечто совершенно новое. Это является общим свойством всех расширений С#, которое лишь подтверждает тот факт, что никаких значительных изменений в таких улучшениях не наблюдается.
Глава 14. Расширения в языке С# 3.0 403 В настоящей главе рассматриваются следующие расширения: □ инициализаторы; □ выведение типов; □ анонимные типы; □ методы расширения; □ лямбда-выражения. Пожалуй, наибольшим изменением в языке С# является технология LINQ (Language Integrated Query — язык интегрированных запросов), которая будет рассматриваться позже, в главах 26-29. Она представляет собой новый способ для манипулирования коллекциями объектов, которые могут происходить из ряда различных источников, в том числе из базы данных и XML-документа. Многие из усовершенствований, которые будут рассматриваться в настоящей главе, в основном используются вместе с LINQ, хотя сами по себе являются расширениями. Инициализаторы В предыдущих главах уже показывалось, как создавать экземпляры объектов и инициализировать их различными способами. В каждом случае для этого требовалось либо добавлять в определения классов дополнительный код для обеспечения инициализации, либо создавать экземпляры объектов и инициализировать их с помощью отдельных операторов. Также демонстрировалось и то, как создавать классы коллекций различных типов, обобщенные классы коллекций включительно. Опять-таки, можно было заметить, что никакого простого способа для объединения создания коллекции с добавлением в нее элементов не существовало. Инициализаторы (initializers) объектов предоставляют способ для упрощения кода, позволяя объединять операцию создания экземпляров и операцию инициализации объектов. Инициализаторы коллекций предлагают простой элегантный синтаксис для создания и заполнения коллекций за один шаг. В этом разделе объясняется, как использовать оба новых механизма. Инициализаторы объектов Рассмотрим следующее простое определение класса: public class Curry { public string Mainlngredient {get; set;} public string Style {get; set;} public int Spiciness {get; set;} } У этого класса есть три свойства, определенные с применением синтаксиса автоматических свойств, который демонстрировался в главе 10. При желании создать и инициализировать экземпляр объекта этого класса нужно использовать несколько следующих операторов: Curry tastyCurry = new Curry(); tastyCurry.Mainlngredient = "panir tikka"; tastyCurry.Style = "jalfrezi"; tastyCurry.Spiciness = 8; Если не включить в определение класса никакого конструктора, для этого кода будет использоваться конструктор по умолчанию, не принимающий параметров, кото-
404 Часть I. Язык С# рый предоставляется компилятором С#. Для упрощения этой операции инициализации можно предусмотреть соответствующий конструктор не по умолчанию: public class Curry { public Curry(string mainlngredient, string style, int spiciness) { Mainlngredient = mainlngredient; Style = style; Spiciness = spiciness; } } '"" Это позволит написать следующий код и объединить операцию создания экземпляра с операцией инициализации: Curry tastyCurry = new Curry("panir tikka", "jalfrezi", 8); Этот код будет работать нормально, но еще также будет вынуждать код, использующий данный класс, применять именно этот конструктор и тем самым не позволять работать предшествующему коду, в котором использовался конструктор без параметров. Поэтому зачастую, а особенно в тех случаях, когда классы должны допускать сериали- зацию, необходимо также предоставлять и конструктор без параметров: public class Curry { public Curry() { } } '" Теперь создавать экземпляр класса Curry и инициализировать его можно любым способом, но чтобы добиться этого, в первоначальное определение класса пришлось добавить нескольких строк кода, которые не делают ничего, кроме предоставления базовых деталей, необходимых для того, чтобы все работало. Инициализаторы объектов представляют собой способ, позволяющий создавать экземпляры и инициализировать объекты без добавления дополнительного кода (вроде описанного в предыдущем примере кода конструкторов). Они подразумевают предоставление при создании экземпляра объекта значений для общедоступных свойств или полей с использованием пары "имя-значение" для каждого свойства, которое требуется инициализировать. Необходимый для этого синтаксис выглядит так: имя_класса имя_переменной = new имя_класса { свойство_или_поле1 = значение1, свойство_или_поле2 = значение2, свойство_или_полеЫ = valueN }; Например, код для создания экземпляра и инициализация объекта типа Curry, который приводился ранее, можно было бы переписать следующим образом: Curry tastyCurry = new Curry { Mainlngredient = "panir tikka", Style = "jalfrezi", Spiciness = 8 };
Глава 14. Расширения в языке С# 3.0 405 Часто в случае применения инициализатора объекта явно вызывать конструктор класса нельзя (в синтаксисе инициализаторов объектов не разрешено использовать круглые скобки). Вместо этого автоматически вызывается конструктор по умолчанию без параметров. И происходит это до установки инициализатором каких-либо значений параметров, что позволяет при желании предоставлять в конструкторе по умолчанию для параметров стандартные значения. В случае если никакой конструктор для класса не предусмотрен, предоставляемый компилятором общедоступный конструктор по умолчанию тоже будет работать вполне хорошо. Из-за такого автоматического вызова конструктора, чтобы инициализаторы объектов работали, им необходимо иметь доступ к конструктору класса по умолчанию. Поэтому в случае добавления конструктора не по умолчанию с параметрами также требуется предоставлять и конструктор по умолчанию, точно так же, как было в предыдущем примере, где конструкторы применялись для инициализации свойств. Если одно из свойств, которые нужно инициализировать с помощью инициализатора объектов, сложнее свойств простых типов, которые были показаны в данном примере, тогда может потребоваться использовать вложенный инициализатор объектов. Это подразумевает применение точно такого же синтаксиса, как уже приводился: Curry tastyCurry = new Curry { Mainlngredient = "panir tikka", Style = "jalfrezi", Spiciness = 8, Origin = new Restaurant {Name = "King's Balti", Location = "York Road", Rating = 5} }; Здесь инициализируется свойство с именем Origin типа Restaurant (который тут не показан). В частности, в представляющей его строке кода вместе с ним инициализируется еще и три таких свойства, как Name, Location и Rating, со значениями, соответственно, типа string, string и int. Получается, что в этом коде инициализации используется вложенный инициализатор объектов. Обратите внимание, что инициализаторы объектов не являются заменителями конструкторов не по умолчанию. Тот факт, что их можно использовать для установки значений свойств и полей при создании экземпляра объекта, вовсе не означает, что всегда будет известно, какое именно состояние нужно инициализировать. В случае же применения конструкторов можно точно указывать, какие значения требуются для того, чтобы объект мог функционировать, и затем немедленно выполнять код в ответ на эти значения. Инициализаторы коллекций В главе 5 уже показывалось, как можно инициализировать массивы со значениями с помощью следующего синтаксиса: int[] mylntArray = new int[5] {5, 9, 10, 2, 99}; Он является быстрым и простым способом для объединения создания экземпляра и инициализации массива. Инициализаторы коллекций просто адаптируют этот синтаксис под коллекции: List<int> mylntCollection = new List<int> {5, 9, 10, 2, 99}; Комбинируя инициализаторы коллекций с инициализаторами объектов, коллекции можно конфигурировать с помощью простого и элегантного кода. То есть, например, вместо такого кода:
406 Часть I. Язык С# List<Curry> curries = new List<Curry> (); curries.Add(new Curry("Chicken", "Pathia", 6) ) ; curries.Add(new Curry("Vegetable", "Korma", 3) ) ; curries.Add(new Curry("Prawn", "Vindaloo", 9) ) ; можно использовать следующий код: List<Curry> moreCurries = new List<Curry> { new Curry {Mainlngredient = "Chicken", Style = "Pathia", Spiciness = 6}, new Curry {Mainlngredient = "Vegetable", Style = "Korma", Spiciness =3}, new Curry {Mainlngredient = "Prawn", Style = "Vindaloo", Spiciness = 9} }; Такой подход прекрасно подходит для тех типов, которые в основном используются для представления данных, и потому делает инициализаторы коллекций замечательным дополнением к технологии LINQ, которая будет описываться позже в книге. Практическое занятие Инициализаторы 1. Создайте новое консольное приложение по имени Chi4Ex01 и сохраните его в каталоге С: \BegVCSharp\Chapterl4. 2. Щелкните правой кнопкой мыши на имени этого проекта в окне Solution Explorer и выберите в контекстном меню, которое появится после этого, пункт Add^Add Existing Item (Добавить1^ Добавить существующий элемент). 3. Выберите файлы Animal.cs, Cow.cs, Chicken.cs, SuperCow.cs и Farm.cs из каталога С: \BegVCSharp\Chapterl2\Chl2Ex04\Chl2Ex04 и щелкните на кнопке Add (Добавить). 4. Измените объявление пространства имен в каждом из добавленных файлов следующим образом: namespace Chl4Ex01 5. Добавьте в классы Cow, Chicken и SuperCow конструктор по умолчанию. Например, в случае класса Cow добавьте следующий код: namespace Chl4Ex01 { public class Cow : Animal { public Cow() { } 6. Измените код в файле Program, cs, как показано ниже: static void Main(string[] args) { Farm<Animal> farm = new Farm<Animal> { new Cow { Name="Norris" }, new Chicken { Name="Rita" }, new Chicken(), new SuperCow { Name="Chesney" } }; farm.MakeNoises (); Console.ReadKey();
Глава 14. Расширения в языке С# 3.0 407 7. Выполните сборку приложения. На рис. 14.1 показаны ошибки, которые должны появиться в результате этого процесса сборки. \£\*ы™1\ **тт**Иишшт~\ Description \Q 1 Chi4€x01.Firm< Chl4ExOLAnlmal>' does not contain • definition for Add' 9 2 Chl4€xOLF«rm<Chl4Ex01>nlmal>- does not contain • definition for Add' Q Э Chl4Ex01.Farm< Chl4£xOlAnimil» does not contain • definition for Add' \Q 4 Сhl4£x01.F»rm< Сhl*ExOlAnimii>' does not contain a definition for Add -ЬError Ll5t Щ*штгщ[**№шш&\ File Program.cs Program.es Program.cs Program.cs lint 14 15 16 17 Column 17 17 17 17 Project ChWCxUl Chl4Ex01 Chl4Ex01 Chl4£x01 Рис. 14.1. Ошибки при попытке сборки приложения 8. Добавьте в файл Farm, cs следующий код: namespace Chl4Ex01 { public class Farm<T> : IEnumerable<T> where T : Animal { public void Add(T animal) { animals. Add (animal) ; > 9. Запустите приложение. На рис. 14.2 показан результат, который должен получиться. Рис. 14.2. Приложение Chi 4Ex01 в действии Описание полученных результатов Этот пример иллюстрирует применение инициализаторов объектов и коллекций для создания и заполнения коллекции объектов за один шаг. В роли коллекции выступает уже демонстрировавшаяся в предыдущих главах коллекция объектов, представляющих животных с фермы, для применения инициализаторов с которыми требуется внести два изменения. Во-первых, ко всем классам, унаследованным от базового класса Animal, добавляются конструкторы по умолчанию. Это необходимо потому, что, как показывалось ранее в этой главе, при использовании инициализаторов объектов вызываются конструкторы по умолчанию. В результате применения этих конструкторов по умолчанию свойство Name инициализируется в соответствии с конструктором по умолчанию базового класса, код которого выглядит так: public Animal () { name = "The animal with no name";
408 Часть I. Язык С# Однако при использовании инициализатора объекта с классом, унаследованным от Animal, важно помнить о том, что любые свойства, за установку которых отвечает инициализатор, устанавливаются после создания экземпляра объекта и, следовательно, после выполнения конструктора базового класса. Это означает, что в случае предоставления значения для свойства Name в виде части инициализатора объекта, оно будет перекрывать значение, устанавливаемое для него по умолчанию. В рассматриваемом примере значение для свойства Name устанавливается для всех, кроме одного из добавляемых в коллекцию элементов. Во-вторых, в класс Farm добавляется метод Add (). Делается это в ответ на ряд выданных компилятором ошибок следующего вида: 'ChHExOl.Farm <Chl4Ex01.Animal>' does not contain a definition for 'Add' fChi4Ex01.Farm <Chl4Ex01.Animal>' не содержит определения для 'Add' Это ошибка отражает часть базовой функциональности инициализаторов коллекций. "За кулисами" компилятор вызывает метод Add () коллекции для каждого элемента, который предоставляется в инициализаторе коллекции. Класс Farm предоставляет доступ к коллекции объектов через свойство по имени Animals. Компилятор не может догадаться, что это и есть то самое свойство, которое нужно заполнить (методом Animals .Add ()), и потому выдает сообщение об ошибке. Для устранения данной проблемы в класс Farm и добавляется метод Add(), который инициализируется с помощью инициализатора объектов. В качестве альтернативы можно было бы изменить код в примере и путем предоставления вложенного инициализатора для свойства Animals, как показано ниже: static void Main(string[] args) { Farm<Animal> farm = new Farm<Animal> { Animals = { new Cow { Name="Norris" }, new Chicken { Name="Rita" }, new Chicken (), new SuperCow { Name="Chesney" } } }; farm.MakeNoises(); Console.ReadKey(); } В случае использования такого кода предоставлять метод Add() для класса Farm уже бы не потребовалось. Этот альтернативный вариант больше подходит для таких ситуаций, когда имеется класс, содержащий несколько коллекций. В данном случае никакого очевидного кандидата на роль коллекции, подлежащей добавлению с помощью метода Add () содержащего класса, не наблюдается. Выведение типов Ранее в настоящей книге уже упоминалось о том, что С# является старого типизированным языком, т.е. что каждая переменная в нем имеет фиксированный тип и может использоваться только в том коде, в котором это учитывается. Во всех приводившихся до сих пор примерах переменные объявлялись с помощью либо такого синтаксиса: тип имя_переменной;
Глава 14. Расширения в языке С# 3.0 409 либо такого: тип имя_переменной = значение; В следующем коде сразу видно, к какому типу относится переменная mylnt: int mylnt = 5; Console.WriteLine(mylnt); Еще можно удостовериться, что IDE-среде тоже известно о типе переменной, просто наведя курсор мыши на идентификатор переменной (рис. 14.3). int mylnt =5; Console. WnteLine (my|nt); [(local variable) int myint| Puc. 14.3. Идентификация переменной my Int В С# 3.0 появилось новое ключевое слово var, которое можно использовать в качестве альтернативного варианта, т.е. вместо ключевого слова type в предыдущем коде: var varName = значение; Здесь переменная varName неявным образом приводится к типу значение. Обратите внимание, что никакого типа с именем var не существует. В строке кода var myVar = 5; под my Var подразумевается переменная типа int, а не переменная типа var. Опять- таки, как показано на рис. 14.4, IDE-среде известно и об этом. var myVar =5; Console.WriteLine(myvjr); [(local variable) int myVar| Puc. 14.4. Идентификация переменной my Var Этот момент является чрезвычайно важным. Использование var не означает объявления переменной без типа или даже переменной с типом, который может изменяться. Если бы это было так, язык С# не являлся бы более строго типизированным. А означает это лишь то, что ответственным за определение типа переменной назначается компилятор. При невозможности определить тип той переменной, которая была объявлена с использованием var, компилятор не будет компилировать код. Поэтому объявлять переменную с применением var и при этом не инициализировать ее нельзя, поскольку в таком случае у компилятора не будет значения, которое он смог бы использовать для определения типа этой переменной. То есть следующий код не скомпилируется: var myVar; Еще ключевое слово var может применяться для выведения (infer) типа массива посредством его инициализатора: var myArray = new[] {4, 5, 2); В этом коде массиву myArray неявным образом назначается тип int [ ].
410 Часть I. Язык С# При неявном задании типа массива подобным образом, элементы массива, используемые в инициализаторе, должны: □ либо все относится к одинаковому типу; □ либо все относится к одинаковому ссылочному типу или равняться null; □ либо все представлять собой элементы, которые могут быть неявно приведены к одному типу. В последнем из этих случаев, тип, к которому могут быть приведены элементы, называется наилучшим типом для элементов массива. При наличии любой неоднозначности в отношении того, какой тип является наилучшим, т.е. если есть два или более типов, к которым неявно могут быть приведены все элементы, код компилироваться не будет. Вместо этого будет появляться сообщение об ошибке, указывающее на недоступность наилучшего типа: var myArray = new[] {4, "not an int", 2}; Также обратите внимание на то, что числовые значения никогда не интерпретируются как нулевые типы, поэтому следующий код тоже не будет компилироваться: var myArray = new[] {4, null, 2}; Чтобы добиться такого поведения, однако, можно использовать стандартный инициализатор массива: var myArray = new int?[] {4, null, 2}; И, напоследок, обратите внимание на то, что идентификатор var вовсе не запрещено использовать в именах классов. Это значит, что, например, при наличии1 в области видимости (в том же пространстве имен или в том пространстве имен, на которое имеется ссылка) класса с именем var, применять метод неявного задания типа с использованием ключевого слова var нельзя. Сам по себе механизм выводимости типов (type inference) не является особо полезным, потому что, например, в коде, который приводился в данном разделе, он лишь все усложняет. Использование ключевого слова var затрудняет мгновенное распознавание типа, к которому относится та или иная переменная. Однако, как будет показано позже в этой главе, понятие выводимых типов является важным, поскольку лежит в основе других механизмов. Следующий рассматриваемый механизм, например — механизм анонимных типов — является одним из тех, в которых выводимые типы играют существенную роль. Анонимные типы При программировании, особенно приложений баз данных, часто оказывается, что много времени уходит на создание простых, скучных классов для представления данных. Семейства классов, не делающих больше ничего, кроме предоставления свойств, встречаются довольно часто. Приводившийся ранее в этой главе класс Curry является прекрасным тому примеру: public class Curry { public string Mainlngredient {get; set;} public string Style {get; set;} public int Spiciness {get; set;} } Этот класс ничего фактически не делает — он просто хранит структурированные данные. В базе данных или электронной таблице он мог бы просто представлять ка-
Глава 14. Расширения в языке С#3.0 411 кую-нибудь строку в таблице. Класс коллекции, способный хранить экземпляры этого класса, тогда мог бы служить представлением множества строк в этой таблице. Такое применение классов является вполне допустимым, но написание кода для этих классов может превращаться в рутинную работу, а внесение любых изменений в лежащую в основе схему данных требует добавлять, удалять или изменять определяющий их код. Анонимные типы предлагают способ для упрощения этой модели программирования. В частности, они позволяют не определять никаких простых типов для хранения данных, а использовать вместо этого компилятор С# для автоматического создания типов на базе данных, которые в них нужно хранить. Например, экземпляр показанного выше типа класса Curry можно создать следующим образом: Curry curry = new Curry { Mainlngredient = "Lamb", Style = "Dhansak", Spiciness = 5 }; В качестве альтернативного варианта можно использовать для этого и анонимный тип, как показано ниже: var curry = new { Mainlngredient = "Lamb", Style = "Dhansak", Spiciness = 5 }; Здесь видны два отличия. Во-первых, используется ключевое слово var. Объясняется это тем, что у анонимных типов нет идентификатора, который можно было бы использовать разработчику. На самом деле, на внутреннем уровне у них все-таки есть идентификатор, как очень скоро будет показано, но для использования разработчиком в своем коде он не доступен. Во-вторых, никакое имя для типа после ключевого слова new не указывается. Именно благодаря этому компилятор и узнает, что требуется использовать анонимный тип. IDE-среда распознает определение анонимного типа и соответствующим образом обновляет данные IntelliSense. В случае приведенного выше объявления анонимный тип будет отображаться так, как показано на рис. 14.5. var curry - new { Hainlngxedient - "Lwrir", Style ■ "I?h<ansnV, Spiciness - 5 }; curryl * I... -4 Convert I [Л| Convertero I к CrossAppDomamDelegate I Щ CtOf I ! -*| Cuny I di cw I ", DttaMUtllgnedExcepbon I *V DtteTime I :У DateTimeKind Puc. 14.5. Отображение в IntelliSense данных об анонимном типе Здесь, на внутреннем уровне, видно, что тип переменной curry выглядит как 'а. Очевидно, что использовать такое обозначение в своем коде нельзя, поскольку оно не является даже допустимым именем идентификатора. ' — это просто символ, которым в IntelliSense обозначаются анонимные типы. Еще IntelliSense позволяет инспектировать данные членов анонимного типа, как показано на рис. 14.6. var curry - new { Hainlngjredient - "Lamb", Style - "Dhaneak", Spiciness - 5 }; curry.I f I 1 % Equals 1 # GetHashCode 1 Ф GetType 1 uf Mainlngredient 1 pf Spldness 1 1 ♦ ToString *-,,.,._".. |L [string 'a Style ■Anonymous Types Л 'a is new{ string Mainlngredient, stnng Style, int Spiciness } Puc. 14.6. Отображение в IntelliSense данных о членах анонимного типа (local variable) a curry [Anonymous Types { 'a Is new{ stnng Mainlngredient, stnng Style, int Spiciness }j
412 Часть I. Язык С# Обратите внимание на то, что показанные здесь свойства являются доступными только для чтения. Это означает, что при желании иметь возможность изменять значения свойств в объектах хранения данных, применять анонимные типы нельзя. Как реализуются остальные члены анонимных типов можно увидеть в следующем практическом занятии. Практическое занятие Анонимные типы 1. Создайте новое консольное приложение по имени СЫ4Ех02 и сохраните его в каталоге С:\BegVCSharp\Chapterl4. 2. Измените код в его файле Program, cs следующим образом: static void Main(string [ ] args) { var curries = new[] { new { Mainlngredient = "Lamb", Style = "Dhansak", Spiciness = 5 }, new { Mainlngredient = "Lamb", Style = "Dhansak", Spiciness = 5 }, new { Mainlngredient = "Chicken", Style = "Dhansak", Spiciness = 5 } Console.WriteLine (curries [0] Console.WriteLine(curries [0] Console.WriteLine(curries [1] Console.WriteLine (curries [2] Console.WriteLine(curries[0] Console.WriteLine (curries [0] Console.WriteLine (curries[0] Console.WriteLine (curries [0] Console.ReadKey(); .ToStringO ) ; .GetHashCodeO ) ; .GetHashCodeO ) ; .GetHashCodeO ) ; .Eguals (curries [1])), .Eguals(curries [2])); == curries [1]); == curries [2]); 3. Запустите это приложение. На рис. 14.7 показан результат, который должен получиться. Рис. 14.7. ПриложениеСЫ4Ех02 в действии Описание полученных результатов В этом примере иллюстрируется создание массива объектов с анонимными типами и последующее его применение для тестирования поставляемых анонимными типами членов. Код для создания массива анонимно типизированных объектов выглядит следующим образом: var curries = new[] { new { Mainlngredient = "Lamb", Style = "Dhansak", Spiciness = 5 }, new { Mainlngredient = "Lamb", Style = "Dhansak", Spiciness = 5 }, new { Mainlngredient = "Chicken", Style = "Dhansak", Spiciness = 5 }
Глава 14. Расширения в языке С# 3.0 413 Здесь используется массив, который неявно приводится к анонимному типу за счет применения комбинированного синтаксиса, одна часть которого демонстрировалась в этом разделе, а другая — в разделе "Выведение типов" ранее в главе. В результате получается, что в переменной curries содержится три экземпляра анонимного типа. После создания такого массива в коде первым делом выводится результат вызова для анонимного типа метода ToString: Console.WriteLine(curries[0].ToString()); Это приводит к получению следующего вывода: { Mainlngredient = Lamb, Style = Dhansak, Spiciness = 5 } To есть реализация метода ToString () в анонимном типе подразумевает вывод значений каждого из определенных для этого типа свойств. Далее в коде для каждого из трех объектов массива вызывается метод GetHashCode (): Console.WriteLine(curries[0].GetHashCode()); Console.WriteLine(curries[1].GetHashCode()); Console.WriteLine(curries[2].GetHashCode() ) ; При реализации метод GetHashCode () должен возвращать для объекта уникальное целое число на базе его состояния. Первые два объекта в массиве имеют одинаковые значения свойств и, следовательно, одинаковое состояние. Поэтому в результате вызова этого метода для каждого из этих объектов возвращается одинаковое целое число и лишь для третьего объекта возвращается другое целое число. То есть, в общем, вывод выглядит так, как показано ниже: 294897435 294897435 621671265 Затем вызывается метод Equals () для сравнения первого объекта сначала со вторым, а потом — с третьим объектом: Console.WriteLine(curries[0].Equals(curries[1])); Console.WriteLine(curries[0].Equals(curries[2])); Результат получается следующий: True False Реализация метода Equals () в анонимных типах подразумевает сравнение состояния объектов. Значение true возвращается в тех случаях, когда каждое свойство одного объекта содержит такое же значение, как и сравниваемое с ним свойство в другом объекте. В случае использования операции ==, однако, такого не происходит. Операция ==, как показывалось в предыдущих главах, подразумевает сравнение ссылок на объекты. В последнем разделе кода выполняются те же сравнения, что и в предыдущем разделе кода, но только вместо метода Equals () используется операция ==: Console.WriteLine(curries [0] == curries[1]); Console.WriteLine(curries [0] == curries [2]); Каждый элемент в массиве curries ссылается на другой экземпляр анонимного типа, поэтому в результате в обоих случаях возвращается значение false. To есть вывод выглядит именно так, как и следовало ожидать: False False
414 Часть I. Язык С# Интересно, что при создании экземпляров анонимных типов компилятор заметил, что параметры выглядят одинаково, и создал три экземпляра одного и того же анонимного типа, а не трех отдельных анонимных типов. Это, однако, не означает, что при создании экземпляра объекта из анонимного типа, компилятор выполняет поиск соответствующего ему типа. Даже в случае определения где-то в коде класса с соответствующими свойствами, при использовании синтаксиса анонимных типов всегда будет создаваться (или, как было в этом примере, повторно использоваться) все-таки анонимный тип. Методы расширений Методы расширений (extension methods) позволяют расширять функциональные возможности типов без внесения изменения в сами типы. Их можно использовать для расширения даже таких типов, которые нельзя изменять — тех, что поставляются в .NET Framework включительно. Например, с помощью метода расширения, можно даже прибавлять функциональных возможностей и таким фундаментальным типам, как System. String. В таком контексте под расширением функциональных возможностей типа подразумевается предоставление метода, который можно было бы вызывать через экземпляр этого типа. Создаваемый для этого метод как раз и называется методом расширения. Он может принимать любое количество параметров и возвращать значение любого типа (включая void). Необходимые для создания и использования метода расширения шаги перечислены ниже. 1. Создать не обобщенный статический класс. 2. Добавить в созданный класс метод расширения в виде статического метода с использованием специального синтаксиса (который будет описываться чуть ниже). 3. Удостовериться в том, что в коде, где планируется использовать метод расширения, импортируется содержащее его класс пространство имен с помощью оператора using. 4. Вызвать метод расширения через экземпляр расширяемого типа тем же самым образом, каким бы вызывался и любой другой метод этого типа. Компилятор языка С# творит свои чудеса между третьим и четвертым шагом. IDE- среда мгновенно понимает, что был создан метод расширения, и даже отображает его в окне IntelliSense, как показано на рис. 14.8. На рис. 14.8 видно, что метод MyMarvelousExtensionMethod () доступен через объект string (под каковым здесь подразумевается всего лишь литеральная строка). Этот метод обозначен немного отличающимся значком с указывающей вниз стрелкой синего цвета, не принимает никаких дополнительных параметров и возвращает string. "My string object ".1^ 1 I ♦ IsNormallzed * 1 ♦ lartlndexOf 1 * lasHndexOfAny 1 Щ length J 1 V Normalize 1 ♦ Padleft 1 ♦ PidRlght 1 • Remove 1 1 -'♦ Replace • | 1 [(extension) string string MyMarvelousExtenslonMethodOl V Рис. 14.8. Отображение в IntelliSense данных о методе расширения
Глава 14. Расширения в языке С# 3.0 415 Метод расширения определяется тем же образом, что и любой другой метод, но должен обязательно отвечать требованиям предусмотренного для методов расширения синтаксиса. Эти требования выглядят следующим образом. □ Метод должен обязательно быть статическим. □ Метод должен обязательно включать параметр, представляющий экземпляр того типа, для которого будет вызываться этот метод расширения. (Здесь этот параметр будет называться параметром экземпляра.) □ Параметр экземпляра должен обязательно определяться для метода первым. □ Параметр экземпляра не должен сопровождаться никаким другим модификатором, кроме ключевого слова this. Синтаксис, который необходимо использовать для определения метода расширения, выглядит следующим образом: public static class класс_расширения { public static возвращаемый_тип имя_метода_расширения ( this расширяемый_тип instance) { } } Импортировав пространство имен, содержащее статический класс, в состав которого входит данный метод (т.е., как говорят, сделав метод расширения доступным), можно приступать к написанию непосредственно самого кода, как показано ниже: расширяемый_тип myVar; // myVar инициализируется в коде, который здесь не показан. myVar. имя_метода_расширения () ; При желании можно включать в метод расширения любые дополнительные параметры и пользоваться его возвращаемым типом. По сути, приведенный выше вызов идентичен тому, что показан ниже, но только имеет более простой синтаксис: расширяемый_тип myVar; // myVar инициализируется в коде, который здесь не показан. кла сс_ра сширения. имя_метода_ра сширения (myVaг) ; Другое преимущество состоит в том, что после импорта находить необходимые функциональные возможности становится гораздо легче путем просмотра данных о методах расширения в окне IntelliSense. Методы расширения могут быть разбросаны по многим классам расширения или даже библиотекам, но все они все равно будут появляться в списке членов расширенного типа. При определении метода расширения, предназначенного для использования с каким-то определенным типом, его также можно применять и с любым из типов, унаследованных от данного. В случае примера, приводившегося ранее в этой главе, определение метода расширения для класса Animal позволило бы вызывать его также, например, и для объекта Cow. Методы расширения представляют собой замечательный способ для предоставления библиотек вспомогательного кода, пригодного для многократного использования во множестве разных приложений. Еще они обширно применяются в технологии LINQ, о которой более подробно рассказывается позже в книге. Следующее практическое занятие позволит разобраться в них еще лучше.
416 Часть I. Язык С# ЩШЯШШШШШШШЯШШШШШШШШШвШЯЯШНШШШШШШвШНШЯЯШШЙЯШЯШШЯИШШШШШвШЯШвШШШНШШШШ Практическое занятие Методы расширения 1. Создайте новое консольное приложение с именем СЫ4ЕхОЗ и сохраните его в каталоге С:\BegVCSharp\Chapterl4. 2. Добавьте в решение новый проект типа библиотеки классов с именем ExtensioLib. 3. Удалите уже существующий файл класса Class 1. cs из ExtensionLib и добавьте новый класс с именем ExtensionLibExtensions. 4. Измените код в файле ExtensionLibExtensions. cs следующим образом public static class ExtensionLibExtensions { public static string ToTitleCase (this string inputString, bool forceLower) { mputString = inputString.Trim (); if (mputString == "") { return "" ; } if (forceLower) { inputString = inputString.ToLower() ; } string[] inputStringAsArray = inputString.Split(' '); StringBuilder sb = new StringBuilder(); for (int i = 0; i < inputStringAsArray.Length; i++) { if (inputStringAsArray[i].Length > 0) { sb.AppendFormatC {0}{1} ", inputStringAsArray[i].Substring @, 1).ToUpper (), inputStringAsArray[i].SubstringA)); } } return sb.ToString @, sb.Length - 1); public static string ReverseString (this string inputString) return new string(inputString.ToCharArray().Reverse().ToArray()); public static string ToStringReversed (this object inputObject) return inputObject.ToString () .ReverseString(); 5. Добавьте в проект СЫ4ЕхОЗ ссылку на проект ExtensionLib. 6. Измените код в файле Program, cs следующим образом: using System; using System.Collections.Generic; using System.Linq; using System.Text; using ExtensionLib;
Глава 14. Расширения в языке С#3.0 417 namespace Chl4Ex03 { class Program { static void Main(string [ ] args) { Console.WriteLine("Enter a string to convert:"); // Введите строку, подлежащую преобразованию string sourceString = Console.ReadLine(); Console.WriteLine ("String with title casing: {0}" , // Строка со словами, начинающимися с заглавной буквы sourceString.ToTitleCase(true)); Console. WriteLine ("String backwards: {0}" , sourceString. Reversestring ()) ; // Строка в обратном порядке Console.WriteLine ("String length backwards: {0}" , // Длина строки в обратном порядке sourceString.Length.ToStringReversed()) ; Console.ReadKey(); } } } 7. Запустите приложение. Когда появится приглашение, введите строку (состоящую хотя бы из 10 символов в длину и более чем одного слова для достижения наилучшего эффекта). На рис. 14.9 показан пример результата, который должен получиться. Рис. 14.9. Приложение СЫ4ЕхОЗ в действии Описание полученных результатов В этом примере мы сначала создали библиотеку классов, содержащую вспомогательные методы расширения, и затем использовали ее в простом клиентском приложении. Статический класс, содержащий методы расширения, называется в этой библиотеке ExtensionLibExtensions, поэтому мы импортировали содержащее его пространство имен ExtensionLib в клиентское приложение, чтобы сделать методы расширения доступными. Три созданных метода расширения показаны в табл. 14.1. Таблица 14.1. Методы расширения Метод Что делает ToTitleCase () Преобразует в заглавную первую букву каждого слова в строке (string) и возвращает строку. Имеет параметр bool, который в случае равенства true приводит вначале к преобразованию строки в нижний регистр ReverseStnng () Изменяет порядок букв в строке на обратный и возвращает ее ToStringReversed () Использует метод Reverse () для изменения на обратный порядка букв в строке, возвращаемой после вызова метода Tostring о для объекта object. Возвращает string
418 Часть I. Язык С# В клиентском коде мы использовали каждых из этих трех методов по очереди, применяя в качестве отправной точки вводимую пользователем строку. Первый из этих методов расширения, т.е. метод ToTitleCase (), содержит больше всего кода, но является самым простым для понимания. Определяется он с помощью того же синтаксиса методов расширения, который демонстрировался ранее. Сразу же видно, что он будет применяться к объектам string, поскольку параметр экземпляра в нем выглядит как string. Еще у него имеется второй определенный параметр — f orceLower. При вызове данного метода расширения, однако, использоваться будет только один параметр, поскольку параметр экземпляра будет браться из объекта, из которого это метод будет вызываться. Код для этого метода начинается с подготовки входной строки путем отсечения всех пробельных символов и (необязательно) преобразования ее в нижний регистр: public static string ToTitleCase(this string inputString, bool forceLower) { inputString = inputString.Trim(); if (inputString == "") { return ""; } if (forceLower) { inputString = inputString.ToLower(); } Далее строка разбивается на массив отдельных слов, после чего выполняется разбор этого массива в цикле for, где с помощью объекта StringBuilder производится сборка результирующей строки: string[] inputStringAsArray = inputString.Split(' '); StringBuilder sb = new StringBuilder(); for (int i = 0; i < inputStringAsArray.Length; i++) { if (inputStringAsArray[i].Length > 0) { sb.AppendFormat(M{0}{1} ", inputStringAsArray[i].Substring@, 1).ToUpper (), inputStringAsArray[i].SubstringA)); } } Напоследок результирующая строка возвращается. Последним символом в строке из- за обработки в цикле for является пробел, поэтому при ее возврате пробел отсекается: return sb.ToString @, sb.Length - 1) ; } Следующий метод расширения— ReverseString () - выглядит просто, но внешний вид может быть обманчив. В последовательности выполняемых им операций нет ничего сложного: сначала он сравнивает строку с массивом char, затем изменяет порядок элементов в этом массиве на обратный, потом преобразует измененный массив обратно в строку и возвращает ее. Однако метод, который он использует для изменения порядка элементов в массиве char на обратный, сам является методом расширения. Называется он Reverse () и определен в пространстве имен System.Linq следующим образом: public static IEnumerable<TSource> Reverse<TSource> ( this IEnumerable<TSource> source);
Глава 14. Расширения в языке С#3.0 419 Это определение выглядит сложнее, чем оно есть на самом деле, из-за того, что в нем используются обобщения. В методах расширения могут применяться параметры обобщенных типов во многом точно так же, как и в любых других методах. В методе Reverse () используется один единственный параметр обобщенного типа— TSource, а параметр экземпляра, применяемый в обобщенном методе, должен обязательно поддерживать интерфейс IEnumerable<TSource>. Внутри метода ReverseString () метод Reverse () вызывается для объекта типа char [ ]. Массив типа TSource поддерживает интерфейс IEnumerable<TSource> по определению, поэтому массив типа char поддерживает интерфейс IEnumerable<char>. А это значит, что вызываемая версия Reverse () на самом деле выглядит так: public static IEnumerable<char> Reverse<char> (this IEnumerable<char> source); В ReverseString () получающийся в результате интерфейс IEnumerable<char> преобразуется обратно в объект char [ ] за счет вызова его метода ТоАггау (), а сам этот объект char [ ] используется в конструкторе нового объекта string. Этот последний объект string, наконец, возвращается: public static string ReverseString(this string inputString) { return new string(inputString.ToCharArray().Reverse().ToArray()); } В пространстве имен System. Linq содержится много методов расширения, большинство из которых предназначены для использования с технологией LINQ, как будет показано позже в этой книге. Многие из них, однако, могут также быть полезными и в других ситуациях, одна из которых и была продемонстрирована здесь. Просмотреть эти методы расширения можно с помощью IntelliSense при условии, если было импортировано пространство имен System.Linq (которое импортируется по умолчанию). Однако на данном этапе лучше пока не делать этого, поскольку многие из таких методов основаны на использовании лямбда-выражений, о которых более подробно рассказывается в следующем разделе. Последний метод расширения в рассматриваемом примере, т.е. ToStringReversed (), является примером более общего метода расширения. Вместо параметра экземпляра типа string он принимает параметр экземпляра типа object. Это значит, что он может вызываться для любого объекта, и будет появляться в окне IntelliSense для каждого используемого объекта. В этом методе можно делать не так уж много из-за ограниченности в предположениях касательно того, какой объект может использоваться. В частности, в нем можно либо применять операцию is, либо пытаться выполнять преобразование для выяснения того, к какому типу относится параметр экземпляра и действовать соответствующим образом, или же, как было сделано в этом примере, использовать базовую функцию, которая поддерживается всем объектами, т.е. метод ToStringO: public static string ToStringReversed(this object inputObject) { return inputObject.ToStringO .ReverseString () ; } Здесь метод ToStringReversed () просто вызывает метод ToString () для своего параметра экземпляра и изменяет порядок элементов в нем на обратный с помощью описанного ранее метода ReverseString (). В примере клиентского приложения метод ToStringReversed () вызывается для переменной int, что приводит к получению строкового представления целого числа с идущими в нем в обратном порядке цифрами.
420 Часть I. Язык С# Методы расширения, пригодные для использования с множеством типов, могут быть очень полезными. Еще не стоит забывать и о возможности определения обобщенных методов расширения, в которых на допустимые для использования типы можно устанавливать ограничения, как показывалось в главе 12. Лямбда-выражения Лямбда-выражения (lambda expressions) являются новой конструкцией в С# 3.0 и могут упрощать некоторые аспекты программирования на языке С#, особенно в случае использования в сочетании с технологией LINQ. Поначалу их осваивание может вызывать сложности, в основном из-за того, что они являются очень гибкими в отношении способов их применения. Больше всего пользы они приносят тогда, когда используются в сочетании с другими механизмами С#, например, с анонимными методами. Если не брать технологию LINQ, которая будет рассматриваться позже в этой книге, анонимные методы являются наилучшей отправной точкой для изучения этой темы. Поэтому сначала вкратце вспомним, как работают анонимные методы. Краткое повторение анонимных методов В главе 13 уже рассказывалось об анонимных методах, т.е. методах, которые предоставляются внутристрочно в местах, где в противном случае должна была бы идти переменная типа делегата. При добавлении обработчика событий для какого-то события необходимо выполнить перечисленные ниже шаги. 1. Определить метод обработчика событий, причем так, чтобы его возвращаемый тип и параметры совпадали с возвращаемым типом и параметрами делегата, требуемого для подписываемого события. 2. Объявить переменную с типом делегата, используемого для события. 3. Инициализировать переменную делегата экземпляром типа делегата, который ссылается на метод обработчика событий. 4. И, наконец, добавить переменную делегата в список подписчиков события. На практике эти шаги выглядят немного проще, потому что обычно никто не хочет возиться с переменной для хранения делегата, и потому предпочтение отдается просто использованию при подписке на событие экземпляра делегата. Именно это и было сделано при применении следующего кода в главе 13: Timer myTimer = new TimerA00) ; myTimer.Elapsed += new ElapsedEventHandler(WriteChar) ; В этом коде осуществлятся подписка на событие Elapsed объекта Timer. Это событие подразумевает использование делегата типа ElapsedEventHandler, экземпляр которого и создается с помощью идентификатора метода WriteChar. В результате здесь получается, что при генерации объектом Timer события Elapsed будет вызываться метод WriteChar (). То, какие параметры будут передаваться методу WriteChar (), зависит от типа параметров, определенных в делегате ElapsedEventHandler, и значений, которые будут поступать из кода того объекта Timer, который сгенерировал событие. На самом деле, компилятор С# сможет достигать точно такого же результата и с помощью даже меньшего количества кода: myTimer.Elapsed += WriteChar;
Глава 14. Расширения в языке С# 3.0 421 Компилятору С# известен тип делегата, который требуется событию Elapsed, поэтому он сможет самостоятельно добавить все недостающие сведения. Однако в большинстве случаев поступать подобным образом все же не рекомендуется, поскольку такой подход затрудняет чтение кода и не позволяет видеть, что именно в нем происходит. При использовании анонимного метода приведенная выше последовательность шагов сокращается до одного единственного шага, который выглядит следующим образом. 1. Использовать внутристрочный анонимный метод с возвращаемым типом и параметрами, соответствующими возвращаемому типу и параметрам делегата, который требуется для подписываемого события. Определяется внутристрочный анонимный метод с использованием ключевого слова delegate: myTimer.Elapsed += delegate(object source, ElapsedEventArgs e) { Console.WriteLine("Event handler called after {0} milliseconds.", (source as Timer).Interval); }; Этот код будет работать так же хорошо, как и отдельный обработчик. Главное отличие состоит в том, что использованный здесь анонимный метод будет фактически скрыт от остальной части кода. То есть, например, применять этот обработчик повторно в другом месте приложения не получится. Кроме того, примененный здесь синтаксис является, мягко говоря, несколько громоздким. Ключевое слово delegate сразу же сбивает с толку, поскольку оно, в сущности, перегружается, так как используется и для определения анонимных методов, и для определения типов делегатов. Использование лямбда-выражений для анонимных методов Мы подошли, наконец, непосредственно к самим лямбда-выражениям. Лямбда-выражения являются способом упрощения синтаксиса анонимных методов. На самом деле они являются не только этим, но давайте пока не будем усложнять дело. С помощью лямбда-выражения код, который был приведен в конце предыдущего раздела, можно переписать следующим образом: myTimer.Elapsed += (source, e) => Console.WriteLine( "Event handler called after {0} milliseconds.", (source as Timer).Interval); На первый взгляд это выражение выглядит несколько устрашающе (если только вы не знакомы с так называемыми языками функционального программирования вроде Lisp или Haskell, например). Однако если получше к нему присмотреться, то можно увидеть или, по крайней мере, догадаться, каким образом оно работает и какое отношение имеет к тому анонимному методу, который собой заменяет. Состоит оно из трех частей: □ список (нетипизированных) параметров в круглых скобках; □ операция =>; □ оператор на языке С#. Типы параметров будут выводиться из контекста с применением той же самой логики, которая демонстрировалась в разделе "Анонимные типы" ранее в главе. Операция => просто отделяет список параметров от тела выражения, которое будет выполняться при вызове лямбда-выражения.
422 Часть I. Язык С# Компилятор будет брать это лямбда-выражение и создавать анонимный метод, функционирующий аналогично анонимному методу из предыдущего раздела. На самом деле он будет компилировать его в тот же или похожий MSIL-код. Следующее практическое занятие позволит лучше понять, что происходит в лямбда-выражениях. зан wme Простые лямбда-выражения 1. Создайте новое консольное приложение по имени Chi4Ex04 и сохраните его в каталоге С:\BegVCSharp\Chapterl4. 2. Измените код в его файле Program, cs следующим образом: namespace Chl4Ex04 { delegate int TwoIntegerOperationDelegate(int paramA, int paramB); class Program { static void PerformOperations(TwoIntegerOperationDelegate del) { for (int paramAVal = 1; paramAVal <= 5; paramAVal++) { for (int paramBVal = 1; paramBVal <= 5; paramBVal++) { int delegateCallResult = del(paramAVal, paramBVal); Console.Write("f({0}r{1})={2}", paramAVal, paramBVal, delegateCallResult); if (paramBVal != 5) { Console.WriteC, ") ; } } Console.WriteLine(); static void Main(string[] args) { Console.WriteLine("f(a, b) = a + b:"); PerformOperations((paramA, paramB) => paramA + paramB); Console.WriteLine() ; Console.WriteLine("f(a, b) = a * b:"); PerformOperations((paramA, paramB) => paramA * paramB); Console.WriteLine() ; Console.WriteLine("f (a, b) = (a - b) % b:"); PerformOperations((paramA, paramB) => (paramA - paramB) % paramB); Console.ReadKey(); 3. Запустите приложение. На рис. 14.10 показан результат, который должен получиться.
Глава 14. Расширения в языке С# 3.0 423 Рис. 14.10. Приложение СЫ4Ех04 в действии Описание полученных результатов В этом примере лямбда-выражения применяются для генерации функций, которые могли бы использоваться для возврата результатов выполнения специфической операции над двумя входными параметрами. Далее эти функции обрабатывают 25 пар значений и выводят результаты на консоль. Первым делом определяется тип делегата с именем TwoIntegerOperationDelegate для представления метода, принимающего два параметра int и возвращающего результат int: delegate int TwoIntegerOperationDelegate(int paramA, int paramB); Он применяется позже при определении лямбда-выражений. Эти лямбда-выражения компилируются в методы, возвращаемый тип и параметры которых соответствуют этому типу делегата, как будет показано ниже. Затем добавляется метод по имени PerformOperations (), который принимает единственный параметр типа TwoIntegerOperationDelegate: static void PerformOperations(TwoIntegerOperationDelegate del) { Его предназначение состоит в том, что ему можно передавать экземпляр делегата (а также анонимный метод или лямбда-выражение, поскольку эти конструкции тоже компилируются в экземпляр делегата), и он будет вызывать метод, который предоставляет этот экземпляр делегата, с рядом значений: for (int paramAVal = 1; paramAVal <= 5; paramAVal++) { for (int paramBVal = 1; paramBVal <= 5; paramBVal++) { int delegateCallResult = del(paramAVal, paramBVal); После этого осуществляется вывод параметров и результатов на консоль: Console.WnteC'f ({0}, {1})={2}", paramAVal, paramBVal, delegateCallResult); if (paramBVal != 5) { Console.Write (", "); } } Console.WriteLine (); } }
424 Часть I. Язык С# В методе Main () создаются три лямбда-выражения, которые затем по очереди используются для вызова метода Perf ormOperations (). Первый из этих вызовов выглядит следующим образом: Console.WriteLineC'f (a, b) = a + b:"); PerformOperations((paramA, paramB) => paramA + paramB); Лямбда-выражением здесь является: (paramA, paramB) => paramA + paramB Опять-таки, оно делится на три описанных ниже части. 1. Раздел определения параметров. Здесь определены два параметра— paramA и paramB. Они являются не типизированными, а это значит, что компилятор может выводить типы этих параметров из контекста. В данном случае он может определить, что вызов метода PerformOperations () требует использования делегата типа TwoIntegerOperationDelegate. У этого типа делегата имеется два параметра int, так что путем выведения получается, что типом и paramA, и paramB является int. 2. Операция =>. Она просто отделяет параметры от тела лямбда-выражения. 3. Тело выражения. Здесь задается простая операция, подразумевающая суммирование параметров paramA и paramB. Обратите внимание на то, что указывать, что это является возвращаемым значением, нет никакой необходимости. Компилятору известно о том, что для создания метода, пригодного для использования TwoIntegerOperationDelegate, необходимо, чтобы его возвращаемым типом был int. Поскольку заданная операция — paramA+paramB — вычисляется в int, и никакой дополнительной информации не предоставляется, компилятор понимает, что результатом данного выражения должен быть возвращаемый тип метода. В длинном варианте тогда код, в котором используется это лямбда-выражение, можно было бы расширить до следующего кода, в котором используется уже анонимный метод: Console.WriteLineC'f (a, b) = а + b:"); PerformOperations (delegate (int paramA, int paramB) { return paramA + paramB; }); В остальной части кода выполняются операции с использованием двух разных лямбда-выражений одинаковым образом: Console.WriteLine(); Console.WriteLineC'f (a, b) = a * b:"); PerformOperations((paramA, paramB) => paramA * paramB); Console.WriteLine(); Console.WriteLineC'f (a, b) = (a - b) % b:"); PerformOperations((paramA, paramB) => (paramA - paramB) % paramB); Console.ReadKey(); Последнее лямбда-выражение подразумевает больше вычислений, но сложнее других от этого не становится. Синтаксис лямбда-выражений позволяет выполнять и гораздо более сложные операции, как будет показано ниже.
Глава 14. Расширения в языке С# 3.0 425 Параметры лямбда-выражений В коде, который демонстрировался до сих пор, в лямбда-выражениях для определения типов передаваемых параметров применялся механизм выведения типов. На самом деле, обязательным это не является; при желании типы параметров можно и определять. Например, можно было бы использовать и такое лямбда-выражение: (int paramA, int paramB) => paramA + paramB При таком подходе код получается более удобочитаемым, но утрачивает краткость и гибкость. Лямбда-выражения с неявным определением типов из предыдущего практического занятия можно было бы использовать для типов делегатов с другими числовыми типами, например, long. Обратите внимание на то, что использовать и неявные и явные типы параметров в одном и том же лямбда-выражении нельзя. Следующее лямбда-выражение компилироваться не будет, потому что тип параметра paramA в нем определяется явно, а параметра paramB — неявно: (int paramA, paramB) => paramA + paramB Списки параметров в лямбда-выражениях всегда состоят из разделенного запятыми перечня либо всех неявно типизированных, либо всех явно типизированных параметров. Если параметр только один, тогда круглые скобки могут опускаться; в противном случае они являются обязательной частью списка параметров, как в показанном выше примере. Например, лямбда-выражение с единственным неявно типизированным параметром может выглядеть так: paraml => paraml * paraml Еще можно определять лямбда-выражения, не имеющие параметров. Отсутствие параметров обозначается пустыми круглыми скобками, как показано ниже: () => Math.PI Такое выражение могло бы пригодиться при необходимости в использовании делегата, не требующего никаких параметров, но возвращающего значение double. Тело операторов лямбда-выражений Во всем приводившемся пока что коде в теле оператора лямбда-выражений использовалось единственное выражение. Это выражение воспринималось как возвращаемое значение лямбда-выражения, что и показывает то, как, к примеру, выражение paramA + paramB можно применять в качестве тела оператора для лямбда-выражения, предназначенного для делегата с возвращаемым типом int (при условии приведения обоих параметров paramA и paramB явным или неявным образом к int, как это и было сделано в примере кода). Еще в одном из прежних примеров было показано, как делегат с возвращаемым типом void позволяет использовать менее громоздкий код в теле оператора: myTimer.Elapsed += (source, e) => Console.WriteLine ( "Event handler called after {0} milliseconds.", (source as Timer).Interval); Здесь оператор ни во что не вычисляется, и потому будет выполняться без использования возвращаемого значения. С учетом того, что лямбда-выражения могут визуализироваться в виде расширения синтаксиса анонимных методов, вряд ли покажется удивительным то, что в виде части тела операторов лямбда-выражений можно также использовать и множество операторов. Осуществляется это путем предоставления заключенного в фигурные скобки
426 Часть I. Язык С# кода, во многом подобно тому, как это делается в любых других ситуациях в С#, когда требуется предоставлять несколько строк кода: (paraml, param2) => { // Здесь идет несколько операторов. } В случае использования лямбда-выражения в сочетании с делегатом, возвращаемым типом которого является не void, нужно обязательно выполнять возврат значения с помощью ключевого слова return, точно так же как и в любом другом методе: (paraml, param2) => { // Здесь идет несколько операторов! return возвращаемов_значение ; } Например, ранее уже показывалось, что следующий код из практического занятия: PerformOperations((paramA, paramB) => paramA + paramB); можно было бы переписать так: PerformOperations(delegate(int paramA, int paramB) { return paramA + paramB; }); В качестве альтернативного варианта, этот код можно было бы переписать и следующим образом: PerformOperations((paramA, paramB) => { return paramA + paramB; }); В этом случае достигается большая преемственность с исходным кодом, поскольку сохраняется неявная типизация параметров paramA и paramB. По большей части лямбда-выражения наиболее полезными и, конечно же, наиболее элегантными, являются тогда, когда используются с одним выражением. Честно говоря, при наличии необходимости в добавлении нескольких операторов код может читаться гораздо лучше, если вместо лямбда-выражения применить отдельный не анонимный метод; это еще также сделает его и более пригодным для многократного использования. Лямбда-выражения как делегаты и как деревья выражений Некоторые отличия между лямбда-выражениями и анонимными методами, вроде того, что лямбда-методы обладают большей гибкостью (например, могут иметь неявно типизированные параметры), уже были показаны. Теперь пришла пора рассказать о еще одном важном отличии, последствия которого, однако, прояснятся лишь позже в книге, при изучении технологии LINQ. Лямбда-выражения можно интерпретировать двумя способами. Первый способ, который демонстрировался повсюду в этой главе, позволяет интерпретировать их в качестве делегата. То есть он позволяет присваивать лямбда-выражение переменной типа делегата, как это и делалось в предыдущем практическом занятии.
Глава 14. Расширения в языке С# 3.0 427 В общих чертах это означает, что лямбда-выражение с параметрами в количестве вплоть до четырех можно представлять в виде одного из следующих обобщенных типов, все их которых определены в пространстве имен System. □ Actiono для лямбда-выражений без параметров и с возвращаемым типом void. □ Actiono для лямбда выражений с параметрами в количестве вплоть до четырех и возвращаемым типом void. □ Funco для лямбда-выражений с параметрами в количестве вплоть до четырех и возвращаемым типом, который не является типом void. В Actiono предусмотрено вплоть до четырех параметров обобщенного типа, по одному для каждого параметра в лямбда-выражении, а в Funco — вплоть до пяти параметров обобщенного типа, т.е. четыре для возможных четырех параметров в лямбда- выражении и один для возвращаемого типа в нем. В Funco возвращаемый тип всегда идет последним в списке. Например, следующее лямбда-выражение (которое уже приводилось ранее): (int paramA, int paramB) => paramA + paramB может быть представлено в виде делегата типа Func<int, int, int>, поскольку у него есть всегда два параметра и их типом вместе с возвращаемым типом является int. Второй способ позволяет интерпретировать лямбда-выражение как дерево выражения (expression tree). Дерево выражения является абстрактным представлением лямбда-выражения и как таковое не может выполняться напрямую. Вместо этого его можно использовать для программного анализа лямбда-выражения и выполнения в ответ на него различных действий. Очевидно, что данная тема является сложной. Но деревья выражений играют критически важную роль в функциональных возможностях LINQ. Чтобы привести более конкретный пример, скажем, что в состав каркаса LINQ входит обобщенный класс по имени Expressiono, который можно использовать для инкапсуляции лямбда-выражения. Один из способов применения этого класса выглядит так: взять лямбда-выражение, которое было написано разработчиком на С#, и преобразовать его в эквивалентный сценарий на языке SQL, пригодный для выполнения прямо в базе данных. На данном этапе больше ничего знать не требуется. Теперь, когда эта функциональная возможность будет встречаться далее в книге, вы сможете понять, что происходит, поскольку в этой главе были предоставлены сведения с* ключевых концепциях, предлагаемых С# 3.0. Лямбда-выражения и коллекции Сейчас, когда уже было рассказано об обобщенном делегате Funco, можно переходить к рассмотрению других методов расширения, которые предлагаются в пространстве имен System. Linq для типов-массивов. Например, там есть метод расширения Aggregate (), определение которого включает три перегрузки: public static TSource Aggregate<TSource>( this IEnumerable<TSource> source, Func<TSource, TSource, TSource> func); public static TAccumulate Aggregate<TSource, TAccumulate> ( this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func);
428 Часть I. Язык С# public static TResult Aggregate<TSource, TAccumulate, TResult> ( this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector); Как и код показанного ранее метода расширения, код этого метода на первый взгляд кажется малопонятным, но если разбить его на части, то все станет довольно просто. В окне IntelliSense описание предназначения этого метода будет выглядеть так: Applies an accumulator function over a sequence. Применяет аккумулирующую функцию к последовательности. Это означает, что аккумулирующая функция (которая может предоставляться в виде лямбда-выражения) будет применяться для каждой пары элементов в коллекции от начала до конца с превращением выходных данных каждой операции вычисления во входные данные следующей операции вычисления. В самой простой из трех перегрузок присутствует спецификация только одного обобщенного типа, который может выводиться из типа параметра экземпляра. Например, в следующем коде этим обобщенным типом будет int (аккумулирующая функция пока оставляется пустой): int[] mylntArray = {2, 6, 3}; int result = mylntArray.Aggregate(...); Этот код эквивалентен следующему: int[] mylntArray = {2, 6, 3}; int result = mylntArray.Aggregate < int > (...); Требуемое лямбда-выражение здесь может быть выведено из спецификации метода расширения. Поскольку в этом коде типом TSource является int, лямбда-выражение должно иметь вид делегата Func<int, int, int>. Например, это может быть и лямбда-выражение, которое уже демонстрировалось ранее: int[] mylntArray = [2, 6, 3}; int result = myIntArray.Aggregate( (paramA, paraxnB) => paramA + paramB) ; Такой вызов приведет к тому, что лямбда-выражение будет вызвано дважды: первый раз с параметром paramA равным 2 и параметром paramB равным 6, а второй — с параметром paramA равным 8 (в результате первого вычисления) и параметром paramB равным 3. В конечном результате переменной result будет присвоено int- значение 11, являющееся суммой значений всех элементов в массиве. Две других перегрузки метода расширения Aggregate () выглядят похоже, но позволяют выполнять немного более сложные операции по обработке и детально иллюстрируются в следующем практическом занятии. Практическое занятие Лямбда-ВЫраЖвНИЯ И КОЛЛвКЦИИ 1. Создайте новое консольное приложение по имени Chl4Ex05 и сохраните его в каталоге C:\BegVCSharp\Chapterl4. 2. Измените код в его файле Program.cs следующим образом: static void Main(string [ ] args) { string [] curries = { "pathia", "jalfreze", "korma" }; Console.WriteLine (curries .Aggregate ( (a, b) => a + " " + b) );
Глава 14. Расширения в языке С# 3.0 429 Console.WriteLine(curries.Aggregate < string, int > ( 0, (a, b) => a + b.Length)); Console.WriteLine(curries.Aggregate < string, string, string > ( "Some curries:", (a, b) => a + " " + b, a => a)) ; Console.WriteLine(curries.Aggregate < string, string, int > ( "Some curries:", (a, b) => a + " " + b, a => a.Length)); Console.ReadKey(); } 3. Запустите приложение. На рис. 14.11 показан результат, который должен получиться. I We:///C:/BegVCSharp/Ch«pter14/Ch14Ex05/. '■-M^ttffid Рис. 14.11. Приложение Chi 4Ex05 в действии Описание полученных результатов Этот пример демонстрирует применение каждой из трех перегрузок такого метода расширения, как Aggregate (), с использованием в качестве исходных данных строкового массива с тремя элементами. Сначала выполняется простая конкатенация: Console.WriteLine(curries.Aggregate ( (a, b) => a + " " + b) ) ; Первая пара элементов путем конкатенации превращается в одну строку с помощью простого синтаксиса. Такой синтаксис далеко не является лучшим для конкатенации строк — в идеале вместо него для достижения оптимальной производительности следовало бы использовать либо метод string. Concat (), либо метод string. Format () — но здесь он позволяет очень легко увидеть, что происходит. После этой первой операции конкатенации результат передается обратно лямбда-выражению вместе с третьим элементом массива во многом подобно тому, как это происходило при суммировании значений int, которое показывалось ранее. Это в конечном итоге приводит к конкатенации всего массива с разделением его элементов пробелами. Далее применяется вторая перегрузка функции Aggregate (), в которой предусмотрены два параметра обобщенного типа— TSource и TAccumulate. В таком случае лямбда-выражение должно иметь вид Func<TAccumulate, TSource, TAccumulateX Вдобавок, еще должно быть указано и начальное значение типа TAccumulate. Это начальное значение используется в первом вызове лямбда-выражения вместе с первым элементом массива. В последующих вызовах берется аккумулированный результат предыдущих вызовов этого выражения. Код, который использовался в данном примере, выглядит следующим образом: Console.WriteLine(curries.Aggregate<string, int>( 0, (a, b) => a + b.Length) ) ;
430 Часть I. Язык С# Здесь типом аккумулятора (и, косвенно, возвращаемого значения) является int. Значение аккумулятора первоначально устанавливается в исходное значение 0 и с каждым вызовом лямбда-выражения суммируется с длиной соответствующего элемента массива. В конечном счете получается сумма значений длины всех элементов в массиве. Далее идет последняя перегрузка метода Aggregate (). Она принимает три параметра обобщенного типа и отличается от предыдущей версии только тем, что позволяет, чтобы тип возвращаемого значения не совпадал ни с типом элементов в массиве, ни с типом значения аккумулятора. Первым делом такая перегрузка используется для конкатенации строковых элементов с начальной строкой: Console.WriteLine(curries.Aggregate<string, string, string>( "Some curries:", (a, b) => a + " " + b, a => a)); Последний параметр этого метода, а именно — параметр resultSelector, должен задаваться даже в том случае, если (как и в рассматриваемом примере) значение аккумулятора просто копируется в результат. Этот параметр представляет собой лямбда- выражение типа Func<TAccumulate, TResult>. В последнем разделе кода та же самая версия Aggregate () используется снова, но на этот раз с возвращаемым значением int. Здесь параметр resultSelector предоставляется с лямбда-выражением, которое возвращает длину строки аккумулятора: Console.WriteLine(curries.Aggregate<stnng, string, int> ( "Some curries:", (a, b) => a + " " + b, a => a.Length)); Ничего зрелищного в этом примере нет, но зато он демонстрирует то, как можно использовать более сложные методы расширения, обладающие параметрами обобщенного типа, коллекциями и, очевидно, более сложным синтаксисом. Такие методы будут еще не раз встречаться далее в этой книге. Резюме В настоящей главе были рассмотрены новые средства, которые появились в версии С# 3.0, предлагаемой для использования в средах Visual Studio 2008 и Visual C# Express Edition 2008. Здесь было рассказано о том, как эти средства могут упрощать написание части кода, требуемого для достижения определенных наиболее часто используемых и/или развитых функциональных возможностей. Далее перечислены ключевые моменты, с которыми вы ознакомились в этой главе. □ Как использовать инициализаторы объектов и коллекции для создания экземпляров и инициализации объектов и коллекций за один шаг. □ Как IDE-среда и компилятор С# умеют выводить типы из контекста и как использовать ключевое слово var для вынуждения их применять механизм выведения типов с переменными любого типа. □ Как создавать и использовать анонимные типы, которые подразумевают применение и уже упомянутых инициализаторов, и механизма выведения типов. □ Как создавать методы расширения, которые можно вызывать для экземпляров других типов без добавления какого-либо кода в определение этих типов, и как этой методикой пользоваться для предоставления библиотек вспомогательных методов.
Глава 14. Расширения в языке С# 3.0 431 □ Как использовать лямбда-выражения для предоставления анонимных методов экземплярам делегатов и как расширенный синтаксис лямбда-методов позволяет получить дополнительную функциональность. Большинство из тех средств С#, о которых говорилось в этой главе, было добавлено специально с расчетом на применение вместе с новой технологией LINQ, которая появилась в .NET Framework. Степень элегантности и многие тонкие детали показанного в этой главе кода станут очевидными лишь позже. Несмотря на это, некоторые рассмотренные здесь приемы являются настолько мощными, что их можно незамедлительно начинать применять на практике для совершенствования своих навыков программирования на С#. Вот и подошло к концу изучение языка С# в том виде, в котором он предлагается в версии 3.0. Для программирования с использованием .NET Framework, однако, одних только знаний С# не достаточно. Язык С# обеспечивает лишь всеми средствами, которые необходимы для написания .NET-приложений, а классы, доступные в .NET Framework, предоставляют исходный материал, с которого необходимо начинать разработку. Поэтому, начиная с этого момента, в настоящей книге будет много внимания уделяться именно этим классам и демонстрироваться, как с их помощью можно решать массу различных задач. Со следующей главы мы прекратим использовать консольные приложения, с которыми постоянно приходилось иметь дело ранее, и начнем применять богатые функциональные возможности, предлагаемые приложениями Windows Forms для создания графических пользовательских интерфейсов. При этом нужно будет помнить, что базовые принципы выглядят для всех типов приложений одинаково. Навыки, приобретенные в первой части книги, очень хорошо помогут при изучении материала последующих глав. Упражнения 1. Почему нельзя использовать инициализатор объектов с показанным ниже классом? После изменения данного класса так, чтобы он допускал использование инициализатора объектов, приведите пример кода, который вы бы применили для создания экземпляра и инициализации этого класса за один шаг. public class Giraffe { public Giraffe (double neckLength, string name) { NeckLength = neckLength; Name = name; } public double NeckLength {get; set;} public string Name {get; set;} } 2. Верно ли то, что в случае объявления переменной типа var ее можно будет использовать для хранения объектов любого типа? 3. В случае использования анонимных типов, как можно выполнить сравнение двух экземпляров для выяснения того, содержатся ли в них одинаковые данные? 4. Попробуйте исправить код следующего метода расширения, в котором присутствует ошибка:
432 Часть I. Язык С# public string ToAcronym(this string inputString) { inputString = inputString.Trim(); if (inputString == "") { return ""; } string[] inputStringAsArray = inputString.Split (' '); StringBuilder sb = new StringBuilder (); for (int i = 0; i < inputStringAsArray.Length; i++) { if (inputStringAsArray[i].Length > 0) { sb.AppendFormat("{0}", inputStringAsArray[i].Substring@, 1).ToUpper()); } } return sb.ToString () ; } 5. Как сделать так, чтобы метод расширения в четвертом упражнении был точно доступен клиентскому коду? 6. Перепишите показанный здесь метод ToAcronym так, чтобы его код занимал одну строку. В этом коде должна обеспечиваться гарантия того, что строки с множеством пробелов между словами не будут приводить к ошибкам. Подсказка: для этого потребуется тернарная операция ? :, функция расширения string.Aggregate<string, string>() и лямбда-выражение.
ЧАСТЬ Программирование для Windows В ЭТОЙ ЧАСТИ... Глава 15. Основы программирования для Windows Глава 16. Расширенные функциональ-ные возможности Windows Forms Глава 17. Использование обычных диалоговых окон Глава 18. Развертывание Windows-приложений
Основы программирования для Windows Около 10 лет назад Visual Basic был встречен с огромным энтузиазмом, поскольку он предложил программистам инструментальные средства создания чрезвычайно детализированных интерфейсов пользователя посредством интуитивного конструктора форм и простой для изучения язык программирования, которые в сочетании друг с другом образуют, вероятно, наилучшую на сегодняшний день среду для быстрой разработки приложений (Rapid Application Development — RAD). Одно из преимуществ, предоставляемых средствами RAD, такими как Visual Basic, состоит в том, что они обеспечивают доступ к ряду заранее созданных элементов управления, которые можно использовать для быстрого построения интерфейса пользователя приложения. В основе разработки большинства приложений Visual Basic для Windows лежит применение средств Forms Designer (Конструктор форм). Создание интерфейса пользователя осуществляется перетаскиванием элементов управления из панели инструментов на форму и их размещением там, где они должны отображаться во время выполнения программы. При этом двойной щелчок на элементе управления добавляет дескриптор данного элемента управления. Элементы управления, предоставленные Microsoft, а также дополнительные нестандартные элементы управления, которые можно приобрести по умеренным ценам, снабдили программистов невероятно обширным арсеналом многократно используемого, тщательно протестированного кода, доступного единственным щелчком кнопкой мыши. Теперь такой подход к построению приложений доступен и разработчикам на С# через Visual Studio. В этой главе мы поработаем с Windows Forms и воспользуемся некоторыми из множества элементов управления, поставляемых с Visual Studio. Эти элементы управления охватывают широкий диапазон функциональных возможностей, и благодаря средствам конструирования Visual Studio, разработка интерфейсов пользователей и осуществление взаимодействия с пользователем становится очень простой — к тому же приятной — задачей. Рассмотрение всех элементов управления Visual Studio в рамках этой книги невозможно, потому в настоящей главе рассмотрены некоторые из
Глава 15. Основы программирования для Windows 435 наиболее часто применяемых компонентов, начиная с надписей и текстовых полей и заканчивая представлениями и элементами управления с вкладками. В этой главе рассматриваются следующие вопросы. □ Конструктор Windows Forms. □ Элементы управления, предназначенные для отображения информации пользователю, такие как Label и LinkLabel. □ Элементы управления, такие как Button, предназначенные для запуска событий. □ Элементы управления, такие как Text Box, которые позволяют пользователю приложения вводить текст. □ Элементы управления, такие как RadioButton и CheckButton, которые позволяют информировать пользователя о текущем состоянии приложения и позволяют пользователю изменять это состояние. □ Элементы управления, позволяющие отображать информацию в виде списков, такие как ListBox и ListView. □ Элементы управления, такие как TabControl и Groupbox, позволяющие группировать другие элементы управления. Элементы управления Возможно, вы и не замечаете это, но, работая с Windows Forms, вы работаете с пространством имен System. Windows . Forms. Это пространство имен определено в директивах using в одном из файлов, содержащих класс Form. Большинство элементов управления в каркасе .NET являются производными от класса System. Windows . Forms.Control. Этот класс определяет основные функциональные возможности элементов управления, вследствие чего многие свойства и события элементов управления, с которыми вам придется встретиться, идентичны. Многие из этих классов сами являются базовыми для других элементов управления, как это имеет место в случае с классами Label и TextBoxBase (рис. 15.1). Object —?— Marshal BvRef Object Component t Label —*— ListView TextBoxBase LinkLabel TextBox RichTextBox Рис. 15.1. Иерархия наследования некоторых элементов управления Свойства Все элементы управления обладают рядом свойств, которые служат для манипулирования поведением элемента управления. Базовый класс большинства элементов управления, System.Windows .Forms .Control, обладает рядом свойств, которые дру-
436 Часть II. Программирование для Windows гие элементы управления наследуют непосредственно или замещают для обеспечения того или иного нестандартного поведения. Некоторые из наиболее часто используемых свойств класса Control описаны в табл. 15.1. Эти свойства представлены в большинстве элементов управления, рассмотренных в данной главе, потому мы не будем повторно подробно останавливаться на них, если только поведение рассматриваемого элемента управления не будет требовать изменений. Приведенная таблица не является исчерпывающей. Если хотите ознакомиться со всеми свойствами этого класса, обратитесь к документации по .NET Framework SDK. Таблица 15.1. Часто используемые свойств класса Control Свойство Описание Anchor Указывает поведение элемента управления при изменении размеров его контейнера. Это свойство подробно описано в следующем разделе BackColor Цвет фона элемента управления Bottom Указывает расстояние от верхнего края окна до нижнего края элемента управления. Это свойство не совпадает с указанием высоты элемента управления Dock Пристыковывает элемент управления к краям его контейнера. Это свойство подробнее описано в следующем разделе Enabled Установка значения свойства Enabled равным true означает, что элемент управления может принимать данные, вводимые пользователем. Установка этого значения равным false означает невозможность приема этих данных ForeColor Цвет изображения элемента управления Height Расстояние между верхним и нижним краями элемента управления Left Положение левого края элемента управления относительно левого края его контейнера { Name Имя элемента управления. Это имя может использоваться для ссылки на элемент управления в коде Parent Родительский объект элемента управления Right Положение правого края элемента управления относительно левого края его контейнера Tab index Порядковый номер элемента управления в последовательности перехода между элементами внутри его контейнера по клавише табуляции Tabstop Указывает доступность элемента управления по клавише табуляции Tag Обычно это значение не используется самим элементом управления. Оно позволяет хранить информацию об элементе управления в самом элементе управления. При присваивании этому свойству значения с помощью конструктора Windows Forms Designer в качестве значения можно использовать только строку Text Содержит текст, связанный с данным элементом управления тор Положение верхнего края элемента управления относительно верхнего края его контейнера Visible Указывает видимость элемента управления во время выполнения Width Ширина элемента управления
Глава 15. Основы программирования для Windows 437 Привязка, стыковка и определение формы элементов управления С появлением Visual Studio 2005 используемая по умолчанию рабочая область конструктора форм Forms Designer изменилась с сеточной поверхности, на которой можно было размещать элементы управления, на гладкую поверхность, использующую линии привязки для позиционирования элементов управления. Переключение между этими двумя стилями конструирования можно осуществлять, выбирая пункт Options (Параметры) в меню Tools (Сервис). Выберите узел Windows Forms Designer в дереве слева и установите Layout Mode (Режим макета). Выбор наиболее подходящего инструментального средства в значительной мере зависит от личных предпочтений. В следующем практическом занятии применен режим, используемый по умолчанию. Использование линий привязки Чтобы попрактиковаться в применении линий привязки в Windows Forms Designer, выполните следующие действия: 1. Создайте новое приложение Windows Forms и назовите его SnapLines. 2. Перетащите элемент одиночной кнопки из панели управления Toolbox в середину формы. 3. Перетащите кнопку к верхнему левому углу формы. Обратите внимание, что при приближении элемента управления к краю формы появляются линии, исходящие от ее левого и верхнего краев, а элемент управления помещается в фиксированную позицию. Элемент управления можно переместить за пределы линий привязки или оставить в данной позиции. Линии привязки, отображаемые Visual Studio при перемещении кнопки в верхний левый угол формы, показаны на рис. 15.2. 4. Снова переместите кнопку в центр формы и перетащите на форму еще одну кнопку из Toolbox. Переместите ее под существующую кнопку. При перемещении кнопки под существующую появятся линии привязки. Эти линии привязки позволяют выравнивать элементы управления так, чтобы они размещались непосредственно над или на одной высоте с другим элементом управления. При перемещении новой кнопки по направлению к существующей другие линии привязки позволяют размещать кнопки на заблаговременно определенном расстоянии между ними. Вид формы при размещении кнопок в непосредственной близости друг от друга показан на рис. 15.3. •^ Forml аав Ьи«оп1 ■ . Foiml button2 [ btftonl Рис. 15.2. Линии привязки при перемещении кнопки в верхний левый угол формы Рис. 15.3. Линии привяжи при размещении кнопок в непосредственной близости друг от друга
438 Часть II. Программирование для Windows •^ Forml | button2 | buttonl j БЙШ Hello Work "ШВ| 5. Измените размер кнопки buttonl, чтобы она была шире, чем кнопка button2. Затем измените также размеры кнопки button2 и обратите внимание, что когда ее ширина становится равной ширине кнопки button2, появляются линии привязки, которые позволяют установить ширину элементов управления одинаковой. 6. Добавьте в форму элемент управления TextBox, поместив его под кнопками, и измените его свойство Text на Hello World!. Рис. 15.4. Линия привязки, по- 7. Добавьте в форму элемент управления Label звояяющаяразместитьэлемен- и поместите его слева от элемента TextBox. ты на одной высоте Обратите внимание, что при перемещении элемента управления появляются линии привязки, которые позволяют привязать его к верхнему и нижнему краю элемента управления TextBox, но между ними появляется также третья линия привязки. Эта линия привязки, показанная на рис. 15.4, позволяет разместить элемент Label так, чтобы текст в элементах управления TextBox и Label отображался на одной высоте. Свойства Anchor и Dock Эти два свойства особенно полезны при конструировании формы. Задача предотвращения искажения формы при изменении размеров окна пользователем далеко не тривиальна, и раньше для этого требовалось написание множества строк кода. Многие программы решают эту проблему путем простого запрещения изменений размеров окна. Этот способ решения проблемы является простейшим, но не всегда наилучшим. Свойства Anchor и Dock, появившиеся в каркасе .NET, позволяют решать эту проблему без написания даже единой строки кода. Свойство Anchor указывает поведение элемента управления при изменении размеров окна пользователем. Можно указать, чтобы элемент управления изменял свои размеры, сберегая пропорции своих краев, либо сохранял свои размеры, сберегая свою позицию относительно краев окна. Свойство Dock указывает, что элемент управления должен пристыковываться к краю своего контейнера. Если пользователь изменяет размеры окна, элемент управления остается пристыкованным к краю окна. Если, например, указать, что элемент управления должен быть пристыкован к нижнему краю своего контейнера, то элемент управления изменяет свои размеры и/или перемещается, чтобы всегда занимать нижнюю часть окна независимо от изменения его размеров. Более подробно свойство Anchor описано далее в этой главе. События В главе 13 вы узнали, что собой представляют события, и как они используются. В этом разделе освещены конкретные виды событий — а именно, те, которые генерируются элементами управления Windows Forms. Обычно эти события связаны с действиями, выполняемыми пользователем. Например, когда пользователь щелкает на кнопке, кнопка генерирует событие, указывающее, что только что с ней произошло. Обработка события — это средство, посредством которого программист может снабдить данную кнопку теми или иными функциональными возможностями. Класс Control определяет ряд событий, которые присущи всем элементам управления, используемым в этой главе. Некоторые из них описаны в табл. 15.2. Как и в предыдущей таблице, здесь приведена всего лишь подборка наиболее часто исполь-
Глава 15. Основы программирования для Windows 439 зуемых событий. Если хотите ознакомиться с полным перечнем событий, обратитесь к документации по .NET Framework SDK. Таблица 15.2. Часто используемые события класса Control Событие Описание Click Происходит при щелчке на элементе управления. В некоторых случаях это событие происходит также при нажатии пользователем клавиши <Enter> Doubleclick Происходит при двойном щелчке на элементе управления. Обработка события Click для некоторых элементов управления, таких как Button, полностью исключает возможность вызова события Doubleclick DragDrop Происходит по завершении операции перетаскивания и оставления — иначе говоря, при перетаскивании объекта поверх элемента управления и освобождении кнопки мыши пользователем DragEnter Происходит, когда перетаскиваемый объект перемещается внутрь границ элемента управления DragLeave Происходит, когда перетаскиваемый объект покидает границы элемента управления DragOver Происходит, когда объект перетаскивается поверх элемента управления KeyDown Происходит при нажатии клавиши во время нахождения элемента управления в фокусе. Это событие всегда происходит прежде событий KeyPress и KeyUp Keypress Происходит при нажатии клавиши, в то время как элемент управления находится в фокусе. Это событие всегда происходит после события KeyDown и перед событием KeyUp. Различие между событиями KeyDown И KeyPress СОСТОИТ В ТОМ, ЧТО KeyDown передает код нажатой клавиши, a KeyPress — соответствующее значение char клавиши. KeyUp Происходит при освобождении клавиши, в то время как элемент управления находится в фокусе. Это событие всегда происходит после событий KeyDown и KeyPress GotFocus Происходит, когда элемент управления получает фокус. Это событие не следует использовать для выполнения проверки допустимости элементов управления. В этом случае вместо него следует применять события Validating и Validated LostFocus Происходит, когда элемент управления теряет фокус. Это событие не следует использовать для выполнения проверки допустимости элементов управления. В этом случае вместо него следует применять события validating и validated MouseDown Происходит при помещении указателя мыши над элементом управления и нажатии кнопки мыши. Это событие не эквивалентно событию Click, поскольку MouseDown происходит сразу после нажатия кнопки мыши и перед ее освобождением MouseMove Происходит непрерывно в процессе перемещения указателя мыши над элементом управления Mouseup Происходит при помещении указателя мыши над элементом управления и освобождении кнопки мыши Paint Происходит при прорисовке элемента управления Validated Запускается, когда элемент управления, свойство CausesValidation которого установлено равным true, готов принять фокус. Это событие запускается по завершении события validating, и оно указывает на завершение проверки Validating Запускается, когда элемент управления, свойство CausesValidation которого установлено равным true, готов принять фокус. Обратите внимание, что проверяемым элементом управления является тот, который теряет фокус, а не тот, который его получает
440 Часть II. Программирование для Windows • but tool System.Windows.Forms.Button U -J - И (DataB«x*ngs) AutoSceChanged BackColorChanged BackgroundImageCh< BackgroundlmageLay ftndngCortextChafx CausesVaUationChai ChangeUICues МНЯНь Context MenuStrpCh. ControlAdded ControlRemoved Cursor Changed DockChanged DragOrop - 4 Occurs *»^en the control is ckked. Рис. 15.5. Отображение списка событий Многие из этих событий использованы в примерах этой главы. Все примеры следуют одному и тому же формату: вначале мы создаем визуальное представление формы, выбирая и размещая элементы управления, и лишь затем приступаем добавлению обработчиков событий — основной объем работы примеров выполняется именно на этом этапе. Существуют три основных способа обработки конкретного события. Первый — двойной щелчок на элементе управления. В результате происходит обращение к обработчику события, используемого по умолчанию данного элемента управления — это событие различно для различных элементов управления. Если это событие то, что требуется — прекрасно. В противном случае существуют два возможных подхода. Один — использование списка событий в окне Properties (Свойства), отображаемого при щелчке на кнопке с пиктограммой молнии, показанной на рис. 15.5. Затененное событие — событие элемента управления, используемое по умолчанию. Чтобы добавить обработчик для конкретного события, дважды щелкните на этом событии в списке событий. В результате будет сгенерирован код подписки элемента управления на событие и сигнатура метода обработки этого события. Или же можно ввести имя метода для обработки конкретного события в поле, расположенном рядом с данным событием в списке Events, и после нажатия клавиши <Enter> будет сгенерирован обработчик события с выбранным именем. Вторая возможность — добавление кода подписки на событие вручную. Мы будем часто применять этот подход в следующей главе, добавляя код в конструктор формы после вызова метода InitializeComponent (). Даже при вводе кода, необходимого для связывания с событием, Visual Studio обнаруживает выполняемое действие и предлагает добавить в код сигнатуру метода, как если бы операция выполнялась из Forms Designer. Каждый из этих двух подходов требует выполнения двух действий — подписки на событие (т.е. связывания с ним) и создания соответствующей сигнатуры метода обработчика. Двойной щелчок на элементе управления и попытка обработки другого события посредством замены сигнатуры метода события, используемого по умолчанию, на событие, которое действительно нужно обработать, окажется безуспешной — необходимо также изменить код подписки на событие в методе InitializeComponent (). Поэтому упомянутый обходной путь в действительности не может служить быстрым способом обработки конкретных событий. Теперь можно приступить к рассмотрению самих элементов управления. И мы начнем с того, с который, без сомнения, приходится использовать несчетное количество раз при работе с Windows-приложениями: элемента управления Button. Элемент управления Button Когда говорят о кнопке, вероятно, в первую очередь на ум приходит прямоугольная кнопка, на которой можно щелкать для выполнения той или иной задачи. Однако .NET Framework предоставляет класс, производный от класса Control — System. Windows . Forms .ButtonBase, — который реализует основные функциональные возможности, необходимые в элементе управления Button, что позволяет программистам выполнять наследование из этого класса и создавать собственные нестандартные элементы управления Button.
Глава 15. Основы программирования для Windows 441 Пространство имен System. Windows . Forms предоставляет три элемента управления, производные от класса ButtonBase: Button, CheckBox и RadioButton. Этот раздел посвящен элементу управления Button (являющемуся стандартной, хорошо известной прямоугольной кнопкой). Остальные два элемента управления освещены в последующих разделах этой главы. Элемент управления Button присутствует практически в каждом диалоговом окне Windows, которое можно себе представить. В основном кнопка служит для выполнения трех видов задач. □ Закрытие диалогового окна с определенным состоянием (например, кнопки ОК и Cancel (Отмена)). □ Выполнение действия с данными, введенными в диалоговом окне (например, при щелчке на кнопке Search (Поиск) после ввода какого-либо критерия поиска). □ Открытие другого диалогового окна или приложения (например, кнопки Help (Справка)). Работа с элементом управления Button очень проста. Обычно она состоит из добавления элемента управления в форму и двойного щелчка на нем для добавления кода в событие Click — как правило, этого достаточно для большинства приложений. Свойства элемента управления Button В этом разделе рассмотрены некоторые свойства элемента управления Button. В результате вы получите представление о том, как его можно использовать. Наиболее часто применяемые свойства класса Button (даже если в действительности они определены в базовом классе ButtonBase) перечислены в табл. 15.3. Для ознакомления с полным перечнем свойств, обратитесь к документации .NET Framework SDK. Таблица 15.3. Часто используемые свойства класса Button Свойство Описание Flatstyle Изменяет стиль кнопки. Если в качестве стиля установлен Popup, кнопка выглядит плоской до тех пор, пока пользователь не поместит указатель мыши над ней. В этом случае внешний вид кнопки изменяется на трехмерный Enabled Хотя это свойство наследуется из класса Control, оно указано в этой таблице, поскольку очень важно для кнопки. Установка его значения равным false означает, что кнопка становится затемненной и щелчок на ней не вызывает никаких последствий image Указывает изображение (растровое, пиктограмму и т.п.), которое будет отображаться на кнопке imageAlign Указывает позицию изображения на кнопке События элемента управления Button До сих пор наиболее часто используемое событие кнопки — Click. Это событие происходит при каждом щелчке пользователя на кнопке — т.е. при нажатии и отпускании левой кнопки мыши в то время, когда указатель располагается над кнопкой. Следовательно, если нажать левую кнопку мыши и отвести указатель мыши в сторону от кнопки до освобождения кнопки мыши, событие Click не будет запущено. Кроме того, событие Click запускается, когда кнопка находится в фокусе и пользователь нажимает клавишу <Enter>. Это событие всегда нужно обрабатывать при наличии кнопки в форме.
442 Часть II. Программирование для Windows В следующем практическом занятии мы создадим диалоговое окно с тремя кнопками. Две из них изменяют язык интерфейса с английского на датский и обратно. (Можете использовать любые языки по своему выбору.) Последняя кнопка закрывает диалоговое окно. Практическое занятие Работа с кнопками Чтобы создать небольшое Windows-приложение, которое использует три кнопки для изменения текста в заголовке диалогового окна, выполните следующие действия. 1. Создайте новое Windows-приложение ButtonTest в каталоге С: \BegVCSharp\ Chapterl5. 2. Разверните панель инструментов Toolbox, щелкнув на пиктограмме с канцелярской кнопкой, расположенной рядом с символом х в верхнем правом углу окна, и три раза выполните двойной щелчок на элементе управления Button. Разместите кнопки и измените размеры формы, как показано на рис. 15.6. 3. Щелкните на кнопке правой кнопкой мыши и из контекстного меню выберите пункт Properties. Измените имя каждой кнопки, как показано на рис. 15.6, выбирая в окне Properties поле редактирования (Name) и вводя соответствующий текст. 4. Измените свойство Text каждой кнопки в соответствии с ее именем, но опустите при этом префикс button. 5. Чтобы было понятно, к какому языку выполняется переход, перед текстом желательно отображать соответствующий флаг. Выберите кнопку English (Английский) и найдите свойство Image (Изображение). Щелкните справа от него, чтобы открыть диалоговое окно, в котором можно будет добавить изображения в файл ресурсов формы. Щелкните на кнопке Import (Импортировать) и выполните просмотр доступных пиктограмм. Нужные нам пиктограммы включены в состав файлов проекта ButtonTest, который доступен для загрузки на сайте издательства. Выберите пиктограммы UK. PNG и DK. PNG. 6. Выберите UK и щелкните на кнопке ОК. Затем выберите кнопку buttonDanish, щелкните на поле (...) свойства Image и выберите DK, прежде чем щелкнуть на кнопке ОК. 7. На этом этапе текст и пиктограмма кнопки размещены друг поверх друга, поэтому нужно изменить способ выравнивания пиктограммы. Для обеих кнопок English и Danish измените значение свойства ImageAlign на MiddleLef t. 8. Возможно, ширину кнопок придется изменить, чтобы тест начинался не сразу за изображениями. Для этого выберите каждую из кнопок и раздвиньте селекторную метку, расположенную у правого края кнопки. 9. И, наконец, щелкните на форме и измените свойство Text на Do you speak English? (Вы говорите по-английски?) ■a1 Foiml [buttonEnglishl 1 buttonOK '"SOBl buttonDanish | || J - Do you spe.ik English? jDEJO (English [И Danish OK Рис. 15.6. Размещение кнопок Рис. 15.7. Окончательный вид диалогового окна
Глава 15. Основы программирования для Windows 443 Вот и все, что касается интерфейса пользователя этого диалогового окна. Теперь диалоговое окно должно выглядеть подобно приведенному на рис. 15.7. Сейчас можно приступать к добавлению обработчиков событий. Дважды щелкните на кнопке English. Это приведет непосредственно к обработчику события элемента управления, используемому по умолчанию — для кнопки таким событием является событие Click, поэтому именно его обработчик создается. Добавление обработчиков событий После двойного щелчка на кнопке English добавьте следующий код в обработчик события: private void buttonEnglish_Click(object sender, EventArgs e) { this.Text = "Do you speak English?"; } Когда Visual Studio создает метод для обработки такого события, в качестве имени метода присваивается имя элемента управления, за которым следуют символ подчеркивания и имя обрабатываемого события. Первый параметр события Click — object sender — содержит элемент управления, на котором был выполнен щелчок. В данном примере им всегда будет элемент управления, указанный именем метода. В других случаях для обработки события многие элементы управления могут использовать один и тот же метод. В таких случаях для выяснения того, какой элемент управления вызывается, можно проверить это значение. Использование одного и того же метода для нескольких элементов управления описано в разделе "Элемент управления TextBox" далее в этой главе. Второй параметр, System.EventArgs e, содержит информацию о том, что происходит в действительности. В данном случае эта информация не требуется. Вернитесь к представлению Design View и дважды щелкните на кнопке Danish. Откроется обработчик события этой кнопки. Его код имеет следующий вид: private void buttonDanish_Click(object sender, EventArgs e) { this.Text = "Taler du dansk?"; } Этот метод идентичен методу btnEnglishClick, за исключением того, что текст отображается на датском языке. И в заключение добавьте обработчик кнопки ОК, как это уже было выполнено дважды. Однако теперь код несколько отличается от использованного ранее: private void buttonOK_Click(object sender, EventArgs e) { Application.Exit (); } На этом создание кода приложения, а вместе с ним и этого первого примера, завершается. Скомпилируйте и запустите его, и пощелкайте на кнопках. Вы должны получить результат, подобный показанному на рис. 15.8. ". 1.11-1 -III -I.III \ ' |ЕЭ Engfeh С ] ОК ша| ДДдмИ и Рис. 15.8. Работающее приложение переключения языков
444 Часть II. Программирование для Windows Элементы управления Label и LinkLabel Вероятно, элемент управления Label является наиболее часто используемым. Он присутствует практически в каждом диалоговом окне любого Windows-приложения. Этот элемент управления служит только одной цели — отображению текста внутри формы. .NET Framework включает два по-разному отображаемых элемента управления типа метки: □ Label — стандартная надпись Windows; □ LinkLabel — надпись, аналогичная стандартной (и производную от нее), но отображаемая в виде Internet-ссылки (гиперссылки). Оба типа надписей в форме приведены на рис. 15.9. и. Foiml laben ImkLabdl *шв J -J Рис. 15.9. Разновидности меток Обычно для стандартной надписи Label добавление кода обработки события не требуется, хотя она и поддерживает события, как все элементы управления. Однако в случае LinkLabel требуется дополнительный код, чтобы посредством щелчка на этом элементе управления пользователи могли осуществлять переход к цели ссылки. Для элемента управления Label можно определять множество свойств. Большинство из них унаследованы от элемента управления Control, но некоторые являются новыми. Наиболее часто используемые свойства перечислены в табл. 15.4. Если не указано иное, свойства являются общими для элементов управления Label и LinkLabel. Таблица 15.4. Часто используемые свойства классов Label и LinkLabel Свойство Описание BorderStyle Указывает стиль рамки вокруг надписи. По умолчанию рамка отсутствует Flatstyle * Определяет способ отображения элемента управления. Установка значения этого свойства равным Popup приводит к тому, что элемент выглядит плоским до тех пор, пока пользователь не помещает указатель мыши над ним. При этом элемент приобретает приподнятый вид image Указывает одиночное изображение (растровое, пиктограмму и т.п.), которое будет отображаться в элементе надписи imageAlign Указывает позицию отображения изображения в элементе Label LinkArea (Только для элемента управления LinkLabel.) Это свойство указывает часть текста, которая должна отображаться в качестве ссылки LinkColor (Только для элемента управления LinkLabel.) Это свойство указывает цвет ссылки Links (Только для элемента управления LinkLabel.) Элемент управления LinkLabel может содержать более одной ссылки. Это свойство позволяет выбрать нужную ссылку. Элемент управления отслеживает ссылки, отображенные в тексте. Свойство недоступно во время разработки
Глава 15. Основы программирования для Windows 445 Окончание табл. 15.4 Свойство Описание LinkVisited (Только для элемента управления LinkLabel.) Установка этого свойства равным true означает, что цвет ссылки изменяется после щелчка на ней Text Align Указывает позицию отображения текста в элементе управления VisitedLinkColor (Только для элемента управления LinkLabel.) Это свойство указывает цвет элемента управления LinkLabel после щелчка на нем Элемент управления TextBox Текстовые поля нужно использовать в тех случаях, когда пользователям требуется предоставить возможность вводить текст, о котором ничего не известно во время разработки (например, имя пользователя). Основное назначение текстового поля — предоставление пользователю возможности ввода текста. Но, хотя возможен ввод любых символов, пользователей можно ограничить вводом только числовых значений. .NET Framework предоставляет два основных элемента управления для приема текста от пользователей: TextBox и RichTextBox. Оба эти элемента управления являются производными от базового класса TextBoxBase, который сам является производным от класса Control. TextBoxBase предоставляет основные функциональные возможности манипулирования текстом в текстовом поле, такие как выбор текста, вырезание и вставка из буфера обмена, и широкое множество событий. Пока мы не будем особо вникать в то, какие свойства наследуются из того или иного класса, а рассмотрим вначале более простой из этих двух элементов управления — TextBox. Вначале создадим пример, демонстрирующий свойства элемента TextBox, а затем расширим его для демонстрации свойств RichTextBox. Свойства элемента управления TextBox Существует слишком много свойств, чтобы описывать их все. В табл. 15.5 перечислены только наиболее часто используемые из них. Таблица 15.5. Часто используемые свойства класса TextBox Свойство Описание CausesValidation Когда элемент управления, у которого это свойство установлено равным true, готов принять фокус, запускаются два события: validating и Validated. Обработку этих свойств можно выполнять для проверки допустимости данных в элементе управления, который утрачивает фокус. В результате может возникать ситуация, когда элемент управления никогда не получит фокус. Соответствующие события освещены ниже. CharacterCasing Значение, указывающее, изменяет ли элемент управления TextBox регистр введенного текста. Возможные значения следующие: Lower — весь введенный текст преобразуется в строчный Normal — текст не подвергается никаким изменениям Upper — весь введенный текст преобразуется в прописной
446 Часть И. Программирование для Windows Окончание табл. 15.5 Свойство Описание Значение, которое указывает максимальную длину (в символах) любого текста, введенного в элементе управления TextBox. Если максимальная длина должна ограничиваться только доступным объемом памяти, установите это значение равным нулю Указывает, является ли данный элемент многострочным, т.е. может ли отображать несколько строк текста. Когда это свойство установлено равным true, обычно значение свойства Wordwrap также устанавливают равным true Указывает, должен ли символ пароля замещать реальные символы, введенные в однострочном элементе TextBox. Если значение свойства Multiline установлено равным true, это свойство не оказывает никакого влияния Булевское значение, указывающее, является ли текст доступным только для чтения Указывает, должен ли элемент управления TextBox отображать линейки прокрутки Текст, который выбран в элементе управления TextBox Количество символов, выбранных в тексте. Если это значение больше общего числа символов в тексте, элемент управления переустанавливает его равным общему количеству символов минус значение свойства SelectionStart Начало выбранного текста в элементе управления TextBox Указывает, должен ли многострочный элемент TextBox автоматически переносить слова на следующую строку, если длина строки превышает ширину элемента управления События элемента управления TextBox Тщательность проверки допустимости текста, введенного в элементах управления TextBox формы, может оказывать решающее влияние на то, будут пользователи удовлетворены или же раздосадованы результатом. Вы, вероятно, на собственном опыте убедились, как неприятно, когда допустимость содержимого диалогового окна проверяется только после щелчка на кнопке ОК. Обычно такой подход к проверке допустимости данных ведет к отображению окна сообщения о том, что данные в "текстовом поле номер три" неверны. В этом случает можно продолжать щелкать на кнопке ОК до тех пор, пока все данные не будут введены правильно. Очевидно, что такой подход к проверке допустимости данных — не самый рациональный. Но что можно с этим поделать? Ответ заключается в обработке событий проверки допустимости данных, предоставляемых элементом управления TextBox. Для обеспечения невозможности ввода в текстовом поле недопустимых символов или гарантирования ввода значений только из допустимого диапазона необходимо указать пользователю, допустимо ли введенное значение. Список событий, предоставляемых элементом управления TextBox, дан в табл. 15.6 (все они унаследованы от элемента управления Control). MaxLength Multiline PasswordChar Readonly ScrollBars SelectedText SelectionLength SelectionStart Wordwrap
Глава 15. Основы программирования для Windows 447 Таблица 15.6. Часто используемые события класса TextBox Событие Описание Enter Эти четыре события происходят в указанном в этой таблице порядке. Они Leave называются событиями фокуса и, за двумя исключениями, запускаются во Validating всех случаях изменения фокуса элемента управления. События Validating и Validated Validated запускаются, только если свойство CausesValidation элемента управления, который принимает фокус, установлено равным true. Причина запуска события именно принимающим фокус элементом состоит в том, что в некоторых случаях элемент управления не нужно проверять даже при изменениях фокуса. Пример такой ситуации — щелчок на кнопке Help (Справка) KeyDown Эти три события называют событиями клавиш. Они позволяют отслеживать и KeyPress изменять текст, введенный в элементах управления.-События KeyDown и KeyUp KeyUp принимают код клавиши, соответствующий нажатой клавише. Это позволяет выявлять нажатие специальных клавиш, таких как <Shift>, <Ctrl> или <F1> И наоборот, событие KeyPress принимает символ, соответствующий клавише клавиатуры. Это означает, что значение буквы а не совпадает со значением буквы А. Это удобно, если нужно исключить диапазон символов — например, разрешить ввод только численных значений Textchanged Происходит при каждом изменении текста в текстовом поле, независимо от сути изменения В следующем практическом примере мы создадим диалоговое окно, в котором можно вводить имя, адрес, род занятий и возраст. Назначение этого примера — получение необходимых навыков в манипулировании свойствами и использовании событий, а не создание какого-либо действительно полезного приложения. .n^KT,weCKOe3aHI,Tlfe Работа с элементом управления TextBox Вначале создадим интерфейс пользователя. 1. Создайте новое Windows-приложение TextBoxTest в каталоге С: \BegVCSharp\ Chapterl5. 2. Перетаскивая элементы управления Label, TextBox и Button в область конструирования, создайте форму, показанную на 15.10. Прежде чем размеры двух элементов управления TextBox — textBoxAddress и textBoxOutput — можно будет изменить, как показано на рисунке, значение их свойства Multiline потребуется установить в true. Для этого щелкните на элементах управления правой кнопкой мыши и из контекстного меню выберите пункт Properties. 3. Назовите элементы управления, как показано на рис. 15.10. 4. Установите свойство Text всех остальных элементов управления в соответствии с их именами, за исключением префиксов, указывающих тип элемента (т.е. Button, TextBox и Label). Свойство Text формы установите равным TextBoxTest. 5. Свойство Scrollbars элементов управления txtOutput и txtAddress установите равным Vertical. 6. Значение свойства Readonly элемента управления txtOutput установите равным true.
448 Часть II. Программирование для Windows ■й Foiml labdName testBoxName labettddress (extBoxAddress labdOccupafon textBoxCbcupation labeftge textBoxAge bbdOutput textBoxOutput " QSQI ( buttonOK | | buttonHdp I J II Рис. 15.10. Пользовательский интерфейс приложения TextBoxTest 7. Значение свойства CausesValidation кнопки btnHelp установите равным false. Вспомните из описания событий Validating и Validated, что установка этого свойства равным false позволяет пользователям щелкать на этой кнопке, не беспокоясь о допустимости введенных данных. 8. После того, как размеры формы и элементов управления подобраны, следует определить привязку элементов управления так, чтобы они вели себя нужным образом при изменении размеров формы. Установите значение свойства Anchor для каждого типа элемента управления. Вначале, удерживая нажатой клавишу <Ctrl>, последовательно выберите все элементы управления типа TextBox, за исключением textBoxOutput. Выбрав все эти элементы управления, в окне Properties установите значение свойства Anchor равным Top, Left, Right. Тем самым вы установите значение свойства Anchor для всех выбранных элементов типа TextBox. Выберите элемент управления textBoxOutput и установите значение его свойства Anchor равным Top, Bottom, Left, Right. Теперь установите значение свойства Anchor обоих элементов управления Button равным Top, Right. Причина того, что элемент управления txtOutput привязывается, а не пристыковывается к нижнему краю формы в том, размер области вывода текста должна изменяться при растягивании формы. Пристыковка элемента управления к нижнему краю формы приводила бы к его перемещению синхронно с нижним краем формы, но не к изменению его размеров. 9. Теперь остается определить последние свойства. Найдите свойства Size и MinimumSize формы. Форма будет практически бесполезной, если ее размеры меньше используемых в настоящий момент. Поэтому установите значение свойства MinimumSize равным значению Size. Описание полученных результатов Работа по определению визуальной части формы завершена. При выполнении приложения щелчок на кнопках или ввод текста не приведут ни к какому видимому результату, но при максимизации или растягивании диалогового окна элементы управления будут вести себя надлежащим образом, сохраняя свое взаимное расположение и изменяя размеры для заполнения всей области диалогового окна.
Глава 15. Основы программирования для Windows 449 Добавление обработчиков событий В представлении Design View дважды щелкните на кнопке buttonOK. Повторите это действие для второй кнопки. Как было показано в примере создания кнопки, ранее в этой главе, это ведет к созданию обработчиков события Click. Щелчок на кнопке ОК должен приводить к передаче текста из текстовых полей ввода в поле вывода, доступное только для чтения. Код двух обработчиков событий Click имеет следующий вид: private void buttonOK_Click(object sender, EventArgs e) { // Никакой проверки недопустимости значений не выполняется, // поскольку это не обязательно string output; // Конкатенация текстовых значений четырех элементов управления типа TextBox. output = "Name: " + this.textBoxName.Text + "\r\n"; output += "Address: " + this.textBoxAddress.Text + "\r\n"; output += "Occupation: " + this.textBoxOccupation.Text + "\r\n"; output += "Age: " + this.textBoxAge.Text; // Вставка нового текста. this.textBoxOutput.Text = output; } private void buttonHelp_Click(object sender, EventArgs e) { // Запись краткого описания каждого элемента TextBox в поле Output. string output; output = "Name = Your name\r\n"; output += "Address = Your address\r\n"; output += "Occupation = Only allowed value is 'Programmer'\r\n"; output += "Age = Your age"; /* string output; output = "Name = Ваше имя\г\п"; output += "Address = Ваш адрес\г\п"; output += "Occupation = Единственное допустимое значение 'Programmer'\r\n"; output += "Age = Ваш возраст"; */ // Вставка нового текста. this. textBoxOutput.Text = output; } Обе функции используют свойство Text каждого элемента управления TextBox. Свойство Text элемента управления textBoxAge используется для получения значения, введенного в качестве возраста данного лица, а это же свойство элемента управления textBoxOutput служит для отображения объединенного текста. Вставка информации, введенной пользователем, выполняется без проверки ее правильности. Следовательно, проверка должна выполняться где-то в другом месте программы. В данном примере для проверки правильности значений используется ряд критериев. □ Имя пользователя не может быть пустым. □ Возраст пользователя должен быть числом, которое больше или равно нулю. □ Профессией пользователя должна быть "Programmer" или же это значение должно быть оставлено пустым. □ Адрес пользователя не может быть пустым.
450 Часть II. Программирование для Windows Следовательно, проверка, которая должна выполняться для двух текстовых полей (textBoxName и textBoxAddress), идентична. Кроме того, необходимо воспрепятствовать вводу недопустимого значения в поле Age (Возраст), а также проверить, подтверждает ли пользователь, что он является программистом. Чтобы воспрепятствовать щелчку на кнопке ОК до ввода каких-либо данных, начнем с установки ее свойства Enabled равным false — на этот раз в конструкторе формы, а не в окне Properties. При установке свойств в конструкторе это должно выполняться не ранее вызова сгенерированного кода в функции InitializeComponent (): public Forml() { InitializeComponent (); this.buttonOK.Enabled = false; } Теперь создадим обработчик для двух текстовых полей, которые необходимо проверять на отсутствие пустых значений. Это выполняется посредством подписки на событие Validating текстовых полей. Информирование элемента управления о необходимости обработки события осуществляется методом txtBoxEmptyValidating () — таким образом, этот единый метод обработки события используется для двух различных элементов управления. Нам требуется также способ выяснения состояния элементов управления. Для этого применяется свойство Tag элемента управления TextBox. Вспомните, что из среды Forms Designer свойству Tag можно присваивать только строки. Однако, поскольку установка значения Tag выполняется из кода, возможности его использования достаточно велики, т.к. свойство Tag принимает тип object, который в данном случае больше подходит для ввода булевского значения. Добавьте в конструктор следующие строки: this.buttonOK.Enabled = false; // Значения свойства Tag, предназначенные для проверки допустимости данных this.textBoxAddress.Tag = false/ this.textBoxAge.Tag = false; this.textBoxName.Tag = false; this.textBoxOccupation.Tag = false; // Подписки на события this.textBoxName.Validating += new System.ComponentModel.@@ta CancelEventHandler(this.textBoxEmpty_Validating); this.textBoxAddress.Validating += new System.ComponentModel.CancelEventHandler(this.textBoxEmpty_Validating) ; Обратите внимание, что при вводе фрагмента += кода обработчика события Visual Studio обнаруживает добавление обработчиков события к объекту и предлагает остальную часть текста. Однократное нажатие клавиши <ТаЬ> ведет к вставке текста, в том числе и нового оператора, с предложенным именем метода. Повторное нажатие клавиши <ТаЬ> ведет к вставке метода, действительно обрабатывающего событие, с любым введенным именем. Очень часто достаточно будет просто два раза нажать клавишу <ТаЬ>, но в данном случае, прежде чем второй раз нажать клавишу <ТаЬ> не забудьте изменить имя метода на textBoxEmptyValidating, чтобы один и тот же метод обрабатывал оба события. В отличие от ранее рассмотренного обработчика события кнопки, обработчик события Validating является специализированной версией стандартного обработчика System.EventHandler. Это событие нуждается в специальном обработчике, поскольку в случае отрицательного результата проверки требуется способ предотвращения
Глава 15. Основы программирования для Windows 451 любой дальнейшей обработки. Отмена дальнейшей обработки по существу означала бы невозможность выхода из текстового поля до момента ввода допустимых данных. Использование событий Validating и Validated в сочетании со свойством CausesValidation позволяет решить неприятную проблему, возникавшую при использовании событий GotFocus и LostFocus для проверки допустимости элементов управления в предшествующих версиях Visual Studio. Проблема возникала при непрерывном генерировании событий GotFocus и LostFocus вследствие того, что проверяемый код пытался перемещать фокус между элементами управления, что создавало бесконечный цикл. Замените оператор throw в обработчике события, сгенерированном программой Visual Studio, следующим кодом: private void textBoxEmpty_Validating (object sender, System.ComponentModel.CancelEventArgs e) { // Нам известно, что отправитель — объект TextBox, // поэтому мы приводим объект-отправитель к этому типу. TextBox tb = (TextBox)sender; // Если текст пуст, мы устанавливаем красный цвет фона // элемента управления Textbox, чтобы указать на наличие проблемы. // Для указания того, содержит ли элемент управления допустимую // информацию, мы используем свойство Tag элемента управления. if (tb.Text.Length == 0) { tb.BackColor = Color.Red; tb.Tag = false; // В данном случае отмена дальнейшей обработки не требуется, но если //бы это потребовалось, нужно было бы добавить следующую строку: // е.Cancel = true; } else { tb.BackColor = System.Drawing.SystemColors.Window; tb.Tag = true; } // И, наконец, мы вызываем функцию ValidateOK, которая // будет устанавливать значение кнопки ОК. ValidateOK(); } Поскольку этот метод используется для обработки события более чем одним текстовым полем, неизвестно, какое из них вызывает функцию. Однако известно, что результат вызова метода должен быть одинаковым, независимо от вызывающего объекта. Поэтому можно просто привести параметр sender к типу TextBox: TextBox tb = (TextBox)sender; Если длина текста в текстовом поле равна нулю, цвет фона нужно установить красным, а значение свойства Tag равным false. В противном случае в качестве цвета фона устанавливается стандартный цвет окна Windows. Для установки стандартного цвета в элементе управления всегда следует использовать цвета, определенные в перечислении System, Drawing. SystemColors. Если просто установить цвет белым, приложение будет выглядеть странно в случае изменения пользователем заданных по умолчанию цветовых настроек.
452 Часть II. Программирование для Windows Описание функции ValidateOK () приведено в конце этого примера. Следующий обработчик события Validating, который нужно добавить, предназначен для текстового поля Occupation (Профессия). Процедура добавления полностью аналогична описанной для двух предыдущих обработчиков, но код проверки допустимости отличается, поскольку, чтобы быть допустимым значение профессии должно быть Programmer (Программист), либо пустой строкой. Поэтому добавим в конструктор новую строку: this.textBoxOccupation.Validating += new System.ComponentModel.CancelEventHandler(this.textBoxOccupation_Validating); Теперь можно добавить сам обработчик: private void textBoxOccupation_Validating(object sender, System.ComponentModel.CancelEventArgs e) { // Приведение объекта-отправителя к типу textbox. TextBox tb = (TextBox)sender; // Проверка правильности значений. if (tb.Text.CompareTo("Programmer") == 0 | | tb.Text.Length == 0) { tb.Tag = true; tb.BackColor = System.Drawing.SystemColors.Window; } else { tb.Tag = false; tb.BackColor = Color.Red; } // Установка состояния кнопки ОК. ValidateOK(); } Предпоследняя задача, которую остается решить — предоставление обработчика для текстового поля Age (Возраст). Необходимо, чтобы пользователи вводили только положительные числа (включая 0, для упрощения проверки). Для этого мы используем событие KeyPress для удаления любых нежелательных символов до того, как они будут отображены в текстовом поле. Мы ограничим также количество символов, которые могут быть введены в элемент управления, тремя. Вначале установите значение MaxLength элемента управления textBoxAge равным 3. Затем подпишитесь на событие KeyPress посредством двойного щелчка на событии KeyPress в списке Events окна Properties (выберите список Events, щелкнув на пиктограмме молнии). Обработчик события KeyPress также является специализированным. Мы предоставляем обработчик System.Windows .Forms.KeyPressEventHandler, поскольку событие нуждается в информации о нажатой клавише. В сам обработчик события нужно добавить следующий код: private void textBoxAge_KeyPress (object sender, KeyPressEventArgs e) { if ((e.KeyChar < 48 I I e.KeyChar > 57) && e.KeyChar != 8) e.Handled = true; // Удаление символа } ASCII-значения для символов от 0 до 9 лежат в диапазоне от 48 до 57, поэтому нужно удостовериться, что введенный символ относится к этому диапазону — за одним исключением. ASCII-значение символа 8 представляет клавишу забоя, и в целях упрощения редактирования это поведение следует оставить без изменения. Установка значения свойства Handled объекта KeyPressEventArgs равным true указывает элементу управ-
Глава 15. Основы программирования для Windows 453 ления, что он не должен выполнять с символом никаких других действий. Поэтому если нажатая клавиша не является клавишей цифры или забоя, она не отображается. На данный момент элемент управления не помечен как недопустимый или допустимый. Это связанно с тем, что для определения того, было ли что-то введено в элементе управления, требуется выполнение еще одной проверки. Это не представляет сложности, поскольку метод для выполнения такой проверки уже создан. Просто подпишитесь на событие Validating для элемента управления Age, добавив в конструктор следующую строку: this.textBoxAge.Validating += new System.ComponentModel.CancelEventHandler(this.textBoxEmpty_Validating); Остается решить всего одну задачу. Если пользователь вводит допустимый текст во все текстовые поля, а затем вносит какие-то недопустимые изменения, кнопка ОК остается активизированной. Поэтому необходимо добавить последний обработчик события для всех текстовых полей: обработчик события Change, который будет отключать кнопку ОК, если любое текстовое поле содержит недопустимые данные. Событие TextChanged происходит при каждом изменении текста в текстовом поле. Подписка на событие выполняется посредством выбора всех четырех описанных текстовых полей и ввода имени textBoxTextChanged в списке Events. Это ведет к назначению единого обработчика для всех четырех событий. Событие TextChanged использует стандартный обработчик события, известный по событию Click. Наконец, добавьте само событие: private void textBox_TextChanged(object sender, System.EventArgs e) { // Приведение объекта-отправителя к типу Textbox. TextBox tb = (TextBox)sender; // Проверка допустимости данных и установка значения // свойства tag и цвета фона соответствующим образом, if (tb.Text.Length == 0 && tb != textBoxOccupation) { tb.Tag = false; tb.BackColor = Color.Red; } else if (tb == textBoxOccupation && (tb.Text.Length != 0 && tb.Text.CompareTo("Programmer") != 0)) { // He следует устанавливать цвет здесь, поскольку во время // ввода данных пользователем цвет будет изменяться. tb.Tag = false; } else { tb.Tag = true; tb.BackColor = SystemColors.Window; } // Вызов функции ValidateOK для установки состояния кнопки ОК. ValidateOKO ; } На этот раз необходимо точно выяснить, какой элемент управления вызывает обработчик события, поскольку нежелательно, чтобы цвет фона текстового поля Occupation изменялся на красный, когда пользователь начинает вводить данные. Это достигается посредством проверки свойства Name текстового поля, которое было передано в параметре sender.
454 Часть II. Программирование для Windows Остается определить последний компонент зирует или отключает кнопку ОК: private void ValidateOK() метод ValidateOK, который активи- // Активизирует кнопку ОК, если значения всех свойств Tags — true. this.buttonOK.Enabled = ((bool)(this.textBoxAddress.Tag) && (bool)(this.textBoxAge.Tag) && (bool)(this.textBoxName.Tag) && (bool)(this.textBoxOccupation.Tag)); } Метод просто устанавливает значение свойства Enabled кнопки OK равным true, если значения всех свойств Tag — true. Значение свойств Tag необходимо привести к булевскому, поскольку оно хранится в качестве типа object. Если теперь протестировать программу, результат должен выглядеть подобно приведенному на рис. 15.11 (естественно, без красного фона). Обратите внимание, что можно щелкнуть на кнопке Help (Справка), находясь в текстовом поле с недопустимыми данными, и при этом цвет фона не будет изменяться на красный. » Foiml 'УЫЫ] Name Address Occupation Age j Output Jacob Hammer Pedersen Programmer 331 1 H* | Рис. 15.11. Результат тестирования программы Только что завершенная программа достаточно длинна по сравнению с другими, приведенными в этой главе, поскольку они будут строиться на основе этого примера, а не с нуля. Исходные коды всех примеров, приведенных в книге, доступны для загрузки на сайте издательства. Элементы управления RadioButton и Checkbox Как уже было сказано, элементы управления RadioButton и CheckBox имеют тот же базовый класс, что и элемент управления Button, хотя по внешний виду и использованию они существенно отличаются от него.
Глава 15. Основы программирования для Windows 455 Традиционно элементы управления RadioButton (переключатели) отображаются в виде надписи с маленькой окружностью слева от нее, которая может быть выбрана или не выбрана. Переключатели следует применять, когда пользователю нужно предоставить возможность выбора между взаимоисключающими опциями — например, пола пользователя. Чтобы сгруппировать переключатели для образования единого логического блока, следует использовать элемент управления GroupBox или какой-то иной контейнер. При первоначальном помещении элемента GroupBox на форму и последующем размещении необходимых элементов управления RadioButton внутри его границ, состояние элементов RadioButton будет автоматически изменено для отражения того, что только одна опция внутри групповой рамки может быть выбрана. Если элементы управления RadioButton размещены не внутри элемента управления GroupBox, на форме в любой момент времени может быть выбран только один из них. Элемент управления CheckBox (флажок) отображается в виде надписи с расположенным слева от него маленьким квадратом. Флажок следует использовать, когда пользователю нужно предоставить возможность выбора одной или более опций — например, для заполнения вопросника об использовавшихся ранее операционных системах (Windows ХР, Windows Vista, Linux и т.п.). После рассмотрения важных свойств и событий этих двух элементов управления, начиная с RadioButton, мы рассмотрим краткий пример их использования. Свойства элемента управления RadioButton Поскольку элемент управления RadioButton является производным от ButtonBase и поскольку общие свойства уже были рассмотрены в примере использования элемента управления Button, остается рассмотреть всего несколько свойств (они перечислены в табл. 15.7). Полный перечень свойств приведен в документации по .NET Framework SDK. Таблица 15.7. Часто используемые свойства класса RadioButton Свойство Описание Appearance Переключатель может отображаться в виде надписи с круглой меткой выбора слева, в середине или справа от нее, или же в виде стандартной кнопки. При отображении в виде стандартной кнопки выбранный элемент управления выглядит нажатым, а не выбранный — отжатым AutoCheck Когда значение этого свойства равно true, при щелчке на переключателе в окружности выбора отображается черная точка. Когда это значение равно false, установка флажка переключателя должна выполняться в коде вручную из обработчика события click CheckAlign Это свойство используется для изменения способа выравнивания части переключателя, определяющей флажок. Его значение по умолчанию — ContentAlignment.MiddleLeft Checked Указывает состояние элемента управления. Значение этого свойства равно true, если черная точка отображается в элементе управления. В противном случае оно равно false События элемента управления RadioButton Как правило, при работе с элементами управления RadioButton придется использовать только одно событие, но возможна подписка и на множество других. В этой
456 Часть II. Программирование для Windows главе освещены только два события, описанные в табл. 15.8, причем второе упомянуто только для того, чтобы обратить внимание на незначительное различие между ними. Таблица 15.8. Часто используемые события класса RadioButton Событие Описание CheckedChanged Click Отправляется при изменении состояния выбора элемента управления RadioButton Отправляется при каждом щелчке на элементе управления RadioButton. Это событие не эквивалентно событию CheckedChange, поскольку при двукратном и более щелчке на элементе управления RadioButton свойство checked изменяется только один раз — и только, если оно еще не было выбрано. Более того, если значение свойства AutoCheck кнопки, на которой выполняется щелчок, равно false кнопка вообще не будет выбрана, и только событие click будет отправляться Свойства элемента управления Checkbox Как легко догадаться, свойства и события этого элемента управления подобны свойствам и событиям элемента управления RadioButton, но в табл. 15.9 перечислены два новых свойства. Таблица 15.9. Часто используемые свойства класса CheckBox Свойство Описание CheckState ThreeState В отличие от переключателя, флажок может пребывать в трех состояниях: Checked, Indeterminate и Unchecked. Когда состояние флажка — indeterminate, индикатор состояния флажка, расположенный рядом с надписью, обычно затемнен, указывая, что либо текущее значение флажка недопустимо, либо оно не может быть определено по какой-либо причине (например, флажок указывает состояние "только для чтения" двух выбранных файлов, из которых один доступен только для чтения, а второй — нет), либо не имеет смысла в данной ситуации Когда значение этого свойства — false, пользователь не сможет изменять состояние CheckState на indeterminate. Однако значение свойства CheckState все же можно изменять на Indeterminate из кода События элемента управления CheckBox Обычно в этом элементе управления придется использовать одно или два события. Хотя событие CheckChanged применяется и в RadioButton, и в CheckBox, его действие различно в этих двух элементах управления. События элемента управления CheckBox описаны в табл. 15.10. Это завершает ознакомление с событиями и свойствами элементов управления RadioButton и CheckBox. Но прежде чем приступить к рассмотрению примера их использования, рассмотрим упомянутый ранее элемент управления GroupBox.
Глава 15. Основы программирования для Windows 457 Таблица 15.10. Часто используемые события класса CheckBox Событие Описание CheckedChanged Происходит при каждом изменении свойства Checked флажка. Обратите внимание, что в элементе управления CheckBox, свойство ThreeState которого имеет значение true, можно выполнить щелчок на флажке без изменения значения свойства checked. Это происходит при изменении состояния флажка с Checked на Indeterminate CheckStateChanged Происходит при каждом изменении свойства CheckedState флажка. Поскольку и Checked, и Unchecked — возможные значения свойства CheckedState, это событие отправляется при каждом изменении свойства checked. Кроме того, оно отправляется также при изменении СОСТОЯНИЯ С Checked на Indeterminate Элемент управления GroupBox Элемент управления GroupBox часто используют для логического группирования набора элементов управления, таких как RadioButton и CheckBox, и для отображения заголовка и рамки вокруг этого набора. Использование групповой рамки сводится к ее перетаскиванию поверх формы и последующему перетаскиванию в нее тех элементов управления, которые она должна содержать (но не в обратном порядке — т.е., нельзя наложить групповую рамку поверх уже существующих элементов управления). В результате родительским элементом элементов управления становится групповая рамка, а не форма, что делает возможным наличие в форме более одного выбранного переключателя в любой конкретный момент времени. Однако внутри групповой рамки только один переключатель может быть выбран. Вероятно, отношение между родительским и дочерним элементом требует некоторых дополнительных пояснений. Когда элемент управления помещается в форму, форма становится его родительским элементом, и, следовательно, элемент управления является дочерним элементом формы. При помещении элемента управления GroupBox на форму, он становится дочерним элементом формы. Поскольку групповая рамка сама может содержать элементы управления, она становится их родительским элементом. В результате перемещение элемента управления GroupBox ведет к перемещению всех помещенных в нее элементов управления. Еще одно следствие помещения элементов управления в групповую рамку то, что на них можно оказывать влияние, изменяя соответствующее свойство групповой рамки. Например, если нужно отключить все элементы управления внутри элемента управления GroupBox, можно просто установить значение свойства Enabled элемента GroupBox равным false. Применение элемента управления GroupBox продемонстрировано в следующем практическом занятии. Практическое занятие Использование элементов управления RadioButton и CheckBox В этом упражнении мы изменим пример TextBoxTest, созданный ранее во время демонстрации применения текстовых полей. В этом примере единственной возможной профессией была профессия программиста. Вместо того чтобы вынуждать пользователей полностью вводить этот текст, изменим это текстовое поле на флажок. В
458 Часть II. Программирование для Windows целях демонстрирования применения переключателя попросим пользователей предоставить дополнительную информацию: указать свой пол. Измените пример использования текстовых полей следующим образом. 1. Удалите надпись labelOccupation и текстовое поле textBoxOccupation. 2. Добавьте элементы управления CheckBox, a GroupBox и два элемента управления RadioButton и назовите их, как показано на рис. 15.12. Обратите внимание, что элемент управления GroupBox расположен на вкладке Containers (Контейнеры) панели Toolbox. В отличие от других, использованных до сих пор элементов управления, элемент управления GroupBox находится в разделе Containers (Контейнеры) панели Toolbox. щт—и— иг D Sex О i«fto6uttonFe>rate О r«faeuttor*laie Рис. 15.12. Измененная форма 3. Значения свойств Text элементов управления RadioButton и CheckBox должны совпадать с именами элементов управления без первых трех букв, а значением свойства Text элемента управления GroupBox должно быть Sex. 4. Установите значение свойства Checked флажка checkBoxProgrammer равным true. Обратите внимание, что значение свойства CheckState автоматически изменяется на Checked. 5. Установите значение свойства Checked переключателя radioButtonMale или radioButtonFemale равным true. Обратите внимание, что нельзя установить равным true значения этого свойства для обоих переключателей. При попытке проделать это со второй кнопкой значение первого переключателя RadioButton автоматически изменится на false. Реализация визуальных аспектов примера не требует никаких дополнительных действий, но в код нужно внести ряд изменений. Сначала необходимо удалить все ссылки на удаленное текстовое поле. Внесите следующие изменения в существующем коде. 1. В конструкторе формы удалите две строки, которые ссылаются на textBoxOccupation. Ими являются подписки на событие Validating и строка, которое устанавливает значение свойства Tag равным false. 2. Полностью удалите метод txtOccupation_Validating ().
Глава 15. Основы программирования для Windows 459 Описание полученных результатов Метод txtBoxTextChanged включает в себя проверки для определения того, что вызывающим элементом управления было текстовое поле textBoxOccupation. Теперь известно, что это не так (поскольку этот элемент был удален), поэтому метод нужно изменить, удалив из него блок else if и изменив проверку if следующим образом: private void textBox_TextChanged(object sender, System.EventArgs e) { // Приведение объекта-отправителя к типу TextBox. TextBox tb = (TextBox)sender; // Проверка допустимости данных и установка значения // свойства tag и цвета фона соответствующим образом. if (tb.Text.Length = 0) { tb.Tag = false/ tb. BackColor = Color. Red; } else { tb.Tag = true; tb.BackColor = SystemColors.Window; } // Вызов функции ValidateOK для установки состояния кнопки ОК. ValidateOKO ; } Еще одно место, где выполнялась проверка удаленного текстового поля — метод ValidateOK (). Полностью удалите проверку, чтобы код приобрел следующий вид: private void ValidateOKO { // Активизирует кнопку OK, если значения всех свойств Tag - true, this.buttonOK.Enabled = ( (bool) (this.textBoxAddress.Tag) && (bool)(this.textBoxAge.Tag) && (bool)(this.textBoxName.Tag)); } Поскольку теперь мы используем флажок, а не текстовое поле, нам известно, что пользователь не может ввести никакую недопустимую информацию, поскольку он всегда либо является программистом, либо не является им. Известно также, что пользователь является либо мужчиной, либо женщиной, и поскольку значение свойства одного из переключателей установлено равным true, пользователь лишен возможности выбора недопустимого значения. Поэтому остается только изменить текст справки и вывод программы. Это выполняется в обработчиках событий кнопок: private void buttonHelp_Click(object sender, System.EventArgs e) { // Запись краткого описания каждого элемента TextBox в поле Output. string output; output = "Name = Your name\r\n"; output += "Address = Your address\r\n"; output += "Programmer = Check 'Programmer' if you are a programmer\r\n"; output += "Sex = Choose your sex\r\n"; output += "Age = Your age"; /* output = "Name = Ваше имя\г\п"; output += "Address = Ваш адрес\г\п"; output += "Programmer = Установите флажок 'Programmer', если вы являетесь программистом\г\п"; output += "Sex = Выберите свой пол\г\п";
460 Часть II. Программирование для Windows Ч output "Age = Ваш возраст"; // Вставка нового текста, this.textBoxOutput.Text = output; } Изменения претерпел только текст справки, поэтому метод help не таит никаких сюрпризов. Метод ОК представляет несколько больший интерес: private void buttonOK_Click(object sender, EventArgs e) { // Никакая проверка недопустимости значений не выполняется, // поскольку это не обязательно. string output; // Конкатенация текстовых значений четырех элементов управления типа TextBox. output = "Name: " + this.textBoxName.Text + "\r\n"; output += "Address: " + this.textBoxAddress.Text + "\r\n"; output += "Occupation: " + (string)(this.checkBoxProgrammer.Checked ? "Programmer" : "Not a programmer") + "\r\n"; output += "Sex: " -i- (string) (this.radioButtonFemale.Checked ? "Female" : "Male") + "\r\n"; output += "Age: " + this.textBoxAge.Text; // Вставка нового текста. this.textBoxOutput.Text = output; Первая из выделенных строк — строка, в которой выводится профессия пользователя. Мы исследуем свойство Checked элемента управления Checkbox и, если его значение — true, выводится строка Programmer (Программист). Если это значение — false, выводится строка Not a programmer (He программист). Вторая строка анализирует только переключатель radioButtonFemale. Если значение свойства Checked этого элемента управления — true, значит пользователь — женщина. Если оно — false, значит пользователь — мужчина. Можно было бы, чтобы ни один из этих переключателей не был выбран во время запуска программы, но выбор одного из них во время разработки гарантирует выбор одного из них при любых обстоятельствах. Теперь при запуске примера результат должен быть подобным показанному на рис. 15.13. г—7-—: Name Jacob Налитою Pedersen '■ Addrets Arhus. Denmark I E Programmer Sex •■:v Female Mate ! Age 33 Output Name: Jacob Hammer Pedetten Addiets Arhus. Denmark Occupation. Programmer Sex Male Age: 33 I с г. П& OK I J н* 1 Рис. 15.13. Измененная программа в действии
Глава 15. Основы программирования для Windows 461 Элемент управления RichTextBox Подобно обычному элементу управления TextBox, RichTextBox является производным от элемента управления TextBoxBase. Поэтому он имеет ряд свойств общих с TextBox, но различий значительно больше. В то время как TextBox обычно используется для получения коротких текстовых строк от пользователя, RichTextBox служит для отображения и ввода форматированного текста (например, полужирного, подчеркнутого и курсивного). Это достигается посредством использования стандарта форматированного текста, получившего название Rich Text Format (расширенный текстовый формат), или RTF. В предыдущем примере мы использовали стандартный элемент управления TextBox. С таким же успехом можно было применять RichTextBox. Фактически, как показано в последующем примере, вместо элемента управления TextBox с именем textBoxOutput можно вставить элемент RichTextBox с таким же именем, и пример будет вести себя точно так же, как ранее. Свойства элемента управления RichTextBox Поскольку этот вид текстового поля сложнее исследованного в предыдущем разделе, не удивительно, что он обладает большим количеством доступных для использования свойств. Наиболее часто используемые свойства элемента управления RichTextBox описаны в табл. 15.11. Таблица 15.11. Часто используемые свойства класса RichTextBox Свойство Описание CanRedo CanUndo RedoActionName DetectUrls Rtf SelectedRtf SelectedText SelectionAlignment Значение этого свойства — true, когда последняя отмененная операция может быть снова применена с помощью метода Redo Значение этого свойства — true, если возможна отмена последнего действия, выполненного по отношению к элементу управления RichTextBox. Обратите внимание, что свойство CanUndo определено в классе TextBoxBase, поэтому оно доступно также и для элементов управления TextBox Содержит имя действия, которое должно быть выполнено методом Redo Значение этого свойства необходимо установить равным true, если требуется, чтобы элемент управления обнаруживал URL-адреса и форматировал их (подчеркивал, как это имеет место в браузере) Соответствует свойству Text, за исключением того, что содержит текст в RTF-формате Служит для получения или установки текста, выбранного в элементе управления, в RTF-формате. При копировании этого текста в другое приложение — например, Word — он сохранит все форматирование Как и свойство SelectedRtf, это свойство можно использовать для получения или установки выбранного текста. Однако, в отличие от RTF- версии свойства, все форматирование утрачивается Представляет выравнивание выбранного текста. Свойство может принимать значения Center, Left или Right
462 Часть II. Программирование для Windows Окончание табл. 15.11 Свойство Описание SelectionBullet Bulletlndent SelectionColor SelectionFont SelectionLength SelectionType ShowSelectionMargm UndoActionName SelectlonProtected Это свойство служит для определения того, должен ли выбранный текст содержать маркеры абзацев, а также для вставки и удаления маркеров Указывает количество пикселей отступа маркера Изменяет цвет текста в выборке Изменяет шрифт текста в выборке Устанавливает или извлекает длину выборки Содержит информацию о выборке. Это свойство будет сообщать о том, выбран ли один или более объектов OLE, или же только текст Если значение этого свойства — true, слева от элемента управления RichTextBox будет отображаться граница. Она облегчает выбор текста пользователю Извлекает имя действия, которое будет использовано, если пользователь решит отменить что-либо Установка значения этого свойства равным true позволяет указать, что определенные фрагменты текста не должны изменяться Как видно из приведенного перечня, большинство новых свойств связано с выбором. Это обусловлено тем, что любое форматирование, которое будет применять в процессе работы пользователя с текстом, скорее всего, будет применяться к выборке, осуществленной пользователем. Если никакой выбор не выполнен, форматирование начинается с позиции курсора в тексте, называемой точкой вставки. События элемента управления RichTextBox Большинство событий, используемых элементом управления RichTextBox, совпадают с таковыми для элемента TextBox, но некоторые из новых свойств, представляющих интерес, перечислены в табл. 15.12. Таблица 15.12. Часто используемые события класса RichTextBox Событие Описание LinkClicked Protected SelectionChanged Отправляется, когда пользователь щелкает на ссылке внутри текста Отправляется, когда пользователь пытается изменять текст, который помечен как защищенный Отправляется при изменении выборки. Если по какой-либо причине изменение выборки пользователем нежелательно, это событие позволяет воспрепятствовать этому В следующем практическом занятии мы создадим очень простой текстовый редактор. Пример демонстрирует способы изменения общего форматирования текста, а также способы загрузки и сохранения текста из элемента управления RichTextBox. Для простоты загрузка и сохранение выполняется в одном и том же фиксированном файле.
Глава 15. Основы программирования для Windows 463 Практическое занятие ИСПОЛЬЗОВаНИв ЭЛвМвНТЭ управления RichTextBox Как всегда, начнем с разработки формы. 1. Создайте новое Windows приложение RichTextBoxTest на С# в каталоге C:\BegVCSharp\Chapterl5. 2. Создайте форму, как показано на рис. 15.14. Текстовое поле txtSize должно быть элементом управления типа TextBox. Текстовое поле RichTextBoxText должно быть элементом управления типа RichTextBox. : ■, Hlehlextlox l«tt J j псК1ехОо<Гех1 [ bulo-Cofc | bJ(onUnd«ii>e [ xtonialc | [ tuBorCete | 1**1э(» UrfBoSbi [ ЬЛЪЛ** | | Ь.*ЮпС««е J Puc. 15.14. Форма приложения RichTextBoxTest 3. Назовите элементы управления, как показано на рис. 15.14. 4. Установите свойство Text всех элементов управления, кроме текстовых полей, в соответствии с их именами, за исключением первой части имени, описывающей тип элемента управления. 5. Измените значение свойства Text текстового поля textBoxSize на 10. 6. Выполните привязку элементов управления, как показано в табл. 15.13. Таблица 15.13. Привязка элементов управления для приложения RichTextBoxTest Имя элемента управления Значение привязки buttonLoad и buttonSave richTextBoxText Все остальные Bottom Top, Left, Bottom, Right Top 7. Установите значение свойства MinimumSize формы таким же, как у свойства Size.
464 Часть II. Программирование для Windows Описание полученных результатов На этом реализация визуальной части примера завершается. Переходя непосредственно к работе с кодом, дважды щелкните на кнопке Bold, чтобы добавить в код обработчик события Click. Код обработчика события имеет следующий вид: private void buttonBold_Click(object sender, EventArgs e) { Font oldFont; Font newFont; // Получение шрифта, используемого в выбранном тексте. oldFont = this.richTextBoxText.SelectionFont; // Если в настоящий момент шрифт использует полужирный стиль, // нужно удалить форматирование, if (oldFont.Bold) newFont = new Font(oldFont, oldFont.Style & -FontStyle.Bold); else newFont = new Font(oldFont, oldFont.Style I FontStyle.Bold); // Вставка нового шрифта и возвращение фокуса элементу управления RichTextBox. this.richTextBoxText.SelectionFont = newFont; this.richTextBoxText.Focus(); } Начнем с получения шрифта, используемого в текущей выборке, и присваивания его локальной переменной oldFont. Затем проверим, не является ли эта выборка уже набранной полужирным шрифтом. Если да, атрибут полужирного шрифта нужно удалить. В противном случае его нужно установить. Новый шрифт создается с применением oldFont в качестве прототипа, но с добавлением или удалением полужирного стиля при необходимости. И, наконец, мы присваиваем новый шрифт выборке и возвращаем фокус элементу управления RichTextBox — объект Font подробнее описан в главе 33. Обработчики событий для кнопок buttonltalic и buttonUnderline аналогичны рассмотренному, за исключением того, что они выполняют проверку на наличие соответствующих стилей. Дважды щелкните на кнопках Italic и Underline и добавьте следующий код: private void buttonUnderlme_Click (object sender, EventArgs e) { Font oldFont; Font newFont; // Получение шрифта, используемого в выбранном тексте. oldFont = this.richTextBoxText.SelectionFont; // Если в настоящий момент шрифт использует подчеркнутый стиль, его нужно удалить. if (oldFont.Underline) newFont = new Font(oldFont, oldFont.Style & -FontStyle.Underline); else newFont = new Font(oldFont, oldFont.Style I FontStyle.Underline); // Вставка нового шрифта. this.richTextBoxText.SelectionFont = newFont; this.richTextBoxText.Focus(); } private void buttonItalic_Click(object sender, EventArgs e) { Font oldFont; Font newFont;
Глава 15. Основы программирования для Windows 465 // Получение шрифта, используемого в выбранном тексте. oldFont = this.richTextBoxText.SelectionFont; // Если в настоящий момент шрифт использует курсивный стиль, его нужно удалить, if (oldFont.Italic) newFont = new Font(oldFont, oldFont.Style & -FontStyle.Italic); else newFont = new Font(oldFont, oldFont.Style | FontStyle.Italic); // Вставка нового шрифта. this.richTextBoxText.SelectionFont = newFont; this.richTextBoxText.Focus (); } Дважды щелкните на последней кнопке форматирования Center и добавьте следующий код: private void buttonCenter_Click(object sender, EventArgs e) { if (this.richTextBoxText.SelectionAlignment == HorizontalAlignment.Center) this.richTextBoxText.SelectionAlignment = HorizontalAlignment.Left; else this.richTextBoxText.SelectionAlignment = HorizontalAlignment.Center; this.richTextBoxText.Focus (); } В этом методе нужно проверить еще одно свойство — SelectionAlignment, чтобы проверить, центрирован ли уже текст в выборке. Это требуется потому, что нам необходимо, чтобы кнопка вела себя подобно переключателю — если текст выровнен по центру, он становится выровненным по левому краю; в противном случае он становится выровненным по центру. Свойство HorizontalAlignment представляет собой перечисление, которое может принимать значения Left (по левому краю), Right (по правому краю), Center (по центру), Justify (по ширине) и NotSet (без выравнивания). В данном случае мы просто проверяем, установлено ли значение Center. Если да, то мы устанавливаем выравнивание по левому краю. В противном случае мы устанавливаем выравнивание по центру. Последняя задача, которую сможет выполнять наш простенький текстовый редактор — установка размера текста. Мы добавим два обработчика событий для текстового поля Size: один для управления вводом и один для обнаружения момента завершения ввода значения пользователем. В списке Events окна Properties найдите события KeyPress и Validated элемента управления textBoxSize и дважды щелкните на них, чтобы добавить в код обработчики этих событий. В отличие от события Validating, использованного ранее, событие Validated происходит после завершения всей проверки. Оба события используют вспомогательный метод ApplyTextSize, который принимает строку, содержащую размер текста: private void textBoxSize_KeyPress(object sender, KeyPressEventArgs e) { // Удаление всех символов, не являющихся цифрами, // символом забоя или клавишей <Enter>. if ((e.KeyChar < 48 | | e.KeyChar > 57) && e.KeyChar != 8 && e.KeyChar != 13) { e.Handled = true; } else if (e.KeyChar == 13) {
466 Часть II. Программирование для Windows // Применение размера, если пользователь нажимает клавишу <Enter>. TextBox txt = (TextBox)sender; if (txt.Text.Length > 0) ApplyTextSize(txt.Text) ; e.Handled = true; this.richTextBoxText.Focus(); } } private void textBoxSize_Validated(object sender, EventArgs e) { TextBox txt = (TextBox)sender; ApplyTextSize(txt.Text); this.richTextBoxText.Focus(); } private void ApplyTextSize (string textSize) { // Преобразование текста в плавающий, поскольку это потребуется вскоре. float newSize = Convert.ToSingle (textSize); FontFamily currentFontFamily; Font newFont; // Создание нового шрифта этого же семейства, но с новым размером. currentFontFamily = this.richTextBoxText.SelectionFont.FontFamily; newFont = new Font(currentFontFamily, newSize); // Установка нового шрифта в качестве шрифта выбранного текста, this.richTextBoxText.SelectionFont = newFont; } Событие Key Press препятствует вводу пользователем любых значений кроме целочисленных и вызывает метод ApplyTextSize, если пользователь нажимает клавишу <Enter>. Интересующие нас действия выполняются во вспомогательном методе ApplyTextSize (). Он начинает свою работу с преобразования размера строки из string в float. Как уже было отмечено, приложение препятствует вводу пользователем чего-либо кроме целочисленных значений, но при создании нового шрифта требуется тип float, поэтому мы выполняем преобразование к нужному типу. После этого мы извлекаем семейство, к которому принадлежит данный шрифт, и создаем новый шрифт этого же семейства, но нового размера. И, наконец, мы устанавливаем новый шрифт в качестве шрифта выбранного текста. Вот и все форматирование, которое можно выполнить, но кое-какие задачи решаются самим элементом управления RichTextBox. Если теперь запустить пример, можно будет определять для текста полужирный, курсивный и подчеркнутый шрифт и центрировать текст. Это именно то, что требовалось, но обратите внимание на еще одно интересное обстоятельство: попытайтесь ввести в тексте Web-адрес — например, www. wrox. com. Текст, распознанный элементом управления в качестве Internet-адреса, подчеркивается, а указатель мыши при его помещении над таким текстом изменяет свою форму на изображение руки. Если вы полагаете, что на этом тексте можно щелкнуть, чтобы перейти к соответствующей странице, то почти правы. Но вначале необходимо выполнить обработку события, которое отправляется, когда пользователь щелкает на ссылке: LinkClicked. Найдите событие LinkClicked в списке Events окна Properties и дважды щелкните на нем, чтобы добавить обработчик этого события в код. С этим обработчиком события вы еще не встречались — он используется для предоставления текста ссылки, на которой был выполнен щелчок. Этот обработчик удивительно прост:
Глава 15. Основы программирования для Windows 467 private void richTextBoxText_LinkClicked (object sender, System.Windows.Forms.LinkClickedEventArgs e) { System.Diagnostics.Process.Start(e.LinkText); } Этот код открывает используемый по умолчанию браузер, если он еще не был открыт, и переходит к сайту, указанному ссылкой, на которой был выполнен щелчок. Часть приложения, связанная с редактированием, готова. Остается только реализовать загрузку и сохранение содержимого элемента управления. Для этого мы будем использовать один и тот же фиксированный файл. Дважды щелкните на кнопке Load (Загрузить) и добавьте следующий код: private void buttonLoad_Click(object sender, EventArgs e) { Загрузка файла в элемент управления RichTextBox. try { richTextBoxText.LoadFile("Test.rtf"); } catch (System.10.FileNotFoundException) { // Файла для загрузки нет MessageBox.Show("No file to load yet"); } } Вот и все! Мы больше ничего не можем сделать. Поскольку мы имеем дело с файлами, всегда существует вероятность возникновения исключительных ситуаций, и нужно быть готовым справиться с ними. Метод Load выполняет обработку исключения, генерируемого, если файл не существует. Сохранение файла — столь же простая задача. Дважды щелкните на кнопке Save (Сохранить) и добавьте следующий код: private void buttonSave_Click(object sender, EventArgs e) { // Сохранение текста. try { richTextBoxText.SaveFile("Test.rtf"); } catch (System.Exception err) { MessageBox.Show(err.Message); } } Снова запустите пример, выполните форматирование какого-либо текста и щелкните на кнопке Save (Сохранить). Очистите содержимое текстового поля и щелкните на кнопке Load — только что сохраненный текст должен снова появиться. На этом создание примера применения элемента управления RichTextBox завершается. Теперь при его запуске результат должен быть подобным показанному на рис. 15.15.
468 Часть II. Программирование для Windows » Pi.|iT**llW<T*tf - Ю| | O^d | Uidair» Ш | | I^to | «« 10 ThDWa>;v/:bcit: car be ound a: HTTP://www wrox com Yuu com d.wnluad Jla ъш. il = llmJw иг If v .uuk hum il Load Рис. 15.15. Работающее приложение RichTex t Box Те st Элементы управления ListBox и CheckedListBox Списки используются для отображения списков строк, из которых одновременно можно выбирать одну или более строк. Подобно флажкам и переключателям, списки предоставляют способ затребовать от пользователя выбор одного или более элементов. Список следует использовать в тех случаях, когда во время разработки неизвестно точное количество значений, из которых пользователей может осуществлять выбор (например, в списке сотрудников). Даже если все возможные значения известны во время разработки, над применением списка следует подумать при наличии большого числа значений. Класс ListBox является производным от класса ListControl, который предоставляет основные функциональные возможности элементов управления типа списка, поставляемых с каркасом .NET Framework. Еще один вид доступного списка — CheckedListBox. Будучи производным от класса ListBox, он предоставляет список, подобно ListBox, но кроме текстовых строк он предоставляет также флажок для каждого элемента в списке. Свойства элемента управления ListBox Свойства, описанные в табл. 15.14, существуют в обоих классах ListBox и CheckedListBox, если не указано иное. Таблица 15.14. Часто используемые свойства классов ListBox и CheckedListBox Свойство Описание Selectedindex Это значение указывает начинающийся с нуля индекс элемента, выбранного в списке. Если одновременно список может содержать несколько выборок, это свойство содержит индекс первого элемента в выборке ColumnWidth в списке, содержащем несколько столбцов, это свойство указывает ширину столбцов items Коллекция items содержит все элементы списка. Свойства этой коллекции используются для добавления и удаления элементов
Глава 15. Основы программирования для Windows 469 Окончание табл. 15.14 Свойство Описание MultiColumn Selectedlndices Selectedltem Selectedltems SelectionMode Sorted Text Checkedlndices Checkedltems CheckOnClick ThreeDCheckBoxes Список может содержать более одного столбца. Используйте это свойство для выяснения или установки того, должны ли значения отображаться в виде столбцов Коллекция, содержащая начинающиеся с нуля индексы выбранных элементов списка В списке, в котором возможен выбор только одного элемента, это свойство содержит выбранный элемент, если таковой существует. В списке, в котором возможен выбор более одного элемента, оно будет содержать первый из выбранных элементов Коллекция, содержащая все элементы, выбранные в текущий момент времени Перечисление ListSelectionMode в списке позволяет выбирать один из четырех режимов выбора: None — ни один элемент не может быть выбран One — только один элемент может быть выбран в каждый конкретный момент времени MultiSimple — возможен выбор нескольких элементов. При использовании этого стиля при щелчке на элементе он становится выбранным и остается таким даже в случае щелчка на другом элементе до повторного щелчка на нем MultiExtended — возможен выбор нескольких элементов. Для осуществления выбора можно использовать клавиши <Ctrl>, <Shift> и клавиши со стрелками. В отличие от режима MultiSimple, простой щелчок на одном элементе, а затем щелчок на другом будет приводить к выбору только второго элемента, на котором был выполнен щелчок Когда значение этого свойства установлено равным true, элемент управления ListBox выполняет упорядочение содержащихся в нем элементов по алфавиту Со свойствами Text мы встречались при рассмотрении многих элементов управления, но это работает иначе, чем все ранее рассмотренные. При установке свойства Text элемента управления ListBox он выполняет поиск элемента, который соответствует указанному тексту, и выбирает его. При получении свойства Text возвращенным значением является первый выбранный элемент списка. Использование этого свойства невозможно, если значение свойства SelectionMode равно None. (Только для checkedListBox.) Это свойство — коллекция, содержащая индексы всех элементов в CheckedListBox, которые находятся в состоянии Checked или Indeterminate (Только для CheckedListBox.) Это свойство — коллекция, содержащая все элементы в CheckedListBox, которые находятся в состоянии Checked или Indeterminate (Только для CheckedListBox.) Если значение этого свойства — true, элемент будет изменять свое состояние при каждом щелчке на нем (Только для CheckedListBox.) Устанавливая это свойство, можно выбирать для использования плоские и обычные элементы управления checkBox
470 Часть II. Программирование для Windows Методы элемента управления ListBox Чтобы эффективно работать со списком, нужно знать ряд методов, которые можно вызывать. Наиболее часто используемые методы описаны в табл. 15.15. Если не указано иное, эти методы принадлежат обоим классам ListBox и CheckedListBox. Таблица 15.15. Часто используемые события классов ListBox и CheckedListBox Метод Описание ClearSelectedO FindStringO FindStringExact() GetSelectedO SetSelectedO ToStringO GetltemCheckedO GetltemCheckState() SetltemCheckedO SetltemCheckState() Очищает все выборки в элементе управления ListBox Находит в элементе управления ListBox первую строку, начинающуюся с указанной строки (например, Findstring ("а") найдет в ListBox первую строку, начинающуюся с символа "а") Подобен методу Findstring, но вся строка должна совпадать с указанной Возвращает значение, указывающее то, выбран ли элемент Устанавливает или очищает выбор элемента Возвращает элемент, выбранный в текущий момент времени (Только для CheckedListBox.) Возвращает значение, указывающее на то, выбран ли элемент (Только для CheckedListBox.) Возвращает значение, указывающее состояние установки флажка элемента (Только для CheckedListBox.) Устанавливает указанный элемент в состояние Checked (Только для CheckedListBox.) Устанавливает состояние установки флажка элемента События элемента управления ListBox Обычно события, о которых требуется знать при работе с элементами управления ListBox или CheckedListBox, связаны также с выборками, выполняемыми пользователем. События элемента управления ListBox описаны в табл. 15.16. Таблица 15.16. Часто используемые события классов ListBox и CheckedListBox Событие Описание ItemCheck (Только для CheckedListBox.) Происходит при изменении состояния установки флажка одного из элементов списка SelectedindexChanged Происходит при изменении индекса выбранного элемента В следующем практическом примере мы создадим оба элемента управления — ListBox и CheckedListBox. Пользователи могут установить флажки элементов в элементе управления CheckedListBox, а затем щелкнуть на кнопке, которая перемещает выбранные элементы в обычный список ListBox.
Глава 15. Основы программирования для Windows 471 практическое занятие Работа с элементом управления ListBox Создайте диалоговое окно следующим образом. 1. Создайте новое Windows-приложение List Boxes в каталоге С: \BegVCSharp\ Chapterl5. 2. Добавьте в форму элементы управления ListBox, CheckedListBox и Button и измените их имена, как показано на рис. 15.16. ~ ~ ~~ П checkedListBoxPossibleValue """f—lfnlP шш№п [ buttonMove ] listBoxSelected =======r==========================^ -J J Рис. 15.16. Форма приложения List Boxes 3. Измените значение свойства Text кнопки на Move. 4. Измените значение свойства CheckOnClick элемента управления CheckedListBox на true. Описание полученных результатов Мы начнем с проверки свойства Count коллекции Checkedlterns. Его значение будет больше нуля, если любые элементы в коллекции выбраны. Затем мы очищает все элементы в списке listBoxSelected и циклически просматриваем коллекцию Checkedltems, добавляя каждый элемент в список listBoxSelected. И, наконец, мы удаляем все флажки в CheckedListBox. Теперь лишь нужно, чтобы список CheckedListBox содержал какие-то элементы для перемещения. Элементы можно добавлять в представлении Design View, выбирая свойство Items в окне Properties и добавляя элементы, как показано на рис. 15.17. Элементы можно также добавлять в коде — например, в конструкторе формы: String I ollectlon EdHoi "ТВЕГ Г" inter the strings in the colection (one per Ine): One Two Three Four Five Six Seven Eight Nine 1 <* M Cancel Puc. 15.17. Элементы добавляются в редакторе строковых коллекций
472 Часть II. Программирование для Windows public Forml() // // Необходимо для поддержки конструктора Windows Form. // InitializeComponent(); // Добавление десятого элемента в CheckedListBox. this.checkedListBoxPossibleValue.Items.Add("Ten"); } В этом коде мы добавляет десятый элемент в список CheckedListBox, поскольку девять элементов уже были добавлены из конструктора. На этом создание примера использования списка завершается. Если теперь запустить его, результат должен быть подобен показанному на рис. 15.18. Для получения представленного результата были выбраны элементы Two, Four и Six, после чего была нажата кнопка Move (Переместить). ■sj List Boxes □ one □ Two П Thr*e □ Four □ Five □Sbc ■■ □ Seven □ Eight □ Nine □ Ten II | Move | .- :zl -^i Two Fou Six Рис. 15.18. Работающее приложение List Boxes Добавление обработчиков событий Теперь можно добавить необходимые фрагменты кода. Когда пользователь щелкает на кнопке Move, нужно найти отмеченные флажками элементы и скопировать их в правый список. Дважды щелкните на кнопке и введите следующий код: private void buttonMove_Click(object sender, EventArgs e) { // Проверка наличия каких-либо помеченных флажками // элементов в списке CheckedListBox. if (this.checkedListBoxPossibleValue.Checkedltems.Count > 0) { // Очистка списка ListBox, в который будет выполняться перемещение this.listBoxSelected.Items.Clear(); // Циклический просмотр коллекции Checkedltems списка CheckedListBox //и добавление элементов в список выбранных элементов. foreach (string item in this.checkedListBoxPossibleValue.Checkedltems) { this.listBoxSelected.Iterns.Add(item.ToString());
Глава 15. Основы программирования для Windows 473 // Очистка всех флажков в CheckedListBox. for (int i=0; i < this.checkedListBoxPossibleValue.Items.Count; i++) this.checkedListBoxPossibleValue.SetltemChecked (i, false); } } Элемент управления Listview На рис. 15.19 показан, вероятно, наиболее известный элемент управления ListView в среде Windows. Несмотря на то что теперь Windows предлагает множество дополнительных возможностей отображения файлов и папок, вы, несомненно, узнаете некоторые возможности, предоставляемые в элементе управления ListView, такие как отображение больших значков, представление подробных сведений и т.п. Name _j Images -jMag* _JProgrammng RECYCLER System Vokme Information _jTemp _JWebs*e See Type Fie Folder Fte Folder Ffe Folder File Folder File Folder Fie Folder Fie Folder DateModtfied 01-05-2004 02:54 20-02-2004 23:43 09-07-2004 21:35 08-03-2004 22:46 11-02-2004 20:07 24-05-2004 21:47 20-03-2004 00:52 Рис. 15.19. Элемент управления ListView Представление в виде списка обычно используется для данных, применительно к которым пользователь располагает определенным контролем над степенью подробности и стилем их представления. Данные, содержащиеся в элементе управления, можно представлять в виде столбцов и строк, подобном таблице, в виде единственного столба или в виде различных значков. Наиболее часто используемое представление в виде списка подобное показанному на предыдущем рисунке, которое используется для навигации по папкам на компьютере. Элемент управления ListView, пожалуй, наиболее сложен из всех, рассмотренных в этой главе, и освещение всех его аспектов выходит за рамки настоящей книги. В этой главе представлен пример, использующий многие наиболее важные функциональные возможности элемента управления ListView, в том числе подробное описание многочисленных доступных свойств, событий и методов, что все вместе обеспечит прочный фундамент для работы. Мы рассмотрим также элемент управления ImageList, который служит для хранения изображений, используемых в элементе управления ListView. Свойства элемента управления ListView Свойства элемента управления ListView описаны в табл. 15.17. Таблица 15.17. Часто используемые свойства класса ListView Свойство Описание Activation Управляет способом активизирования элемента пользователем в списочном представлении. Возможные значения следующие: standard — эта настройка отражает способ активизирования, выбранный пользователем для своего компьютера OneClick — одиночный щелчок на элементе активизирует его TwoCiick — двойной щелчок на элементе активизирует его
474 Часть II. Программирование для Windows Продолжение табл. 15.17 Свойство Описание Alignment AllowColumnReorder AutoArrange CheckBoxes Checkedlndices Checkedltems Columns Focusedltem FullRowSelect GridLines HeaderStyle Управляет способом выравнивания элементов в представлении в виде списка. Четыре возможных значения перечислены ниже: Default — если пользователь перетаскивает элемент, тот остается там, где был оставлен Left — элементы выравниваются по левому краю элемента управления ListView Тор — элементы выравниваются по верхнему краю элемента управления ListView SnapToGnd — элемент управления содержит невидимую сетку, к которой будут привязаны элементы Если значение этого свойства установлено равным true, пользователь имеет возможность изменять порядок следования столбцов в представлении в виде списка. При использовании этой возможности следует убедиться, что подпрограммы, которые заполняют списочное представление, способны правильно вставлять элементы даже после изменения порядка следования столбцов Если значение этого свойства установлено равным true, элементы будут автоматически выстраиваться в соответствии со свойством Alignment. Если пользователь перетаскивает элемент в центр списочного представления, а значение свойства Alignment — Left, элемент автоматически переместится к левому краю представления. Это свойство имеет смысл только в том случае, если значение свойства View — Largelcon или Smalllcon Если значение этого свойства установлено равным true, слева от каждого элемента в списочном представлении будет отображаться элементу управления checkBox. Это свойство имеет смысл только в том случае, если значением свойства view является Details или List Предоставляют соответственно доступ к коллекции индексов и элементов, которая содержит элементы списка, помеченные флажками Представление в виде списка может содержать столбцы. Это свойство предоставляет доступ к коллекции столбцов, посредством которой можно добавлять или удалять столбцы Содержит элемент, обладающий фокусом в списке. Если ни один элемент не выбран, это значение является нулевым Когда значение этого свойства — true, и на элементе выполнен щелчок, вся строка, содержащая этот элемент, выделяется. Если это значение — false, выделяется только сам элемент Если значение этого свойства установлено равным true, в списочном представлении вычерчиваются линии сетки между строками и столбцами. Это свойство имеет смысл только в том случае, если значением свойства View является Details Управляет способом отображения заголовков столбцов. Существуют три стиля: clickable — заголовок столбца работает подобно кнопке NonClickable — заголовки столбцов не реагируют на щелчки кнопкой мыши None — заголовки столбцов не отображаются
Глава 15. Основы программирования для Windows 475 Окончание табл. 15.17 Свойство Описание HoverSelection Items LabelEdit LabelWrap LargelmageList MultiSelect Scrollable Selectedlndices Selectedltems SmalllmageList Sorting StatelmageList Topltem View Когда значение этого свойства — true, пользователь может выбирать элемент в списочном представлении, задерживая указатель мыши над ним Коллекция элементов в представлении в виде списка Когда значение этого свойства — true, пользователь может редактировать содержимое первого столбца в представлении Details (Подробные сведения) Если значение этого свойства — true, надписи будут занимать столько строк, сколько требуется для отображения всего текста Содержит объект imageList, хранящий большие изображения. Эти изображения могут использоваться, когда значение свойства View равно Largelcon Установка этого свойства равным true позволяет пользователю выбирать несколько элементов Установка этого свойства равным true ведет к отображению линеек прокрутки Содержит коллекции, которые соответственно содержат выбранные индексы и элементы Когда значение свойства View — Small icon, это свойство содержит объект imageList, хранящий используемые изображения Позволяет представлению в виде списка выполнять упорядочение элементов, которые оно содержит. Существуют три возможных режима: Ascending — по возрастанию Descending — по убыванию None — без сортировки ImageList содержит маски изображений, которые используются в качестве накладываемых на изображения LargelmageList и SmalllmageList для представления нестандартных состояний Возвращает элемент, расположенный в верхней части представления в виде списка Представление в виде списка может отображать свои элементы в четырех различных режимах: Largelcon — все элементы представляются в виде большого значка C2x32) и надписи Small icon — все элементы представляются в виде маленького значка A6x16) и надписи List — отображается только один столбец. Этот столбец может содержать значок и надпись Details — возможно отображение любого числа столбцов. Только первый столбец может содержать значок Tile (доступно только на платформах Windows XP и последующих версиях Windows) — отображает большой значок, а справа от него — надпись и информацию о подэлементе
476 Часть II. Программирование для Windows Методы элемента управления Listview Для столь сложного элемента, как ListView, определено удивительно мало специализированных методов (табл. 15.18). Таблица 15.18. Часто используемые методы класса Listview Имя Описание BeginUpdate () ClearO EndUpdate() EnsureVisible() GetltemAt() Указывает представлению в виде списка о необходимости прекращения прорисовки обновлений до момента вызова метода EndUpdate (). Этот метод полезен при одновременной вставке множества элементов, поскольку он предотвращает мерцание представления и радикально увеличивает быстродействие Полностью очищает представление в виде списка. Все элементы и столбцы удаляются Этот метод вызывают после вызова метода BeginUpdate. При его вызове представление в виде списка прорисовывает все свои элементы Указывает представлению в виде списка о необходимости выполнения прокрутки для отображения элемента с указанным индексом Возвращает объект Listviewitem в позиции х, у представления в виде списка События элемента управления ListView События элемента управления ListView описаны в табл. 15.19. Таблица 15.19. Часто используемые события класса ListView Событие Описание AfterLabelEdit BeforeLabelEdit ColumnClick ItemActivate Происходит после редактирования надписи Происходит до того, как пользователь начинает редактировать надпись Происходит при щелчке на столбце Происходит при активизации элемента Listviewitem Элемент в списочном представлении всегда является экземпляром класса Listviewitem. Listviewitem содержит такую информацию, как текст и индекс пиктограммы, которую нужно отображать. Объекты Listviewitem имеют свойство Subltems, которое содержит экземпляры еще одного класса — ListViewSubltem. Эти подэлементы отображаются, если элемент управления ListView находится в режиме Details или Tile. Каждый из подэлементов представляет столбец в представлении в виде списка. Основное различие между подэлементами и основными элементами состоит в том, что подэлемент не может отображать значок. Объекты Listviewitem добавляются в ListView посредством коллекции Items, а объекты ListViewSubltems в Listviewitem — посредством коллекции Subltems в объекте Listviewitem.
Глава 15. Основы программирования для Windows 477 ColumnHeader Чтобы представление в виде списка отображало заголовки столбцов, в коллекцию Columns объекта ListView добавляют экземпляры класса ColumnHeader. Этот класс предоставляет заголовок столбцов, который может отображаться, когда элемент управления ListView отображается в режиме Details. Элемент управления imageList Элемент управления ImageList предоставляет коллекцию, которую можно применять для хранения изображений, используемых в других элементах управления формы. В списке изображений можно хранить изображения любого размера, но внутри каждого элемента управления все изображения должны быть одного размера. Применительно к элементу управления ListView это означает, что чтобы иметь возможность отображать и большие, и маленькие изображения, требуются два элемента управления ImageList. Класс ImageList — первый из представленных в этой главе элементов управления, который не отображается сам по себе во время выполнения. При его перетаскивании в разрабатываемую форму, он помещается не в саму форму, а в лоток под ней, содержащий все подобные компоненты. Эта замечательная особенность призвана предотвращать загромождение рабочей области конструктора форм элементами управления, которые не являются частью интерфейса пользователя. Манипулирование этим элементом управления осуществляется точно так же, как и любым другим, за исключением того, что его нельзя перемещать поверх формы. Изображения в элемент управления ImageList можно добавлять как во время разработки, так и во время выполнения. Если во время разработки изображения, которые нужно отображать, известны, их можно добавить, щелкая на кнопке, расположенной справа от свойства Images (Изображения). В результате откроется диалоговое окно, где можно перейти к изображениям, которые нужно вставить. Если решите добавить изображения во время выполнения, это осуществляется через коллекцию Images. Лучший способ ознакомления с использованием элемента управления ListView и связанных с ним списков изображений — рассмотрение примера. В следующем практическом примере мы создадим диалоговое окно с представлением в виде списка и двумя списками изображений. Представление в виде списка будет отображать файлы и папки, хранящиеся на жестком диске. Для простоты мы не будем извлекать соответствующие значки из файлов и папок, а будем использовать стандартный значок папки для папок и значок текстового файла для файлов. Двойной щелчок на папке позволяет перейти к дереву папки, а кнопка Back (Назад) дает возможность перемещаться вверх по дереву. Пять переключателей служат для изменения режима представления в виде списка во время выполнения. Двойной щелчок на файле будет приводить к попытке его выполнения. практическое занятие работа с элементом управления ListView Как всегда, начнем с создания интерфейса пользователя. 1. Создайте новое Windows-приложение ListView в каталоге С: \BegVCSharp\ Chapterl5. 2. Добавьте в форму элементы управления ListView, Button, Label и GroupBox. Затем добавьте пять переключателей в групповую рамку. Форма должна выглядеть подобно изображенной на рис. 15.20. Чтобы установить ширину элемента
478 Часть II. Программирование для Windows управления Label, установите значение его свойства AutoSize равным False. Сделайте ширину элемента управления Label равной ширине элемента управления ListView. aj ListView L labeCurrentPath | buttonBack | View Mode ij О radioBuKonLargdcon О rack* uttonSmal con О radio6uttonLttt О radoButtonDetafc О radoButtonTile Рис. 15.20. Форма приложения ListView 3. Назовите элементы управления в соответствие с рис. 15.20. ListView не будет отображать свое имя, как показано на рисунке. Дополнительный элемент был добавлен лишь для того, чтобы отобразить имя в этом примере — его добавление не обязательно. 4. Измените свойство Text переключателей и кнопки, чтобы оно совпадало с именами, за исключением наименования элементов управления, а в качестве свойства Text формы установите ListView. 5. Очистите свойство Text надписи. 6. Добавьте в форму два элемента управления ImageList, дважды щелкнув на пиктограмме этого элемента в панели инструментов — она находится в разделе Components (Компоненты). Переименуйте элементы управления на imageListSmall и imageListLarge. 7. Измените значение свойства Size (Размер) элемента управления ImageList с именем imageListLarge на 32, 32. 8. Щелкните на кнопке, расположенной справа от свойства Images списка изображений imageListLarge, чтобы открыть диалоговое окно, где можно будет перейти к изображениям, которые желательно вставить. 9. Щелкните на кнопке Add (Добавить) и перейдите к папке ListView в каталоге кода этой главы. Нужные файлы — Folder 32x32 . ico и Text 32x32 . ico. 10. Удостоверьтесь, что значок папки находится в верхней части списка. 11. Повторите шаги 8 и 9 для компонента imageListSmall, выбирая версии значков с размерами 16x16. 12. Установите значение свойства Checked переключателя radioButtonDetails равным true. 13. Установите свойства представления в виде списка, как указано в табл. 15.20.
Глава 15. Основы программирования для Windows 479 Таблица 15.20. Значения Свойство LargelmageList SmallImageList View свойств представления Значение imageListLarge imageListSmall Details Описание полученных результатов Перед первым из двух блоков f oreach мы вызываем метод BeginUpdate () применительно к элементу управления ListView. Помните, что метод BeginUpdate () элемента управления ListView указывает ему о необходимости прекратить обновление видимой области до момента вызова метода EndUpdate (). Отказ от вызова этого метода привел бы замедлению заполнения представления в виде списка и возможному мерцанию при добавлении элементов. Сразу после второго блока f oreach мы вызываем метод EndUpdate (), который вынуждает элемент управления ListView прорисовать элементы, которыми он был заполнен. Оба блока f о reach содержат код, который представляет интерес. Мы начинаем с создания нового экземпляра ListView It em, а затем устанавливаем свойство Text в соответствии с именем файла или папки, которую собираемся вставить. Свойство Imagelndex объекта ListViewItem ссылается на индекс элемента в одном из списков изображений. Поэтому важно, чтобы значки имели одинаковые индексы в обоих списках изображений. Для сохранения полностью определенного пути к папкам и файлам мы используем свойство Tag, которые будут использоваться, когда пользователь дважды щелкнет на элементе. Затем мы создаем два подэлемента. Им просто присваивается текст, предназначенный для отображения, а затем они добавляются в коллекцию Sub Items компонента ListViewItem. И, наконец, ListViewItem, добавляется в коллекцию Items элемента управления ListView. Элемент управления ListView достаточно "интеллектуален", чтобы просто игнорировать под элементы, если режим просмотра отличается от Details, поэтому мы добавляем подэлементы, независимо от текущего режима просмотра. Обратите внимание, что некоторые аспекты кода остались без внимания — а именно, строки, которые действительно получают информацию о файлах: // Получение информаци о корневой папке. Directorylnfo dir = new Directorylnfo(root); // Извлечение файлов и папок из корневой папки. Directorylnfo[] dirs = dir.GetDirectories (); // Папки Filelnfof] files = dir.GetFiles(); // Файлы Эти строки используют для доступа к файлам классы из пространства имен System. 10, поэтому в верхнюю часть кода нужно добавить следующий раздел using: using System; using System.Collections .Generic- using System.ComponentModel; using System.Data; using System.Drawing; using System.Windows.Forms; using System. 10; Подробнее доступ к файлам и пространство имен System. 10 описаны в главе 24.
480 Часть II. Программирование для Windows Но, чтобы вы могли получить представление о происходящем, отметим, что метод GetDirectories () объекта Directorylnfo возвращает коллекцию объектов, которые представляют папки в просматриваемом каталоге, а метод GetFiles () возвращает коллекцию объектов, представляющих файлы в текущем каталоге. В этих коллекциях можно выполнить циклический просмотр, как это только что было сделано, используя свойство Name объекта для возврата имени соответствующего каталога или файла и создания объекта ListViewItem для хранения этой строки. Чтобы элемент управления ListView отобразил корневую папку, остается только вызвать две функции в конструкторе формы. Одновременно инициализируем коллекцию folderColStringCollection корневой папкой: InitializeComponent(); // Инициализация элемента управления ListView и коллекции папок folderCol = new System.Collections.Specialized.StringCollection(); CreateHeadersAndFillListView(); PaintListView (@"C:\"); folderCol.Add(@"C:\") ; Чтобы пользователи имели возможность дважды щелкать на элементе в компоненте ListView для осуществления просмотра папок, необходимо подписаться на событие ItemActivate. Выберите ListView в окне конструктора и дважды щелкните на событии ItemActivate в списке Events окна Properties. Соответствующий обработчик события имеет следующий вид: private void listViewFilesAndFolders_ItemActivate(object sender, EventArgs e) { // Приводит объект-отправитель к типу ListView и извлекает // свойство Tag первого выбранного элемента. System.Windows.Forms.ListView lw = (System.Windows.Forms.ListView)sender; string filename = lw.Selectedltems[0].Tag.ToString(); if (lw.Selectedltems[0].Imagelndex != 0) { try { // Попытка выполнения файла. System.Diagnostics.Process.Start(filename); } catch { // Если попытка неудачна — просто выход из метода, return; } } else { // Вставка элементов. PaintListView(filename); folderCol.Add(filename); } } Свойство Tag выбранного элемента содержит полностью определенный путь к файлу или папке, где был выполнен двойной щелчок. Известно, что изображение с индексом 0 — папка, поэтому по этому индексу можно определить, является элемент файлом или папкой. Если он — файл, предпринимается попытка загрузки файла. Если он — папка, мы вызываем метод PaintListView () с новой папкой, а затем добавляем новую папку в коллекцию folderCol.
Глава 15. Основы программирования для Windows 481 Прежде чем переходить к переключателям, нужно завершить обеспечение возможностей просмотра, добавив событие Click в кнопку Back. Дважды щелкните на кнопке и поместите в обработчик события следующий код: private void buttonBack_Click(object sender, EventArgs e) { if (folderCol.Count > 1) { PaintListView(folderCol[folderCol.Count - 2].ToString ()); folderCol.RemoveAt(folderCol.Count -1); } else { PaintListView (folderCol [0] .ToStringO ) ; } } Если коллекция folderCol содержит более одного элемента, значит, мы находимся не в корневом каталоге браузера и нужно вызвать метод PaintListView () с путем к предыдущей папке. Последний элемент в коллекции folderCol — текущая папка. Поэтому мы должны извлечь предпоследний элемент. Затем мы удаляем последний элемент коллекции и делаем новый последний элемент текущей папкой. Если коллекция содержит только один элемент, мы просто вызываем метод PaintListView () с этим элементом. Теперь остается только обеспечить возможность изменения типа просмотра элемента управления ListView. Дважды щелкните на каждом переключателе и добавьте следующий код: private void radioButtonLargeIcon_CheckedChanged(object sender, EventArgs e) { RadioButton rdb = (RadioButton)sender; if (rdb.Checked) this.listViewFilesAndFolders.View = View.Largelcon; } private void radioButtonList_CheckedChanged(object sender, EventArgs e) { RadioButton rdb = (RadioButton)sender; if (rdb.Checked) this.listViewFilesAndFolders.View = View.List; } private void radioButtonSmallIcon_CheckedChanged(object sender, EventArgs e) { RadioButton rdb = (RadioButton)sender; if (rdb.Checked) this.listViewFilesAndFolders.View = View.Smalllcon; } private void radioButtonDetails_CheckedChanged(object sender, EventArgs e) { RadioButton rdb = (RadioButton)sender; if (rdb.Checked) this.listViewFilesAndFolders.View - View.Details; } private void radioButtonTile_CheckedChanged(object sender, EventArgs e) { RadioButton rdb = (RadioButton)sender; if (rdb.Checked) this.listViewFilesAndFolders.View = View.Tile; }
482 Часть II. Программирование для Windows Мы проверяем переключатель, чтобы выяснить, изменилось ли его состояние на Checked — если да, то мы устанавливаем свойство View элемента управления ListView соответствующим образом. На этом создание примера применения элемента управления ListView завершается. При его запуске результат должен быть подобным показанному на рис. 15.21. [ ■*■ LhtVUw J CSW1N00WS Нмм I JjTetkt I -JTenv J JWeb JjWrSxS в s E 1 £ 1 E £ t (■ Obg 000001_tmp 002926, Imp Bli*e16bnp booWatdat dockevi See 0 19274 19528 1272 ям 82944 Last accessed 064O 200515 47 07 064O 200515 3932 2506-2005161847 06-07 2005153619 0607-20051857:58 06-07-200515» 37 03-12-20021336 46 07 09-20041512 39 1806-2004185205 06-07-200515 37 00 03-12-2002 000000 [~buttonB«cfc | Ш V««Mode ledeeuttonUrgalcon (ediofluttonSmaicon 0 '«ftoButtorijti © i«faBu*)ri)et«b 0 ladoBUtanTle ■ Рис. 15.21. Работающее приложение ListView Добавление обработчиков событий Мы завершили создание интерфейса пользователя и теперь можем перейти к написанию кода. Вначале нам требуется поле для хранения папок, по которым выполняется перемещение, чтобы к ним можно было возвращаться по щелчку на кнопке Back. Мы будем сохранять абсолютные пути папок, поэтому выберите для их хранения коллекцию StringCollection: partial class Forml : Form { // Поле-член для хранения ранее просматривавшихся папок private System.Collections.Specialized.StringCollection folderCol; Мы не создали ни одного заголовка столбцов в конструкторе форм, поэтому их нужно создать теперь с помощью метода CreateHeadersAndFillListView (): private void CreateHeadersAndFillListView() { ColumnHeader colHead; // Первый заголовок colHead = new ColumnHeader(); colHead.Text = "Filename"; this.listViewFilesAndFolders.Columns.Add(colHead) ; // Второй заголовок colHead = new ColumnHeader(); colHead.Text = "Size"; this.listViewFilesAndFolders.Columns.Add(colHead) ; // Третий заголовок colHead = new ColumnHeader(); colHead.Text = "Last accessed"; this.listViewFilesAndFolders.Columns.Add(colHead) ; // Вставка заголовка // Вставка заголовка // Вставка заголовка } Мы начинаем с объявления единственной переменной colHead, используемой для создания трех заголовков столбцов. Для каждого из трех заголовков мы объявляем переменную заново и присваиваем ей Text, прежде чем добавить ее в коллекцию Columns элемента управления ListView.
Глава 15. Основы программирования для Windows 483 Заключительное действие по инициализации формы, поскольку она отображается впервые, сводится к заполнению представления в виде списка файлами и папками, загруженными с жесткого диска. Для этого служит другой метод: private void PaintListView(string root) try // Две локальные переменные, используемые для создания элементов для вставки. ListViewItem lvi; ListViewItem.ListViewSubltem lvsi; // Если корневая папка не существует, вставка чего-либо невозможна, if (root.CompareTo("") == 0) return; // Получение информации о корневой папке. Directorylnfo dir = new Directorylnfo (root); // Извлечение файлов и папок из корневой папки. Directorylnfo[] dirs = dir.GetDirectories (); // Папки FileInfo[] files = dir.GetFiles (); // Файлы // Очистка элемента ListView. Обратите внимание, // что мы вызываем метод Clear применительно к // коллекции Items, а не к самому объекту ListView. // Метод Clear объекта ListView удаляет все, включая заголовки // столбцов, а нам нужно удалить только элементы из представления. this.listViewFilesAndFolders.Items.Clear(); // Установка содержимого надписи в соответствии с текущим путем, this.labelCurrentPath.Text = root; // Блокирование объекта ListView от обновлений, this.listViewFilesAndFolders.BeginUpdate(); // Циклический просмотр все папок в корневой папке и их вставка. foreach (Directorylnfo di in dirs) { // Создание главного элемента ListViewItem. lvi = new ListViewItem () ; lvi.Text = di.Name; // Имя папки lvi.Imagelndex =0; // Значок папки имеет индекс, равный 0 lvi.Tag = di.FullName; // Установка свойства tag в соответствии //с полным путем папки // Создание двух подэлементов ListViewSubltems. lvsi = new ListViewItem.ListViewSubltem(); lvsi.Text = ""; // Размер - папка не имеет размера, // поэтому данный столбец пуст lvi.Subltems.Add(lvsi) ; // Добавление подэлемента в элемент ListViewItem lvsi = new ListViewItem.ListViewSubltem(); lvsi.Text = di.LastAccessTime.ToStringO; // Последний посещенный столбец lvi.Subltems.Add(lvsi); // Добавление подэлемента в элемент ListViewItem // Добавление ListViewItem в коллекцию Items элемента управления ListView. this.listViewFilesAndFolders.Items.Add(lvi); } // Циклический просмотр всех файлов в корневой папке, foreach (Filelnfo fi in files) { // Создание главного элемента ListViewItem. lvi = new ListViewItem () ; lvi.Text = fi.Name; // Имя файла
484 Часть II. Программирование для Windows lvi.Imagelndex =1; // Значок, используемый для представления // папки, имеет индекс, равный 1 lvi.Tag = fi.FullName; // Установка свойства tag в соответствии //с полным путем файла // Создание двух подэлементов. lvsi = new ListViewItem.ListViewSubltem(); lvsi.Text = fi.Length.ToString(); // Длина файла lvi.Sublterns.Add(lvsi); // Добавление в коллекцию Subltems lvsi = new ListViewItem.ListViewSubltem(); lvsi.Text = fl.LastAccessTime.ToString();// Последний посещенный столбец lvi .Subltems .Add (lvsi) ; // Добавление в коллекцию Subltems // Добавление элемента в коллекцию Items элемента управления ListView. this.listViewFilesAndFolders.Items.Add(lvi); // Разблокирование элемента управления ListView. // Теперь вставленные элементы будут отображаться, this.listViewFilesAndFolders.EndUpdate(); catch (System.Exception err) { MessageBox.Show("Error: " + err.Message); Элемент управления TabControl Элемент управления TabControl предоставляет простой способ организации диалоговых окон в логические части, доступные посредством вкладок в верхней части элемента управления. TabControl содержит элементы TabPages, которые по существу работают подобно элементу управления GroupBox, поскольку они группируют элементы управления, хотя и являются несколько более сложными. На рис. 15.22 показано диалоговое окно Tools'^ Options (Сервис=>Параметры) приложения Word 2003. Обратите внимание на три ряда вкладок в верхней части диалогового окна. Щелчок на каждой из них будет вести к отображению иного набора элементов управления в остальной части диалогового окна. Это очень наглядный пример применения элемента управления TabControl для группирования связанной информации, что облегчает пользователям поиск нужной информации. Использование элемента управления TabControl не представляет сложности. Вы просто добавляете нужное количество вкладок для отображения коллекции объектов TabPage элемента управления, а затем перетаскиваете элементы управления, которые требуется отображать, на соответствующие страницы. ^^^^^LMHeHI Security | User Inf опмЬоп rvjStafr» Та* Panel P Status bar W ScreeQTIps Г lab character» Г Spaces Г Paragraph marks Pre* and Web Layout opt G Orawtop? Г Object anchors Г Test boundaries Outtne and Normal opctor Г W/apcowndow rfrrftfont: Seeing & Gramma, Compahbety - 1 m \ W Smart tags P Arjmetedtext W HortfontalKrolbar W Vertical scrol bar Г picture placeholders 1 t^bben text 1 Optional h^pnons РЩ 1 Background colors ar W Vertical ruler (Print v m Sryte area «tdth f 1 [ | Track Changes Fes Locations Pnht Save W Windows In Taskfear Г peW codes Hetd shadna; |Never ^J id knages (Prrt view crty) «wanly) * ±L J 1 _j OK 1 Cancel | Рис. 15.22. Диалоговое окно Tools^> Options в Word 2003
Глава 15. Основы программирования для Windows 485 Свойства элемента управления TabControl Свойства объекта TabControl (описанные в табл. 15.21) в основном используются для управления внешним видом контейнера объектов Tab Page — в частности, отображаемых вкладок. Таблица 15.21. Часто используемые свойства класса TabControl Имя Описание Alignment Appearance HotTrack Multiline RowCount Selectedlndex SelectedTab TabCount TabPages Это свойство управляет местом отображения вкладок элемента управления TabControl. По умолчанию они отображаются в верхней части элемента управления Свойство Appearance управляет способом отображения вкладок. Вкладки могут отображаться как обычные кнопки или плоскими Если значение этого свойства установлено в true, внешний вид вкладок элемента управления изменяется при прохождении над ними указателя мыши Если значение этого свойства установлено в true, возможно наличие нескольких рядов вкладок RowCount возвращает количество рядов отображенных в текущий момент вкладок Это свойство возвращает или устанавливает индекс выбранной вкладки SelectedTab возвращает или устанавливает выбранную вкладку. Обратите внимание, что это свойство работает с фактическими экземплярами объектов TabPages TabCount возвращает общее количество вкладок Это свойство — коллекция объектов TabPage в элементе управления. Эту коллекцию используют для добавления и удаления объектов TabPage Работа с элементом управления TabControl TabControl работает несколько иначе, чем остальные элементы управления, рассмотренные до сих пор. Этот элемент управления представляет собой немногим большее, чем контейнер для страниц вкладок, служащий для отображения страниц. При двойном щелчке на TabControl в панели инструментов создается элемент управления, который уже содержит два элемента TabPages, как показано на рис. 15.23. Когда мышь перемещается поверх элемента управления, в верхнем правом углу элемента появляется маленькая кнопка с изображением треугольника. Щелчок на этой кнопке ведет к разворачиванию небольшого окна. Это окно Actions Window (Окно действий) позволяет легко получать доступ к выбранным свойствам и методам элемента управления. Вы могли обратить на него внимание раньше, поскольку многие элементы управления в Visual Studio обладают этой функциональной возможностью, но TabControl — первый из элементов управления из числа описанных в этой главе, который действительно позволяет выполнить нечто интересное в окне Actions Window. Упомянутое окно элемента управления TabControl позволяет легко добавлять и удалять элементы TabPages во время разработки. Рис. 15.23. Первоначальный вид элемента управления TabControl
486 Часть II. Программирование для Windows Процедура добавления вкладок к элементу управления TabControl, описанная в предыдущем абзаце, призвана обеспечить быстрое создание и начало работы с элементом управления. Однако если требуется изменить поведение или стиль вкладок, следует использовать диалоговое окно TabPages, доступное посредством кнопки при выборе свойтва TabPages в окне Properties. Свойство TabPages является также коллекцией, которая используется для доступа к отдельным страницам в элементе управления TabControl. Как только нужные элементы TabPages добавлены, элементы управления можно добавлять к страницам так же, как это делалось раньше при работе с элементом управления GroupBox. В следующем практическом занятии демонстрируются основы работы с этим элементом управления. практическое занятие Работа с элементами управления TabPages Выполните следующие действия, чтобы создать Windows-приложение, которое демонстрирует разработку элементов управления, расположенных на различных страницах элемента управления TabControl. 1. Создайте новое Windows-приложение TabControl в каталоге С: \BegVCSharp\ Chapterl5. 2. Перетащите элемент управления TabControl из панели инструментов Toolbox на форму. Как и GroupBox, элемент TabControl находится на вкладке Containers (Контейнеры) панели Toolbox. 3. Найдите свойство TabPages и после его выбора щелкните на расположенной справа от него кнопке, чтобы открыть диалоговое окно, показанное на рис. 15.24. 4. Измените свойство Text страниц вкладок соответственно на Tab one и Tab two и щелкните на кнопке О К, чтобы закрыть диалоговое окно. TabPage Collection Edhoi i;y ftdd &етх tabPagel p/operties: Acces«b*eRote Default BackCotor Q] Transparent Backqroundlrnac | | (none) Backgrounding Tie BorderStyie None Cursor Default Ш Font Wcrosoft Sans Sari ForeCoky Щ ControTText RjghtToLeft No Text tabPagel UseVtsuaiStytoe* True UseWaitCursor False Я ben*™» AlowDrop False ContextMenuStr (none) ImeMode NoControi В ffl (AppkationSetti Ш (Dataftndngs) v lj*: Cancel 1 Рис. 15.24. Редактор коллекции вкладки
Глава 15. Основы программирования для Windows 487 5. Страницы вкладок для работы можно выбирать, щелкая на вкладках в верхней части элемента управления. Выберите вкладку Tab one. Перетащите кнопку на элемент управления. Убедитесь, что кнопка помещена внутри рамки элемента управления TabControl. В случае ее перетаскивания снаружи этой рамки кнопка будет помещена в форму, а не в элемент управления. 6. Измените имя кнопки на buttonShowMessage, а ее свойство Text на Show Message (Показать сообщение). 7. Щелкните на вкладке, свойство которой Text равно Tab two. Перетащите элемент управления TextBox на поверхность TabControl. Назовите этот элемент управления textBoxMessage и очистите его свойство Text. 8. Две созданных вкладки должны выглядеть, как показано на рис. 15.25 и 15.26. Tab one Tab two . | Show Message | „,„.„„„ „ ,, i Рис. 15.25. Вкладка Tab one ! •* Forml ■■•■ Tab one Tab two ! I Рис. 15.26. Вкладка Tab two Описание полученных результатов Доступ к элементу управления на вкладке осуществляется так же, как к любому другому элементу управления в форме. Приложение извлекает свойство Text элемента управления TextBox и отображает его в окне сообщения. Ранее в этой главе было показано, что на форме одновременно может быть выбран только один из переключателей (если только они не помещены в групповую рамку). Элементы управления TabPage работают точно так же, как групповые рамки, поэтому можно использовать несколько наборов переключателей на различных вкладках, обходясь при этом без групповых рамок. Кроме того, как было показано при рассмотрении метода buttonShowMessageClick, можно обращаться к элементам управления, расположенным на вкладках, отличных от той, где находится текущий элемент управления. Последнее, что нужно знать для успешной работы с элементом управления TabControl — способ выяснения того, какая вкладка отображается в данный момент. Для этого можно использовать два свойства: SelectedTab и Selectedlndex. Как видно из имени, SelectedTab (выбранная вкладка) возвращает объект TabPage или значение null, если ни одной вкладки не выбрано, a Selectedlndex (выбранный индекс) возвращает индекс выбранной вкладки или -1, если ни одной вкладки не выбрано.
488 Часть II. Программирование для Windows Для экспериментирования с этими свойствами обратитесь к упражнению 2 в конце этой главы. Добавление обработчика события Теперь можно обратиться к элементам управления. Если запустить код в его нынешнем состоянии, страницы вкладок отобразятся надлежащим образом. Для демонстрации всех возможностей элемента управления TabControl остается только добавить небольшой фрагмент кода, чтобы щелчок на кнопке Show Message (Показать сообщение) на одной вкладке приводил к отображению в окне сообщения текста, введенного на другой вкладке. Добавьте обработчик события Click, дважды щелкнув на кнопке на первой вкладке и добавив следующий код: private void buttonShowMessage_Click(object sender, EventArgs e) { // Обращение к элементу управления TextBox. MessageBox.Show(this.textBoxMessage.Text); } Резюме В этой главе мы рассмотрели некоторые из элементов управления, наиболее часто используемых для создания Windows-приложений, и выяснили, как их можно использовать для создания простых, но предлагающих большие возможности интерфейсов пользователя. В главе были освещены свойства и события этих элементов управления, были приведены примеры, демонстрирующие их использование, и пояснено добавление обработчиков событий для конкретных событий элемента управления. Далее перечислены ключевые моменты, с которыми вы ознакомились в этой главе. □ Использование элементов управления Label и LinkLabel для отображения информации пользователям. □ Использование элемента управления Button и соответствующего события Click для предоставления пользователям возможности указывать приложению о необходимости выполнения того или иного действия. □ Использование элементов управления TextBox и RichTextBox для предоставления пользователям возможности ввода простого либо форматированного текста. □ Различие между элементами управления CheckBox и RadioButton и способы их применения. Вы научились также группировать эти элементы управления с помощью элемента управления GroupBox и узнали, как это влияет на их поведение. □ Использование элемента управления CheckedListBox для предоставления списков, из которых пользователь может выбирать элементы, устанавливая флажки. Вы узнали также, как применять более распространенный элемент управления ListBox для предоставления списка, который аналогичен списку элемента CheckedListBox, но не содержит флажки. □ Использование элементов управления ListView и ImageList для предоставления списка, которые пользователи могут просматривать множеством различных способов. □ Использование элемента управления TabControl для группирования элементов управления на различных страницах одной формы, чтобы пользователь мог их выбирать по собственному желанию.
Глава 15. Основы программирования для Windows 489 В следующей главе мы рассмотрим некоторые более сложные элементы управления и функциональные возможности создания приложений Windows Forms. Упражнения 1. В предшествующих версиях Visual Studio было достаточно трудно заставить свои приложения отображать их элементы управления в стиле текущей версии Windows. Для выполнения этого упражнения выясните, где именно в приложении Windows Forms осуществляется активизация визуальных стилей в новом проекте Windows Forms. Поэкспериментируйте с активизацией и отключением стилей и выясните, как они влияют на элементы управления в формах. 2. Измените пример создания элемента управления TabControl, добавив в него несколько страниц вкладок и отобразив окно сообщения со следующим текстом: Вы изменили текущую вкладку с <Текст только что оставленной вкладки> на <Текст текущей вкладки>. 3. В примере применения ListView для сохранения полностью определенного пути к папкам и файлам в элементе управления ListView использовалось свойство tag. Измените это поведение, создав новый класс, производный от ListViewItem, и используйте экземпляры этого нового класса в качестве элементов в элементе управления ListView. Обеспечьте хранение информации о файлах и папках в новом классе с помощью свойства FullyQualif iedPath.
16 Расширенные функциональные возможности Windows Forms В предыдущей главе мы рассмотрели некоторые из элементов управления, наиболее часто используемых при разработке Windows-приложений. Применение таких элементов управления позволяет создавать впечатляющие диалоговые окна, но очень мало полномасштабных Windows-приложений обладает интерфейсом пользователя, который состоит из единственного диалогового окна. Чаще эти приложения применяют однодокументный (Single Document Interface — SDI) или многодокументный (Multiple Document Interface — MDI) интерфейс. Обычно приложения любого из этих типов интенсивно используют меню и панели инструментов, которые еще не рассматривались в предыдущей главе. Но теперь стоит исправить это упущение. Добавление набора инструментов Windows Presentation Foundation в каркас .NETFramework привело к появлению нескольких новых типов Windows-приложений. Они подробно рассматриваются в главе 34. Эта глава начинается с того, на чем мы остановились при рассмотрении элементов управления в предыдущей главе. Вначале мы рассмотрим элемент управления меню, а затем перейдем к панелям инструментов и научимся связывать кнопки панелей инструментов с элементами меню и наоборот. Затем мы приступим к созданию приложений SDI и MDI, уделив основное внимание приложениям MDI, поскольку по существу приложения SDI представляют собой поднаборы приложений MDI. До сих пор мы применяли только те элементы управления, которые поставляются с каркасом .NET Framework. Как вы убедились, эти элементы управления предоставляют широкое множество функциональных возможностей, но в ряде случаев их оказывается недостаточно. В этих случаях можно создавать нестандартные элементы управления, и ближе к концу этой главы будет показано, как это делается.
Глава 16. Расширенные функциональные возможности Windows Forms 491 В этой главе рассматриваются следующие вопросы. □ Использование трех обычных элементов управления для создания изысканных меню, панелей инструментов и строк состояния. □ Создание приложений MDI. □ Создание собственных элементов управления. Меню и панели инструментов Сколько вы можете вспомнить Windows-приложений, которые не содержат меню или панели инструментов того или иного вида? Ни одного, не так ли? Скорее всего, меню и панели инструментов будут важными составными частями любого создаваемого Windows-приложения. Для облегчения их создания для пользовательских приложений Visual Studio 2008 предлагает два элемента управления, которые позволяют достаточно легко создавать меню и панели инструментов, выглядящие подобно меню из Visual Studio. Два как один Два элемента управления, которые мы рассмотрим на последующих страницах, впервые появились в Visual Studio 2005 и представляют значительное расширение возможностей как для разработчика-любителя, так и для профессионала. Построение приложений с профессионально выглядящими панелями инструментов и меню обычно оставалось уделом тех, кто было готов потратить значительное время для создания нестандартных инструментов рисования, и тех, кто приобретал компоненты у независимых разработчиков. Создание того, что раньше могло занять недели, теперь превратилось в простую задачу, которая может быть выполнена буквально за несколько секунд. Элементы управления, которые мы будем использовать, могут быть сгруппированы в семейство элементов, имена которых заканчиваются суффиксом Strip. Эти элементы управления — ToolStrip, MenuStrip и StatusStrip. К элементу управления StatusStrip мы вернемся в этой главе позднее. Если их рассматривать в чистом виде, то ToolStrip и MenuStrip фактически представляют один и тот же элемент управления, поскольку MenuStrip ведет свое происхождение непосредственно от элемента управления ToolStrip. Это означает, что MenuStrip может выполнять все, что может ToolStrip. Понятно, это означает также, что два эти элемента управления действительно хорошо сочетаются друг с другом. Использование элемента управления MenuStrip Кроме MenuStrip еще несколько элементов управления присутствуют в меню. Три из них — ToolStripMenuItem, ToolStripDropDown и ToolStripSeparator — используются наиболее часто. Все эти три элемента управления представляют конкретный способ просмотра элемента в меню или панели инструментов. ToolStripMenuItem представляет одиночную запись в меню, ToolStripDropDown — элемент, который при щелчке на нем отображает список других элементов, a ToolStripSeparator — горизонтальную или вертикальную разделительную линию в меню или панели инструментов. Существует также еще один вид меню, который будет кратко рассмотрен после MenuStrip — ContextMenuStrip. Он представляет контекстное меню, открываемое при щелчке на элементе правой кнопкой мыши и, как правило, отображающее информацию, связанную с этим элементом.
492 Часть II. Программирование для Windows Теперь, без дальнейших рассуждений, приступим к следующему практическому занятию и создадим первый пример этой главы. Профессиональные меню за пять секунд Первый пример в значительной степени является демонстрационным и призван просто ознакомить с особенностями элементов управления, которые действительно прекрасно подходят для создания стандартных меню, выглядящих и ведущих себя нужным образом. 1. Создайте новое Windows-приложение Professional Menus в каталоге С:\ BegVCSharp\Chapter16. 2. Перетащите экземпляр элемента управления MenuStrip из панели инструментов на поверхность проектирования. 3. Щелкните на треугольнике у правого края MenuStrip в верхней части диалогового окна, чтобы открыть окно действий. 4. Щелкните на маленьком треугольнике в верхнем правом углу меню, а затем на ссылке Insert Standard Items (Вставить стандартные элементы). Вот и все. Если теперь развернуть меню File (Файл), оно будет выглядеть, как показано на рис. 16.1. Пока меню не подкреплено никакими функциональными возможностями — их предстоит добавить. Меню можно изменять любым нужным образом. Это описано в последующих разделах. Создание меню вручную D New 0_pen Save Save As CtiUN Ctrl*0 CtrkS Pi hit Ctrl*P Print Preview Exit Рис. 16.1. Меню File При перетаскивании элемента управления MenuStrip из панели инструментов на поверхность проектирования он помещается как в саму форму, так и в лоток управления. Но инструмент управления можно редактировать непосредственно на форме. Чтобы создать новые элементы меню, достаточно поместить указатель в поле, помеченное надписью Туре Неге (Введите текст здесь), как показано на рис. 16.2. ft menuStripl Рис. 16.2. Создание нового элемента меню При вводе заголовка меню в выделенном поле перед буквой, которая должна служить клавишей быстрого доступа к данному элементу меню — этот символ будет отображаться подчеркнутым, а для выбора пункта нужно будет нажать сочетание <Alt> и клавиши указанного символа — можно вставить знак амперсанда (&).
Глава 16. Расширенные функциональные возможности Windows Forms 493 Обратите внимание, что в одном меню можно создать несколько элементов меню с одним и тем символом быстрого доступа. Данный символ может использоваться для этой цели только единожды в каждом раскрывающемся меню (например, один раз в меню File, один раз в меню View и т.д.). Если один и тот же символ быстрого доступа случайно присвоить нескольким элементам одного и того же раскрывающегося меню, только ближайший к верхней части элемента управления элемент меню будет реагировать на нажатие данного символа. При выборе элемента меню элемент управления автоматически отображает соответствующие элементы ниже и справа от текущего элемента. Ввод заголовка в любом из этих элементов ведет к созданию нового элемента, связанного с начальным. Именно так создаются раскрывающиеся меню. Для создания горизонтальных линий, которые делят меню на группы, вместо ToolStripMenuItem необходимо использовать элемент управления ToolStripSeparator, но в действительности другой элемент управления вставлять не нужно. Вместо этого достаточно ввести символ - в качестве единственного символа заголовка элемента меню и Visual Studio автоматически интерпретирует его как разделитель и изменит тип элемента управления. В следующем практическом занятии мы создадим меню, не прибегая к помощи Visual Studio для генерирования его элементов. Создание меню с нуля В этом примере мы собираемся создать меню File (Файл) и Help (Справка) с нуля. Меню Edit (Правка) и Tools (Сервис) предлагаем вам создать самостоятельно. 1. Создайте новый проект приложения для Windows, назовите его Manual Menus и сохраните его в папке С: \BegVCSharp\Chapterl6. 2. Перетащите элемент управления MenuStrip из панели инструментов на поверхность проектирования. 3. Щелкните в текстовом поле Туре Неге элемента управления MenuStrip, введите &File и нажмите клавишу <Enter>. 4. В текстовых полях под элементом File введите следующий текст: &New &Ореп &Save Save &As &Print Print Preview E&xit Теперь меню должно выглядеть, как показано на рис. 16.3. Обратите внимание на то, как Visual Studio автоматически преобразует символы дефисов в линию, разделяющую элементы меню. 5. Щелкните на текстовой области справа от Files и введите &Не1р.
494 Часть II. Программирование для Windows 6. В текстовых полях под элементом Help введите следующий текст: Contents Index Search About Теперь меню должно выглядеть, как показано на рис. 16.4. Fonn1 ^аО |ш iLJ ED , Mew 1 4p*n 1 Sav» A* Eiint Print Preview Exit J iy,.. Hen ■r Foiml Eile ШёйП Content* Index Seaich jsm About Рис. 16.3. Добавление элементов меню File Рис. 16.4. Добавление элементов меню Help 7. Вернитесь к меню File и определите клавиши быстрого доступа для его элементов. Для этого выберите элемент, который нужно определить, и найдите свойство ShortcutKeys в панели свойств. Щелчок на стрелке развертывания открывает маленькое окно, в котором можно определить клавиатурную комбинацию, связанную с данным элементом меню. Поскольку данное меню является стандартным, следует использовать стандартные сочетания клавиш, но при создании дополнительных элементов можно выбирать любые другие клавиатурные комбинации. Установите свойства ShortcutKeys в меню File, как указано в табл. 16.1. Таблица 16.1. Значения свойств ShortcutKeys Имя элемента Свойства и значения &New &Ореп &Save &Print Ctrl + N Ctrl + О Ctrl + S Ctrl + P 8. Теперь в качестве завершающего штриха определите изображения. В меню File выберите пункт New (Создать) и щелкните на многоточии слева от свойства Image в панели Properties, чтобы открыть диалоговое окно Select Resource (Выберите ресурс). Несомненно, наиболее трудная часть создания этих меню — получение изображений, которые нужно отображать. В данном случае изображения можно получить, загружая исходный код этой книги с сайта www. wrox. com, но, как правило, их придется рисовать самостоятельно или получать каким-либо способом.
Глава 16. Расширенные функциональные возможности Windows Forms 495 9. Поскольку в настоящее время проект не содержит никаких ресурсов, список Entry (Запись) пуст. Поэтому щелкните на кнопке Import (Импорт). Изображения для этого примера можно найти в исходном коде этой книги в каталоге Chapter 16\ Manual Menus\Images. Выберите все хранящиеся там файлы и щелкните на кнопке Open. В данный момент мы выполняем редактирование элемента New, поэтому в списке Entry выберите запись New image (Новое изображение) и щелкните на кнопке ОК. 10. Повторите шаг 9 для изображений кнопок Open (Открыть), Save (Сохранить), Save As (Сохранить как), Print (Печать) и Print Preview (Предварительный просмотр печати). 11. Запустите проект. Меню File можно выбирать щелчком на нем либо нажимая комбинацию клавиш <Alt+F>, а доступ к меню Help осуществляется с помощью клавиатурной комбинации <Alt+H>. Дополнительные свойства элемента управления ToolStripMenuItem Создавая меню, следует знать о нескольких дополнительных свойствах элемента управления ToolStripMenuItem. Приведенная ниже табл. 16.2 не является исчерпывающей. Если хотите ознакомиться со всеми свойствами этого класса, обратитесь к документации по .NET Framework SDK. Таблица 16.2. Часто используемые свойства класса ToolStripMenuItem Свойство Описание Checked Указывает, выбрано ли меню Checkonciick Когда значение этого свойства — true, метка флажка либо добавляется, либо удаляется из позиции, расположенной слева от текста в элементе, которую в противном случае занимает изображение. Для определения состояния элемента меню следует использовать свойство checked Enabled Элемент, значение свойства Enabled которого установлено равным false, будет затемнен и не может быть выбран DropDownitems Возвращает коллекцию элементов, используемую в качестве раскрывающегося меню, связанного с данным элементом меню Добавление функциональных возможностей меню Теперь вы можете создавать меню, которые выглядят столь же привлекательно, как и присутствующие в Visual Studio. Так что остается только обеспечить, чтобы щелчок на них выполнял какое-то полезное действие. Понятно, что связанные со щелчком действия полностью зависят от программиста, но в следующем практическом занятии мы создадим очень простое приложение на основе предыдущего примера. Чтобы приложение реагировало на выбор, выполненный пользователем, необходимо реализовать обработчики для одного из двух событий, отправляемых элементом управления ToolStripMenuItems (табл. 16.3).
496 Часть II. Программирование для Windows Таблица 16.3. События класса ToolStripMenultems Событие Описание Click Отправляется каждый раз, когда пользователь щелкает на элементе. В большинстве случаев это событие, на которое требуется реакция checkedchanged Отправляется при щелчке на элементе, обладающем свойством CheckOnClick Нам предстоит расширить предыдущий практический пример, добавив в диалоговое окно текстовое поле и реализовав несколько обработчиков событий. Мы добавим также еще одно меню — Format (Формат) — между меню Files и Help. В каталоге с загружаемым кодом этот проект назван Extended Manual Menus. практическое занятие обработка событий меню 1. Перетащите элемент управления RichTextBox на поверхность проектирования и измените его имя на richTextBoxText. Установите Fill в качестве значения его свойства Dock. 2. Выберите MenuStrip, а затем в текстовой области введите Format после элемента меню Help и нажмите клавишу <Enter>. 3. Выберите элемент меню Format и перетащите его в позицию между Files и Help. 4. Добавьте в меню Format элемент меню с текстом Show Help Menu (Показывать меню Справка). 5. Установите значение свойства CheckOnClick элемента меню Show Help Menu равным true. Установите значение его свойства Checked равным true. 6. Измените имена пяти элементов меню, как показано в табл. 16.4. Таблица 16.4. Имена элементов меню Элемент меню Имя New MenuItemNew Open MenuItemOpen Save MenuItemSave Show Help Menu MenuItemShowHelpMenu Help MenuItemHelp 7. Выберите элемент меню MenuItemShowHelpMenu и добавьте обработчик события CheckedChanged, дважды щелкнув на событии в разделе Events панели Properties. 8. Добавьте в обработчик события следующий код: private void MenuItemShowHelpMenu_CheckedChanged(object sender, EventArgs e) { ToolStripMenuItem item = (ToolStripMenuItem)sender; MenuItemHelp.Visible = item.Checked;
Глава 16. Расширенные функциональные возможности Windows Forms 497 9. Дважды щелкните на элементах меню MenuItemNew, MenuItemSave и MenuItemOpen. Двойной щелчок на элементе ToolStripMenuItem в представлении конструктора ведет к добавлению события Click. Введите следующий код: private void MenuItemNew_Click(object sender, EventArgs e) { richTextBoxText.Text = ""; } private void MenuItemOpen_Click (object sender, EventArgs e) { try { nchTextBoxText.LoadFile(@".\Example.rtf"); } catch { // Игнорирование ошибок } } private void MenuItemSave_Click(object sender, EventArgs e) { try { richTextBoxText.SaveFile(@M.\Example.rtf"); } catch { // Игнорирование ошибок } } 10. Запустите приложение. Щелчок на элементе меню Show Help Menu, будет вызывать скрытие или отображение меню Help в зависимости от состояния свойства Checked, и вы должны иметь возможность открывать, сохранять и очищать текст в текстовом поле. Описание полученных результатов Вначале выполняется обработка события MenuItemShowHelpMenuCheckedChanged. Обработчик этого события устанавливает значение свойства Visible элемента управления MenuItemHelp равным true, если значение свойства Checked — true. В противном случае это значение должно быть равным false. В результате этот элемент управления работает в качестве переключателя видимости меню Help. И, наконец, три обработчика события Click соответственно очищают текст в элементе RichTextBox, сохраняют текст этого элемента в заранее определенном файле и открывают названный файл. Обратите внимание, что события Click и CheckedChanged идентичны в том, что они оба обрабатывают события, которые происходят при щелчке на элементе меню, но поведение конкретных элементов меню существенно различается и должно обрабатываться в зависимости от назначения элемента. Панели инструментов В то время как меню прекрасно подходят для предоставления доступа ко всему множеству функциональных возможностей приложения, некоторые элементы целесообразно помещать не только в меню, но и в панель инструментов.
498 Часть II. Программирование для Windows Панель инструментов позволяет посредством одного щелчка получать доступ к таким часто используемым функциям, как Open (Открыть), Save (Сохранить) и т.п. Набор панелей инструментов Word 2003 показан на рис. 16.5. Ей! Шш ЩВШ 4A./ja,nrwi>HiaiaiWfff|ю% **- —| ML"™* .^Ba^vieBERe^ д 9 ^Д Ж | ДД j ДДдЦр pr|f j [ j| jg МУЛ Puc. 7d 5. Панели инструментов Word 2003 Обычно кнопка в панели инструментов отображается в виде рисунка без какого- либо текста, хотя могут существовать кнопки, для которых отображается и то, и другое. Примерами панелей инструментов без текста могут служить панели инструментов Word (см. рис. 16.5), а примеры панелей инструментов, включающие и текст, можно найти в Internet Explorer. Изредка могут встречаться панели инструментов, которые кроме кнопок содержат также поля со списками и текстовые поля. Часто при помещении указателя мыши над кнопкой в панели инструментов, особенно, когда панель содержит только пиктограммы, отображается подсказка с информацией о назначении данной кнопки. Элемент управления ToolStrip, как и MenuStrip, был создан так, чтобы иметь профессиональный вид. Когда пользователи видят панель инструментов, они ожидают, что ее можно перемещать в любую удобную позицию. ToolStrip дает возможность пользователям делать это — если вы ее разрешите. При первоначально помещении элемента управления ToolStrip на поверхность проектирования формы он выглядит подобно рассмотренному ранее элементу управления MenuStrip, за исключением двух различий: в крайней левой позиции элемента управления расположены четыре вертикальные точки, подобные присутствующим в меню Visual Studio. Эти точки показывают, что панель инструментов можно перемещать по окну родительского приложения и пристыковывать к его краям. Второе различие то, что по умолчанию панель инструментов отображает пиктограммы, а не текст, поэтому по умолчанию все ее элементы являются кнопками. Панель инструментов отображает раскрывающиеся меню, которое позволяет выбирать тип элемента. Одна из особенностей, полностью подобная MenuStrip, состоит в том, что окно Actions содержит ссылку Insert Standard Items (Вставить стандартные элементы). Щелчок на ней позволяет вставить не совсем те же элементы, которые можно было вставить с помощью MenuStrip, но мы получаем в свое распоряжение кнопки New (Создать), Open (Открыть), Save (Сохранить), Print (Печать), Cut (Вырезать), Сору (Копировать), Paste (Вставить) и Help (Справка). Вместо того чтобы полностью выполнять практическое упражнение, как было сделано ранее, рассмотрим некоторые свойства самого элемента управления ToolStrip и используемых в нем элементов управления. Свойства элемента управления ToolStrip Свойства элемента управления ToolStrip управляют способом и местом его отображения. Помните, что в действительности этот элемент управления является базовым для рассмотренного ранее элемента управления MenuStrip, поэтому они обладают многими общими свойствами. Как и ранее, табл. 16.5 содержит лишь некоторые свойства ToolStrip, представляющие особый интерес — чтобы ознакомиться с полным перечнем, обратитесь к документации .NET Framework SDK.
Глава 16. Расширенные функциональные возможности Windows Forms 499 Таблица 16.5. Часто используемые свойства класса Tooistrip Свойство Описание GripStyle LayoutStyle Items ShowItemToolTip Stretch Управляет тем, должны ли четыре вертикальные точки отображаться в крайней левой позиции панели инструментов. Сокрытие этой рукоятки-манипулятора ведет к тому, что пользователи утрачивают возможность перемещения панели инструментов Управляет способом отображения элементов в панели инструментов. По умолчанию они отображаются горизонтально Содержит коллекцию всех элементов панели инструментов Определяет, нужно ли отображать подсказки для элементов панели инструментов По умолчанию панель инструментов лишь немного шире или выше содержащихся в ней элементов. Если значение свойства stretch установлено равным true, панель инструментов будет заполнять всю длину своего контейнера Элементы элемента управления Tooistrip В Tooistrip можно использовать множество элементов управления. Ранее я уже отметил, что панель инструментов должна иметь возможность содержать кнопки, поля со списками и текстовые поля. Как легко догадаться, существуют элементы управления для каждого из этих элементов, однако имеется также несколько других элементов, описанных в табл. 16.6. Таблица 16.6. Элементы управления, используемые в Tooistrip Элемент управления Описание ToolStripButton ToolStripLabel ToolStripSplitButton ToolStripDropDownButton ToolStripComboBox ToolStripProgressBar ToolStripTextBox ToolStripSeparator Представляет кнопку. Это свойство можно использовать для кнопок с текстом или без него Представляет надпись. Этот элемент управления может также отображать изображения — т.е. его можно использовать для отображения статического изображения перед другим, не отображающим информацию о себе, элементом управления, таким как текстовое поле или поле со списком Отображает кнопку с расположенной слева от нее кнопкой развертывания, щелчок на которой вызывает отображение меню под ней. Меню не разворачивается при щелчке на кнопочной части элемента управления Аналогичен элементу управления ToolStripSplitButton. Единственное различие в том, что кнопка развертывания заменена изображением развернутого массива. Меню разворачивается при щелчке на любой части элемента управления Отображает поле со списком Вставляет индикатор протеканий процесса в панель инструментов Отображает текстовое поле Создает горизонтальные или вертикальные разделители элементов. Этот элемент управления уже был рассмотрен ранее
500 Часть II. Программирование для Windows В следующем практическом упражнении мы расширим пример использования меню, добавив в него панель инструментов. Панель инструментов будет содержать стандартные элементы панели инструментов и три дополнительные кнопки: Bold (Полужирный), Italic (Курсив) и Underline (Подчеркнутый). Она будет содержать также поле со списком для выбора шрифта. (Изображения, используемые в этом примере для кнопки выбора шрифта, можно найти в загружаемом исходном коде.) практическое занятие Расширение панели инструментов Чтобы дополнить предыдущий пример панелями инструментов, выполните следующие действия. 1. Удалите элемент ToolStripMenuItem, который был использован в меню Format. Выберите опцию Show Help Menu и нажмите клавишу <Delete>. Добавьте вместо него три элемента ToolStripMenuIterns и измените значение свойства CheckOnClick каждого из них на true: • Bold (Полужирный) • Italic (Курсив) • Underline (Подчеркнутый) 2. Назовите эти три элемента управления ToolStripMenuItemBold, ToolStripMenuItemltalic и ToolStripMenuItemUnderline. 3. Добавьте элемент управления ToolStrip в форму. В окне Actions Window (Окно действий) щелкните на опции Insert Standard Items (Вставить стандартные элементы). Выберите и удалите элементы Cut, Copy, Paste и следующий за ними элемент разделителя Separator. При вставке элемента управления ToolStrip пристыковка элемента управления RichTextBox может перестать выполняться правильно. В этом случае измените стиль свойства Dock на попе и вручную измените размер элемента управления, чтобы он заполнял форму. Затем измените значение свойства Anchor на Top, Bottom, Left, Right. 4. Создайте три новых кнопки и разделитель в конце панели инструментов, трижды выбрав опцию Button и один раз опцию Separator. (Чтобы вызвать эти опции, щелкните на последнем элементе в ToolStrip.) 5. Создайте два последних элемента, выбирая опцию ComboBox из раскрывающегося списка, а затем добавив разделитель в качестве последнего элемента. 6. Выберите элемент Help и перетащите его из текущей позиции в позицию последнего элемента в панели инструментов. 7. Три первых кнопки станут соответственно кнопками Bold (Полужирный), Italic (Курсив) и Underline (Подчеркнутый). Присвойте им имена, как показано в табл. 16.7. Таблица 16.7. Имена кнопок панели инструментов Кнопка панели инструментов Имя Кнопка Bold ToolStripButtonBold Кнопка Italic ToolStripButtonltalic Кнопка Underline ToolStripButtonUnderline Поле СО СПИСКОМ ToolStripComboBoxFonts
Глава 16. Расширенные функциональные возможности Windows Forms 501 8. Выберите кнопку Bold, щелкните на многоточии (...) в свойстве Image (Изображение), выберите переключатель Project Resource File (Файл ресурса проекта) и щелкните на кнопке Import (Импортировать). Если вы загрузили исходный код примеров этой книги, используйте три изображения из папки Chapterl6\Toolbars\Images: BLD.ico, ITL.ico и UNDRLN.ico. Обратите внимание, что IC0 не входит в число расширений, предлагаемых по умолчанию программой Visual Studio. Поэтому при поиске пиктограмм придется выбрать опцию Show All Files (Показать все файлы) из раскрывающегося списка. 9. Выберите BLD. ico в качестве изображения для кнопки Bold. 10. Выберите кнопку Italic и измените ее изображение на ITL. ico. 11. Выберите кнопку Underline и измените ее изображение на UNDRLN. ico. 12. Выберите элемент ToolStripComboBox. В панели Properties установите свойства, как показано в табл. 16.8. Таблица 16.8. Значения свойств элемента ToolStripComboBox Свойство Значение Items MS Sans Serif Times New Roman DropDownStyle DropDownList 13. Установите значение свойства CheckOnClick каждой из кнопок Bold, Italic и Underline равным true. 14. Чтобы выбрать начальный элемент в поле со списком, введите следующий код в конструкторе класса: public Forml() { InitializeComponent(); this.ToolStripComboBoxFonts.Selectedlndex = 0; } 15. Для запуска примера нажмите клавишу <F5>. Должно открыться диалоговое окно, подобное приведенному на рис. 16.6. /- " Foiml i File boim.it Help : J ^ A _J В I U I MS Sam Serf - 3sa| Mi «• Рис. 16.6. Расширенная панель инструментов Добавление обработчиков событий Теперь можно приступить к добавлению обработчиков событий для элементов меню и панелей инструментов. У нас уже имеются обработчики для элементов меню Save, New и Open, а кнопки панели инструментов должны себя вести совершенно так же, как элементы меню. Этого легко добиться, присваивая события Click кнопок панели инструментов тем же обработчикам, которые уже используются кнопками в меню. Определите события, как показано в табл. 16.9.
502 Часть II. Программирование для Windows Таблица 16.9. Определение событий Кнопка ToolStripButton Событие New MenuItemNew_Click Open MenuItemOpen_Click Save MenuItemSave_Click Теперь пора добавить обработчики для кнопок Bold, Italic и Underline. Поскольку эти кнопки являются кнопками флажков, вместо события Click нужно использовать событие CheckedChanged, поэтому добавьте это событие для каждой из трех названных кнопок. Добавьте следующий код: private void ToolStripButtonBold_CheckedChanged(object sender, EventArgs e) { Font oldFont; Font newFont; bool checkState = ((ToolStripButton)sender).Checked; // Получение шрифта, используемого в выбранном тексте. oldFont = this.richTextBoxText.SelectionFont; if (!checkState) newFont = new Font(oldFont, oldFont.Style & -FontStyle.Bold); else newFont = new Font(oldFont, oldFont.Style I FontStyle.Bold); // Вставка нового шрифта и возвращение фокуса элементу управления RichTextBox. this.richTextBoxText.SelectionFont = newFont; this.richTextBoxText.Focus(); this.ToolStripMenuItemBold.CheckedChanged -= new EventHandler(ToolStripMenuItemBold_CheckedChanged); this.ToolStripMenuItemBold.Checked = checkState; this.ToolStripMenuItemBold.CheckedChanged += new EventHandler(ToolStripMenuItemBold_CheckedChanged) ; } private void ToolStripButtonItalic_CheckedChanged(object sender, EventArgs e) { Font oldFont; Font newFont; bool checkState = ((ToolStripButton)sender).Checked; // Получение шрифта, используемого в выбранном тексте. oldFont = this.richTextBoxText.SelectionFont; if (!checkState) newFont = new Font(oldFont, oldFont.Style & -FontStyle.Italic); else newFont = new Font(oldFont, oldFont.Style | FontStyle.Italic) ; // Вставка нового шрифта. this.richTextBoxText.SelectionFont = newFont; this.richTextBoxText.Focus(); this.ToolStripMenuItemltalic.CheckedChanged -= new EventHandler(ToolStripMenuItemItalic_CheckedChanged); this.ToolStripMenuItemltalic.Checked = checkState; this.ToolStripMenuItemltalic.CheckedChanged += new EventHandler(ToolStripMenuItemItalic_CheckedChanged);
Глава 16. Расширенные функциональные возможности Windows Forms 503 private void ToolStripButtonUnderline_CheckedChanged(object sender, EventArgs e) { Font oldFont; Font newFont; bool checkState = ((ToolStripButton)sender).Checked; // Получение шрифта, используемого в выбранном тексте. oldFont = this.richTextBoxText.SelectionFont; if (!checkState) newFont = new Font(oldFont, oldFont.Style & ~FontStyle.Underline); else newFont = new Font(oldFont, oldFont.Style I FontStyle.Underline); // Вставка нового шрифта. this.richTextBoxText.SelectionFont = newFont; this.richTextBoxText.Focus(); this.ToolStripMenuItemUnderline.CheckedChanged -= new EventHandler(ToolStripMenuItemUnderline_CheckedChanged); this.ToolStripMenuItemUnderline.Checked = checkState; this.ToolStripMenuItemUnderline.CheckedChanged += new EventHandler(ToolStripMenuItemUnderline_CheckedChanged); } Обработчики событий просто устанавливают нужный стиль для шрифта, используемого в элементе управления RichTextBox. Три последние строки каждого из трех методов выполняют действия с соответствующим элементом меню. Первая строка удаляет обработчик события из элемента меню. Это предотвращает запуск каких-либо событий при переходе к следующей строке. В результате состояние свойства Checked устанавливается в то же значение, что и у кнопки панели инструментов. И, наконец, выполняется переустановка обработчика события. Обработчики событий элементов меню должны просто устанавливать свойство Checked кнопок панели инструментов, предоставляя выполнение всех остальных необходимых действий обработчикам кнопок панели инструментов. Добавьте обработчики события CheckedChanged и введите следующий код: private void ToolStripMenuItemBold_CheckedChanged(object sender, EventArgs e) this.ToolStripButtonBold.Checked = ToolStripMenuItemBold.Checked; private void ToolStripMenuItemItalic_CheckedChanged(object sender, EventArgs e) this.ToolStripButtonltalic.Checked = ToolStripMenuItemltalic.Checked; private void ToolStripMenuItemUnderline_CheckedChanged(object sender, EventArgs e) this.ToolStripButtonUnderline.Checked = ToolStripMenuItemUnderline.Checked; Теперь осталось только предоставить пользователям возможность выбора семейства шрифта из элемента управления ComboBox. Каждый раз, когда пользователь изменяет выбор в элементе управления ComboBox, приложение генерирует событие SelectedlndexChanged, поэтому добавьте обработчик для этого события:
504 Часть И. Программирование для Windows private void toolStripComboBoxFonts_SelectedIndexChanged(object sender, EventArgs e) { string text = ((ToolStripComboBox)sender).Selectedltem.ToString(); Font newFont = null; // Создание нового шрифта из нужного семейства шрифтов, if (richTextBoxText.SelectionFont == null) newFont = new Font(text, richTextBoxText.Font.Size); else newFont = new Font(text, richTextBoxText.SelectionFont.Size, richTextBoxText.SelectionFont.Style); richTextBoxText.SelectionFont = newFont; } Теперь запустите приложение. Должна появиться возможность открытия диалогового окна, подобного показанному на рис. 16.7. Панель инструментов слегка смещена вправо от отображаемого меню. 77^7 Flit Foimat Help j.J jj I Bold BddM! I Italic BoU Timet N«w R*m*n Italic Tim4> ЛЬ» Roman л.а1 Тит New Roman .PJJj^ Рис. 16.7. Приложение с добавленной возможностью открытия диалогового окна Элемент управления StatusStrip Последний из элементов небольшого семейства строковых элементов управления — StatusStrip. Он представляет строку, отображаемую в нижней части диалоговых окон многих приложений. Как правило, эта строка служит для отображения краткой информации о текущем состоянии приложения. Наглядным примером может быть отображение в строке состояния приложения Word текущей страницы, столбца, строки и т.п. во время ввода текста. Элемент управления StatusStrip является производным от ToolStrip. Поэтому результат перетаскивания этого элемента управления поверх формы должен выглядеть достаточно знакомо. Три из четырех элементов управления, которые могут быть использованы в элементе управления StatusStrip — ToolStripDropDownButton, ToolStripProgressBar и ToolStripSplitButton — уже были представлены ранее. Поэтому нам остается рассмотреть только один элемент управления, характерный для StatusStrip: StatusStripStatusLabel, который является также используемым по умолчанию. Свойства элемента управления StatusStripStatusLabel StatusStripStatusLabel используется для предоставления пользователю краткой информации о текущем состоянии приложения в виде текста и изображений. Поскольку в действительности надпись — очень простой элемент управления, количество его свойств, которые мы рассмотрим, не слишком велико. Два свойства, описан-
Глава 16. Расширенные функциональные возможности Windows Forms 505 ные в табл. 16.10, не являются специфичными для надписи, но они могут и должны применяться с определенной пользой. Таблица 16.10. Свойства класса StatusStripStatusLabel Свойство Значение AutoSize DoubleClickEnable Свойство AutoSize включено по умолчанию, что в действительности не слишком естественно, поскольку нежелательно, чтобы надписи в строке состояния перемещались по ней только из-за изменения текста в одной из них. Если только информация, отображаемая в надписи, не является статичной, значение этого свойства всегда следует изменять на false Это свойство указывает, будет ли генерироваться событие Doubleclick, что означает предоставление пользователям еще одного места для изменения чего-либо в приложении. Пример такого подхода — предоставление пользователям возможности двойного щелчка на панели, содержащей слово "Bold" для включения или отключения выделения текста полужирным шрифтом В следующем практическом занятии мы создадим простую строку состояния для рассматриваемого примера. Строка состояния будет содержать четыре панели, три из которых отображают изображение и текст, а последняя — только текст. Практическое заш Элемент управления Status Strip Выполните следующие действия, чтобы расширить возможности созданного нами простого текстового редактора. 1. Дважды щелкните на элементе StatusStrip панели инструментов ToolBox, чтобы добавить его в диалоговое окно. Возможно, придется изменить размеры элемента RichTextBox в форме. 2. Щелкните на кнопке с символом многоточия (...) рядом со свойством Items элемента управления StatusStrip на панели Properties. В результате откроется редактор Items Collection Editor (Редактор коллекции элементов). 3. Щелкните на кнопке Add (Добавить) четыре раза, чтобы добавить четыре панели в элемент управления StatusStrip. Установите для панелей свойства, как показано в табл. 16.11. Таблица 16.11. Значения свойств панелей Панель 1 Свойство Name Text AutoSize DisplayStyle Font Size TextAlign Значение toolstripstatusLabelText Clear this property False Text Arial; 8,25pt; style=Bold 259,17 Middle Left
506 Часть II. Программирование для Windows Окончание табл. 16.11 Свойство Name Text DisplayStyle Enabled Font Size Image ImageAlign Name Text DisplayStyle Enabled Font Size Image ImageAlign Name Text DisplayStyle Enabled Font Size Image ImageAlign Значение toolStripStatusLabelBold Bold ImageAndText False Anal; 8.25pt; style=Bold 47, 17 BLD Middle - Center toolStripStatusLabelItalic Italic ImageAndText False Anal; 8.25pt; style=Bold 48, 17 ITL Middle-Center toolStripStatusLabelUnderl. Underline ImageAndText False Arial; 8.25pt; style=Bold 76, 17 UNDRLN Middle-Center 4. Добавьте следующую строку обработчика события в конце метода ToolStrip ButtonBold_CheckedChanged: toolStripStatusLabelBold.Enabled = checkState; 5. Добавьте следующую строку обработчика события в конце метода ToolStrip ButtonItalic_CheckedChanged: toolStripStatusLabelltalic.Enabled = checkState; 6. Добавьте следующую строку обработчика события в конце метода ToolStrip ButtonUnderline_CheckedChanged: toolStripStatusLabelUnderline.Enabled = checkState; 7. Выберите элемент управления RichTextBox и добавьте в код событие TextChanged. Введите следующий код:
Глава 16. Расширенные функциональные возможности Windows Forms 507 private void richTextBoxText_TextChanged(object sender, EventArgs e) { toolStripStatusLabelText.Text = "Number of characters: " + richTextBoxText.Text.Length; При запуске приложения должно открыться диалоговое окно, подобное показанному на рис. 16.8. ^ f eiml file Foimat Help ■ J ^ A J В / Ц , MS Sam Serf - !iQ _JL *шштшш*тлытш*штшшШ This text is exactly 69 chat octets long, including cauiage returns tjumto+t of chJiacteis: (9 Рис. 16.8. Приложение с добавленным элементом StatusStrip Приложения SDI и MDI 'j < .ii. ni.itrti Edit View Help Щ Традиционно для Windows можно программировать три вида приложений. □ Приложения на основе диалоговых окон. Эти приложения открываются для пользователя в виде одного диалогового окна, предоставляющего доступ ко всем своим функциональным возможностям. □ Однодокументные интерфейсы (Single-document interfaces — SDI). Эти приложения представляются пользователю в виде одного окна с меню и одной или несколькими панелями инструментов; в этом окне пользователь может выполнять ту или иную задачу. □ Многодокументные интерфейсы (Multiple-document interfaces — MDI). Эти приложения представляются пользователю так же, как SDI-приложения, но могут одновременно содержать несколько открытых окон. Обычно приложения на основе диалоговых окон представляют собой небольшие, служащие одной единственной цели приложения, ориентированные на выполнение конкретной задачи, которая требует ввода минимума данных пользователем или же ориентирована на работу с очень специфичным типом данных. Примером такого приложения может служить калькулятор Windows, показанный на рис. 16.9. Однодокументные интерфейсы обычно предназначены для решения одной конкретной задачи, поскольку они позволяют пользователям для работы загружать в приложение единственный документ. Однако обычно выполнение этой задачи связано с интенсивным взаимодействием с пользователем, и зачастую пользователям требуется возможность сохранения или загрузки результатов свой работы. Показательными примерами SDI-приложений служат WordPad (рис. 16.10) и Paint, поставляемые с Windows. Back досе Г ИСБИИШВ ваишаи Г^Л ГЛПЛГГ1ГПГП Рис. 16.9. Приложение калькулятора Windows
508 Часть II. Программирование для Windows [ ч) Document WoidPad Eile t Hit View liseit 1 omi.il Help Anal v 10 v We*»«n g . 1 • . • 2 • " • 3 • • • 4 • • • 5 • • • 6 • • ■ 7 • [for Hafr, press Ft v в/и^ ___ :E . ■ 8- ' • 9- ' -10' ' -11' -12- ' -13- • -14- • I ■T. 1_| 1^- -ie- ' NLW Рис. 16.10. Приложение WordPad Однако в каждый конкретный момент времени эти приложения позволяют работать только с одним открытым документом. Поэтому, если пользователю требуется открыть второй документ, ему придется открыть новый экземпляр SDI-приложения, который никак не будет связан с первым экземпляром. Любые изменения в конфигурации одного экземпляра не переносятся в другой экземпляр. Например, в одном экземпляре Paint для рисования можно установить красный цвет, а при открытии второго экземпляра этого приложения цветом рисования будет заданный по умолчанию — черный. Многодокументные интерфейсы во многом подобны SDI-приложениям, за исключением того, что в любой данный момент времени они могут содержать более одного открытого документа в различных окнах. Верным признаком MDI-приложения может служить наличие меню Window (Окно) непосредственно перед меню Help (Справка) в строке меню. Примером MDI-приложения является Adobe Reader, показанный на рис. 16.11. С M«t.«R»-.l« Fik Edit View Document To«t« VMn4cw - Ю' П - * N Тт ш - 62V. Mop i_m ,iV м | Mm _ Ada»t KiHtt Htlp Г ЭрИзпе ▼ * Jans Cnirc t-c p .ootfiq at he WarV ~dk-*j tkt Arf-ih* э EdrtrgAdcloPDF ? Ш. P_| 1V179 , »| О \l/ I Jsinc Cnre hep * -ilng uul AdDbe ' EdhirqAdcbaPJF -\»л„ *vi АНл!» P_JL 1 311/У >i О Рис. 16.11. Приложение Adobe Reader
Глава 16. Расширенные функциональные возможности Windows Forms 509 В этой главе основное внимание уделено задачам, связанным с созданием MDI- приложения. Выбор такого подхода обусловлен тем, что любое SDI-приложение по существу является поднабором MDI-приложения. Поэтому, если вы сможете создавать многодокументные интерфейсы, создание однодокументного интерфейса не составит сложности. Фактически в главе 17 мы создадим простое SDI-приложение, предназначенное для демонстрации применения обычно используемых диалоговых Windows. Построение MDI-приложений Что же требуется для создания многодокументного интерфейса? Во-первых, задача, которую должны быть в состоянии выполнять пользователи, должна быть такой, которая может требовать работы с несколькими открытыми документами. Показательный пример такого приложения — текстовый редактор или программа просмотра текстовых файлов. Во-вторых, необходимо предоставить панели инструментов для выполнения наиболее часто выполняемых в приложении задач, таких как установка стиля шрифта, загрузка и сохранение документов. В-третьих, нужно предусмотреть меню, включающее в себя пункт Window (Окно), которое позволит пользователям позиционировать открытые окна относительно друг друга (в виде плитки или каскадным образом) и будет представлять список всех открытых окон. Еще одна характерная особенность MDI-приложений состоит в том, что когда окно открыто и это окно содержит меню, это меню должно быть интегрировано в главное меню приложения. MDI-приложение состоит, по меньшей мере, из двух различных окон. Первое окно, которое мы создадим, называется контейнером MDI. Окно, которое может отображаться внутри этого контейнера, называется дочерним MDI. В этой главе для контейнера MDI применяется термин "контейнер MDI" или "главное окно", а для дочернего MDI — "дочерний MDI" или "дочернее окно". В следующем практическом занятии рассмотрен небольшой пример выполнения всех этих действий. Затем мы перейдем к выполнению более сложных задач. Практическое занятие^ Создание мЬТ-ПрИЛОЖеНИЯ Создание MDI-приложения мы начнем с того же, что и предпринимается для любого другого приложения — с создания приложения Windows Forms в среде Visual Studio. 1. Создайте новое Windows-приложение MDIBasic в каталоге С: \BegVCSharp\ Chapterl6. 2. Чтобы изменить тип главного окна приложения с формы на контейнер MDI, установите значение свойства IsMdiContainer формы равным true. Цвет фона формы изменится, указывая, что теперь она представляет собой всего лишь фон, на который не следует помещать видимые элементы управления (хотя эти и возможно и даже целесообразно в некоторых ситуациях). Выберите форму и установите ее свойства, как показано в табл. 16.12. Таблица 16.12. Значения свойств формы Свойство Name IsMdiContainer Text WindowState Значение frmContainer True MDI Basic Maximized
510 Часть II. Программирование для Windows 3. Чтобы создать дочернее окно, добавьте в проект новую форму, выбрав Windows Form (Форма Windows) из открывшегося диалогового окна Windows Form посредством выбора команды меню Project^Add New Item (ПроектеДобавить новый элемент). Дайте форме имя f rmChild. 4. Новая форма становится дочерним окном при установке ссылки на главное окно в качестве свойства MdiParent дочернего окна. Это свойство нельзя установить посредством панели Properties. Для этого нужно использовать код. Измените конструктор новой формы следующим образом: public frmChild(MdiBasic.frmContainer parent) { InitializeComponent(); // Установка контейнера в качестве родительского элемента управления формы. this.MdiParent = parent; } 5. Теперь, прежде чем MDI-приложение сможет отображаться в наиболее общем режиме, осталось выполнить всего два действия. Необходимо указать контейнеру MDI, какие окна следует отображать, а затем обеспечить их отображение. Просто создайте новый экземпляр формы, которую нужно отобразить, а затем вызовите для нее метод Show (). Конструктор формы, которую нужно отображать в качестве дочерней, должен связываться с родительским контейнером. Этого можно достичь, устанавливая экземпляр контейнера MDI в качестве свойства MdiParent этого конструктора. Измените конструктор родительской формы MDI следующим образом: public frmContainer () { InitializeComponent(); // Создание нового экземпляра дочерней формы. MdiBasic.frmChild child = new MdiBasic.frmChild(this); // Отображение формы, child.Show(); } Описание полученных результатов Весь код, необходимый для отображения дочерней формы, сосредоточен в конструкторах формы. Вначале рассмотрим конструктор дочернего окна: public frmChild(MdiBasic.frmContainer parent) { InitializeComponent (); // Установка контейнера в качестве родительского элемента управления формы. this.MdiParent = parent; } Чтобы связать дочернюю форму с контейнером MDI, необходимо обеспечить, чтобы дочерняя форма зарегистрировалась с помощью контейнера. Это достигается посредством установки свойства MdiParent формы, как показано в предыдущем коде. Обратите внимание, что используемый конструктор включает в себя параметр parent. Поскольку С# не предоставляет определенные по умолчанию конструкторы для класса, который определяет собственный конструктор, приведенный код не позволяет создать экземпляр формы, не связанной с контейнером MDI. И, наконец, нам нужно отобразить форму. Это выполняется в конструкторе контейнера MDI:
Глава 16. Расширенные функциональные возможности Windows Forms 511 public frmContainer () { InitializeComponent (); // Создание нового экземпляра дочерней формы. MdiBasic.frmChild child = new MdiBasic.frmChild(this) ; // Отображение формы, child.Show() ; } Мы создаем новый экземпляр дочернего класса и передаем конструктору объект this, который представляет текущий экземпляр класса контейнера MDI. Затем мы вызываем метод Show () применительно к новому экземпляру дочерней формы. Вот и все! Если нужно отобразить более одного дочернего окна, достаточно для каждого окна повторить две строки, выделенные в приведенном коде. Теперь запустите код. Интерфейс должен выглядеть подобно показанному на рис. 16.12 (хотя первоначально форма MDI Basic будет развернута во весь экран, на этом рисунке ее размеры были изменены для отображения на одной странице). Рис. 16.12. Форма MDI Basic Созданный интерфейс пользователя не является чем-то выдающимся, но он явно служит хорошей отправной точкой. В следующем практическом упражнении мы создадим простой текстовый редактор на основе того, что уже достигли в этой главе с помощью меню, панелей инструментов и строк состояния. практическое занятие[ Создание текстового редактора MDI Вначале создадим базовый проект, а затем рассмотрим, что и как происходит. 1. Снова обратитесь к рассмотренному ранее примеру использования строки состояния. Переименуйте форму на frmEditor и измените ее свойство Text на Editor. 2. Добавьте в проект новую форму frmContainer .cs и установите для нее свойства, перечисленные в табл. 16.13.
512 Часть II. Программирование для Windows Таблица 16.13. Значения свойств формы Свойство Значение Name frmContainer IsMdiContainer True Text Simple Text Editor WindowState Maximized 3. Откройте файл Program.cs и в методе Main измените строку, содержащую оператор Run, следующим образом: Application.Run(new frmContainer()); 4. Измените конструктор формы f rmEditor, как показано ниже: public frmEditor (frmContainer parent) { InitializeComponent(); this.ToolStripComboBoxFonts.Selectedlndex = 0; // Привязка к родительской форме. this .Mdi Parent = parent; } 5. Замените значение свойства MergeAction элемента меню с текстом &File на Replace, а значение этого же свойства элемента с текстом &Format — на MatchOnly. Измените значение свойства AllowMerge панели инструментов на False. 6. Добавьте элемент управления MenuStrip в форму frmContainer. Добавьте в него единственный элемент с текстом &File. 7. Измените конструктор формы frmContainer следующим образом: public frmContainer() { InitializeComponent(); frmEditor newForm = new frmEditor(this); newForm.Show(); } Запустите приложение, чтобы увидеть результат, подобный показанному на рис. 16.13. »з Simple Text EdWfcr * ЬЛЗ/ЕУ] File Help •s EdHor Format Рис. 16.13. Простой текстовый редактор MDI
Глава 16. Расширенные функциональные возможности Windows Forms 513 Описание полученных результатов Обратите внимание на небольшое "чудо". Создается впечатление, что меню File и Help были удалены из формы f rmEditor. Выберите меню File в окне контейнера, и вы убедитесь, что теперь элементы меню диалогового окна f rmEditor можно найти здесь. Дочерние окна должны содержать те меню, которые специфичны для этих окон. Меню File должно быть общим для всех окон, а не присутствовать только в дочерних окнах. Причина этого становится понятной, если закрыть окно Editor — теперь меню File не содержит ни одного элемента! Меню File должно позволять вставлять элементы, которые специфичны для дочернего окна, когда это окно находится в фокусе, а все остальные элементы должны отображаться главным окном. Свойства, перечисленные в табл. 16.14, управляют поведением элементов меню. Таблица 16.14. Свойства, управляющие поведением элементов меню Свойство Описание MergeAction Определяет поведение элемента при его вставке в другое меню. Возможные значения следующие: Append — вызывает помещение элемента в последнюю позицию меню insert — вставляет элемент непосредственно перед элементом, который соответствует критерию вставки. Этим критерием может быть либо текст элемента, либо индекс MatchOnly — требуется соответствие, но элемент не будет вставлен Remove — удаляет элемент, который соответствует критерию вставки элемента Replace — элемент, соответствующий критерию, заменяется, а следующие за ним элементы размещаются вслед за вставляемым элементом Merge index Представляет позицию элемента меню относительно других элементов меню, с которым выполняется объединение. Если нужно управлять порядком объединяемых элементов, установите это значение равным 0. В противном случае установите его равным 1. При выполнении слияния это значение проверяется, и, если оно не равно 1, то используется для сопоставления индексов элементов, а не их текста AllowMerge Установка значения этого свойства равным false означает, что меню не будут объединяться В следующем практическом занятии мы продолжим создание текстового редактора, изменяя способ объединения меню для отражения их принадлежности тому или иному окну. практическое занятие Объединение меню Чтобы изменить текстовый редактор для обеспечения использования меню и в контейнере, и в дочерних окнах, выполните следующие действия. 1. Добавьте перечисленные в табл. 16.15 четыре элемента меню в меню File формы f rmContainer. Обратите внимание на скачкообразное изменение значений свойства Mergelndex.
514 Часть II. Программирование для Windows Таблица 16.15. Элементы меню File формы frmContainer Элемент Свойство Значение &New &Ореп E&xit Name MergeAction MeгдеIndex ShortcutKeys Name MergeAction Mergelndex ShortcutKeys MergeAction Mergelndex Name MergeAction Mergelndex ToolstripMenuItemNew MatchOnly 0 Ctrl + N ToolStripMenuItemOpen MatchOnly 1 Ctrl + 0 MatchOnly 10 ToolStripMenuItemNewExit MatchOnly 11 2. Нам требуется способ добавления новых окон, поэтому дважды щелкните на элементе меню New (Создать) и добавьте следующий код. Этот код совпадает с введенным в конструктор для первого предназначенного для отображения диалогового окна. private void ToolStripMenuItemNew_Click(object sender, EventArgs e) { frmEditor newForm = new fnnEditor (this) ; newForm. Show () ; 3. В форме frmEditor удалите элемент Open из меню File. Свойства остальных элементов меню измените так, как показано в табл. 16.16. Таблица 16.16. Свойства остальных элементов меню Элемент Свойство Значение &File &New - &Save Save &As - MergeAction Mergelndex MergeAction Mergelndex MergeAction Mergelndex MergeAction Mergelndex MergeAction Mergelndex MergeAction MatchOnly -1 MatchOnly -1 Insert 2 Insert 3 Insert 4 Insert
Глава 16. Расширенные функциональные возможности Windows Forms 515 Окончание табл. 16.16 Элемент Свойство Значение &Print Print - E&xit Preview Mergelndex MergeAction Mergelndex MergeAction Mergelndex MergeAction Mergelndex Name Text MergeAction Mergelndex 5 Insert 6 Insert 7 Insert 8 ToolstripMenuItemClose &Close Insert 9 4. Запустите приложение. Два меню File были объединены, но дочернее диалоговое окно все же сохранило меню File, содержащее один элемент — New. Описание полученных результатов Элементы, для которых значение свойства MergeAction установлено равным MatchOnly, не перемещаются из одного меню в другое, но в случае элемента меню &File совпадение текста двух элементов ведет к объединению их элементов меню. Элементы в меню File объединяются в соответствии со значениями свойств Mergedlndex соответствующих элементов. Значения свойства MergeAction тех из них, которые должны оставаться на месте, устанавливаются равными MatchOnly; значения остальных устанавливаются равными Insert. А что произойдет при щелчке на элементах меню New и Save (Сохранить) в двух различных меню? Вспомните, что меню New в дочернем диалоговом окне просто очищает текстовое поле, в то время как второе меню должно создавать новое диалоговое окно. Поскольку эти два меню должны принадлежать к различным окнам, не удивительно, что они оба работают, как предполагалось. Но как насчет элемента Save? Он перенесен из дочернего диалогового окна в родительское. Откройте несколько диалоговых окон, введите в них какой-либо текст, а затем щелкните на меню Save. Откройте новое диалоговое окно и щелкните на меню Open (Открыть) (помните, что команда Save всегда выполняет сохранение в том же файле). Выберите одно из других окон, щелкните на меню Save, вернитесь к новому диалоговому окну и снова щелкните на меню Open. Вы убедитесь, что элементы меню Save всегда переносятся в диалоговое окно, находящееся в фокусе. При каждом выборе диалогового окна меню снова объединяются. Итак, мы добавили небольшой фрагмент кода в элемент New меню File диалогового окна f rmContainer и удостоверились, что диалоговые окна были созданы. Одно из меню, которое присутствует едва ли не во всех MDI-приложениях — меню Window (Окно). Оно позволяет упорядочивать диалоговые окна и часто отображает ту или иную форму их списка. В следующем практическом занятии мы добавим это меню в свой текстовый редактор.
516 Часть II. Программирование для Windows Практическое занятие Отслеживание ОКОН Чтобы расширить приложение, добавив в него возможность отображения всех открытых диалоговых окон и их упорядочения, выполните следующие действия. 1. Добавьте в меню формы f rmContainer новый элемент меню верхнего уровня &Window. Присвойте ему имя ToolStripMenuItemWindow. 2. Добавьте в новое меню три элемента, перечисленные в табл. 16.17. Таблица 16.17. Элементы меню Window Имя ToolStripMenuItemTile ToolStripMenuItemCascade - Свойство Text &Tile SCascade - 3. Выберите сам элемент MenuStrip, а не какой-то из отображаемых в нем элементов, и измените свойство MDIWindowListltem на ToolStripMenuItemWindow. 4. Дважды щелкните вначале на элементе Tile, а затем на элементе Cascade, чтобы добавить обработчики событий, и введите следующий код: private void ToolStripMenuItemTile_Click(object sender, EventArgs e) { LayoutMdi(MdiLayout.TileHorizontal); } private void ToolStripMenuItemCascasde_Click(object sender, EventArgs e) { LayoutMdi(MdiLayout.Cascade); } 5. Измените конструктор диалогового окна f rmEditor следующим образом: public frmEditor(frmContainer parent, int counter) { InitializeComponent(); this.ToolStripComboBoxFonts.Selectedlndex = 0; // Связывание с родительским окном. this.MdiParent = parent; this.Text = "Editor " + counter.ToStringO; } 6. Добавьте приватную переменную-член в верхнюю часть кода формы f rmContainer и измените конструктор и обработчик событий элемента меню New следующим образом: public partial class frmContainer : Form { private int mCounter; public frmContainer () { InitializeComponent (); mCounter = 1; frmEditor newForm = new frmEditor(this, mCounter); newForm.Show(); }
Глава 16. Расширенные функциональные возможности Windows Forms 517 private void ToolStripMenuItemNew_Click(object sender, EventArgs e) { frmEditor newForm = new frmEditor(this, ++mCounter); newForm.Show(); Описание полученных результатов Наиболее интересная часть этого примера связана с меню Window. Чтобы меню отображало список всех диалоговых окон, открытых в MDI-приложении, нужно всего лишь создать меню верхнего уровня и установить свойство MdiWindowListltem, чтобы оно указывало на этом меню. После этого каркас приложений будет добавлять в меню элемент для каждого отображенного диалогового окна. Элемент, который представляет текущее диалоговое окно, будет помечен флажком, и другое диалоговое окно можно выбрать, щелкая на нем в списке. Остальные два элемента меню — Tile (Мозаика) and Cascade (Каскад) — демонстрируют метод, используемый формой: MdiLayout. Этот метод позволяет упорядочивать диалоговые окна стандартным образом. Изменения в конструкторе и элементе New всего лишь обеспечивают нумерацию диалоговых окон. Если теперь запустить приложение, результат должен выглядеть подобно приведенному на рис. 16.14. |. « п..|.1- Т.- Г lii.i rite л. i .. ИИр •7м TIU 1 I -1И..1 1 ? ЫЬш 2 ^-Ш [I Ц K"SS«".$e» » Рис. 16.14. Раскрытое меню Window Создание элементов управления Иногда элементы управления, поставляемые с Visual Studio, просто не в состоянии удовлетворить все потребности. Причины этого могут быть самые различные — элементы прорисовываются не так, как желательно, ограничены в том или ином отношении, или же нужный элемент управления просто не существует. Осознавая это, Microsoft предоставляет средства для создания нужных элементов управления. Visual Studio предоставляет проект Windows Control Library (Библиотека элементов управления Windows), который можно использовать для самостоятельного создания элемента управления.
518 Часть П. Программирование для Windows Visual Studio позволяет разрабатывать два различных вида самодельных элементов управления. □ Пользовательские или составные элементы управления. При создании новых элементов управления этого вида они строятся на основе функциональных возможностей существующих элементов управления. В основном такие элементы управления создают для инкапсуляции функциональных возможностей в интерфейсе пользователя элемента управления, либо для расширения интерфейса элемента управления посредством объединения нескольких элементов управления в один модуль. □ Нестандартные элементы управления. Эти элементы управления можно создавать, когда ни один из существующих не удовлетворяет конкретным нуждам — т.е. их создание начинается с нуля. Нестандартный элемент управления сам прорисовывает свой интерфейс пользователя, и никакие существующие элементы управления не используются при его создании. Обычно потребность в создании такого элемента управления возникает, когда интерфейс пользователя нужного элемента управления не похож на интерфейс ни одного из доступных элементов управления. В этой главе основное внимание уделено пользовательским элементам управления, поскольку разработка и рисование нестандартного элемента управления с нуля выходят за рамки данной книги. В главе 33, посвященной GDI+, рассматриваются средства для самостоятельного рисования элементов, а от них вы должны быть в состоянии без труда перейти к созданию нестандартных элементов управления. Элементы управления ActiveX, использовавшиеся в среде Visual Studio 6, существовали в специальном файле с расширением . осх. По сути, эти файлы представляли собой СОМ-библиотеки DLL. В среде .NET элемент управления существует совершенно в том же виде, что и любая другая сборка, поэтому расширение .осх исчезло, а элементы управления существуют в виде библиотек DLL. Пользовательские элементы управления являются производными от класса System.Windows .Forms.UserControl. Этот базовый класс снабжает создаваемый элемент управления всеми основными функциональными возможностями, присущими элементу управления в среде .NET, оставляя программисту только задачу создания элемента управления. В качестве элемента управления могут выступать буквально любые компоненты — от надписей причудливой формы до полнофункциональных таблиц. На рис. 16.15 новый элемент управления представлен нижним прямоугольником, UserControll. Пользовательские элементы управления являются производными от класса System. Windows. Forms. UserControl, a нестандартные элементы управления— от класса System. Windows. Forms. Control. При работе с элементами управления от них ожидают определенного поведения. Если элемент управления не оправдывает описанные ниже ожидания, его использование вполне может разочаровать пользователя. Object CUss t MarshalByRefOb... Щ i 0m t Component CUss Control CUss f t ScrolaofeControl CUss t ContainerControJ CUss I UserControl CUss I UserControll CUss V. Puc. 16.15. Иерархия наследования нового элемента управления
Глава 16. Расширенные функциональные возможности Windows Forms 519 □ Поведение элемента управления во время проектирования должно быть во много аналогично его поведению во время выполнения. Это означает, что если элемент управления состоит из элементов управления Label и TextBox, объединяемых для создания элемента управления LabelTextbox, и Label, и TextBox должны отображаться во время проектирования, и текст, введенный в элементе Label, должен также отображаться во время проектирования. Хотя в данном примере достаточно просто добиться выполнения этого условия, в более сложных случаях это может быть сопряжено с проблемами, когда придется искать подходящий компромисс. □ Доступ к свойствам элемента управления из конструктора Forms Designer должен осуществляться логичным образом. Показательным в этом отношении примером может служить элемент управления ImageList, представляющий диалоговое окно, из которого пользователи могут выполнять обзор для выбора нужных изображений; как только изображения импортированы, они отображаются в диалоговом окне в списке. На нескольких последующих страницах рассмотрен пример создания элементов управления. Этот пример создает элемент управления LabelTextbox и демонстрирует основы создания проекта пользовательского элемента управления, создания свойств и событий, а также отладки элементов управления. Как следует из имени элемента управления в последующем разделе, этот элемент управления объединяет в себе два существующих элемента в одном, который за один проход выполняет чрезвычайно распространенную в программировании для Windows задачу: добавление надписи в форму, а затем добавление тестового поля в эту же форму и размещение тестового поля относительно надписи. Пользователь этого элемента управления будет ожидать от него описанное ниже поведение. □ Пользователю будет желательно иметь возможность размещать текстовое поле справа от надписи или под ней. Если текстовое поле размещается справа от надписи, пользователь должен иметь возможность указывать фиксированное расстояние между левым краем элементом управления и текстовым полем, чтобы текстовые поля можно было выравнивать, размещая их одно под другим. □ Пользователь должен иметь доступ к обычным свойствам и событиям текстового поля и надписи. Элемент управления LabelTextbox Теперь, когда стоящие задачи ясны, запустите Visual Studio и создайте новый проект. 1. Создайте новый проект Windows Forms Control Library (Библиотека элементов управления Windows Forms) по имени LabelTextbox и сохраните его в каталоге C:\BegVCSharp\Chapterl6. При использовании версии Visual Studio Express данная опция может быть недоступной. В этом случае создайте новый проект Class Library (Библиотека классов) и добавьте пользовательский элемент управления в проект вручную через меню Project (Проект). Как видно на рис. 16.16, конструктор Forms Designer пре- Рис. 16.16. Поверх- доставляет поверхность проектирования, несколько отли- ность проектирова- чающуюся от использованной ранее. Во-первых, она значи- ния конструктора тельно меньше. Во-вторых, она вообще не выглядит диало- Forms Designer UserControll.es [Design]
520 Часть II. Программирование для Windows гом. Пусть новый вид интерфейса не вводит вас в заблуждение — все работает, как обычно. Основное различие в том, что до сих пор мы помещали элементы управления в уже существующую форму, а теперь нам предстоит создать элемент управления, предназначенный для помещения какую-либо форму. 2. Щелкните на поверхности проектирования и откройте свойства элемента управления. Измените значение свойства name элемента управления на ctlLabelTextbox. 3. Дважды щелкните на элементе управления Label в панели инструментов Toolbox, чтобы добавить его в создаваемый элемент управления, поместив в верхнем левом углу поверхности. Измените значение его свойства Name на lblTextBox. Установите Label в качестве значения свойства Text. 4. Дважды щелкните на элементе TextBox в панели инструментов Toolbox, чтобы добавить его в создаваемый элемент управления. Измените значение его свойства Name на txtLabelText. Во время проектирования неизвестно, как пользователь пожелает разместить эти элементы управления, поэтому нам придется написать код, который будет позиционировать элементы управления Label и TextBox. Этот же код будет определять позицию элементов управления при помещении элемента управления LabelTextbox в форму. Как видно на рис. 16.17, дизайн элемента управления выглядит как угодно, но не ободряюще — мало того, что элемент управ- Р 16 17 Л " ления TextBox заслоняет часть надписи, так еще и поверхность всего составного элемента управления слишком велика. Однако элемента управления 7 г это не имеет значения, поскольку в отличие от элементов, использованных до сих пор, то, что мы видим не является тем, что мы получаем! Код, который мы собираемся добавить к элементу управления, будет изменять его внешний вид, но только при добавлении элемента управления на форму. Вначале позиционируем элементы относительно друг друга. Пользователь должен иметь возможность выбора способа размещения элементов управления, и поэтому мы добавим к элементу управления не одно, а два свойства. Свойство Position позволяет пользователю выбирать одну из двух опций: Right (Справа) и Below (Внизу). Если пользователь выбирает опцию Right, в дело вступает еще одно свойство. Оно называется TextboxMargin, и его значение — значение типа int, которое представляет количество пикселей между левым краем элемента управления и позицией размещения элемента управления TextBox. Если пользователь указывает значение 0, при размещении правый край элемента управления TextBox выравнивается по правому краю элемента управления. Добавление свойств Чтобы предоставить пользователю возможность выбора между опциями Right и Below, начните с определения перечисления этих двух значений. Вернитесь к проекту элемента управления, перейдите в редактор кода и добавьте следующий код: public partial class ctlLabelTextbox : UserControl { // Перечисление двух возможных позиций public enum PositionEnum { Right, Below }
Глава 16. Расширенные функциональные возможности Windows Forms 521 Это всего лишь обычное перечисление, описанное в главе 5. А теперь, что касается "магии": позиция должна быть свойством, которое пользователь сможет устанавливать как посредством кода, так и с помощью визуального конструктора. Мы достигнем этого посредством добавления свойства в класс ctlLabelTextbox. Однако вначале нужно создать два поля-члена, которые будут содержать значения, выбираемые пользователем: // Поле-член, которое будет содержать значения, выбираемые пользователем private PositionEnum mPosition = PositionEnum.Rights- private int mTextboxMargin = 0 ; public ctlLabelTextbox () { Затем добавьте свойство Position следующим образом: public PositionEnum Position { get { return mPosition; } set { mPosition = value; MoveControls() ; } } Это свойство добавляется в класс подобно любому другому свойству. Если требуется вернуть свойство, мы возвращаем поле-член mPosition; если же требуется изменить значение свойства Position, мы присваиваем значение mPosition и вызываем метод MoveControls (). Несколько позже мы вернемся к рассмотрению метода MoveControls () — а пока достаточно знать, что этот метод позиционирует два элемента управления, анализируя значения mPosition и mTextboxMargin. Свойство TextboxMargin аналогично предыдущему, за исключением того, что оно работает с целочисленным значением: public int TextboxMargin { get { return mTextboxMargin; } set { mTextboxMargin = value; MoveControls(); } } Добавление обработчиков событий Прежде чем приступить к тестированию двух описанных свойств, нужно добавить также два обработчика событий. При помещении элемента управления в форму вызывается событие Load. Это событие нужно применять для инициализации элемента управления и любых ресурсов, которые он может использовать. Обработка этого события выполняется для перемещения элемента управления и для изменения его разме-
522 Часть II. Программирование для Windows ров так, чтобы он аккуратно охватывал два содержащихся в нем элемента управления. Второе добавляемое событие — SizeChanged. Оно вызывается при каждом изменении размеров элемента управления, и его обработка требуется для обеспечения правильной своей прорисовки самим элементом управления. Выберите элемент управления и добавьте два события: SizeChanged и Load. Затем добавьте обработчики событий: private void ctlLabelTextbox_Load(object sender, EventArgs e) { lblTextBox.Text = this.Name; // Добавление текста к надписи // Установка высоты элемента управления. this.Height = txtLabelText.Height > lblTextBox.Height ? txtLabelText .Height : lblTextBox.Height; MoveControls(); // Перемещение элементов управления. } private void ctlLabelTextbox_SizeChanged(object sender, System.EventArgs e) { MoveControls(); } И снова мы вызываем метод MoveControls () для позиционирования элементов управления. Теперь, прежде чем снова проверять созданный элемент управления, пора рассмотреть этот метод: private void MoveControls () { switch (mPosition) { case PositionEnum.Below: // Помещение верхнего края Textbox непосредственно под надписью, this.txtLabelText.Top = this.lblTextBox.Bottom; this.txtLabelText.Left = this.lblTextBox.Left; // Изменение ширины Textbox, чтобы она была равна ширине элемента управления, this. txtLabelText .Width = this .Width ; this.Height = txtLabelText.Height + lblTextBox.Height; break; case PositionEnum.Right: // Установка верхнего края Textbox на одном уровне с верхним краем надписи. txtLabelText.Тор = lblTextBox.Тор; // Если ширина границы равна нулю, текстовое поле будет // помешено непосредственно рядом с надписью. if (xnTextboxMargin = 0) { int width = this.Width-lblTextBox.Width-3; txtLabelText. Left = lblTextBox. Right + 3; txtLabelText. Width = width; } else { // Если ширина границы не равна нулю, текстовое поле // помещается там, где указал пользователь. txtLabelText. Left = mTex tboxMargin ; txtLabelText. Width = thi s. Right-mTex tboxMargin ; } this.Height = txtLabelText.Height > lblTextBox.Height ? txtLabelText.Height : lblTextBox.Height; break; ) }
Глава 16. Расширенные функциональные возможности Windows Forms 523 Для определения того, должно ли текстовое поле размещаться под надписью или справа от нее, мы проверяем значение mPosition в операторе switch. Если пользователь выбирает значение Below, верхний край текстового поля перемещается в позицию, расположенную под надписью. Затем левый край текстового поля перемещается к левому краю элемента управления, а его ширина устанавливается равной ширине элемента управления. Если пользователь выбирает опцию Right, существуют две возможности. Если значение TextboxMargin равно нулю, вначале нужно определить ширину незанятой области элемента управления, которую можно предоставить текстовому полю. Затем левый край текстового поля прижимается к правому краю текста, а его ширина устанавливается такой, чтобы заполнить свободную область элемента управления. Если же пользователь указывает ширину границы, левый край текстового поля помещается в указанную позицию, а его ширина определяется соответствующим образом. Теперь можно протестировать элемент управления. Но прежде чем двигаться дальше, постройте проект. Отладка пользовательских элементов управления Отладка пользовательского элемента управления значительно отличается от отладки приложения для Windows. Обычно достаточно добавить точку останова где-либо, нажать клавишу <F5> и посмотреть, что происходит. Если вы еще не знакомы с процессом отладки, обратитесь к главе 7. Чтобы он мог себя отображать, элемент управления нуждается в контейнере, и нам нужно предоставить такой контейнер. Мы выполним это в следующем практическом занятии, создав проект приложения цдя Windows. Шшшшшвшшшшшшштяашшшшаашшшшвшшшшишшшшашшшшшяашшшвшшашшшяшшшшшашл Практическое занятие ОТЛЭДКЭ ПОЛЬЗОВЭТеЛЬСКИХ ЭЛеМвНТОВ управления 1. Из меню File выберите пункт Add"=>New Project (Добавить1^Новый проект). В диалоговом окне Add New Project создайте новое Windows-приложение LabelTextboxTest. Поскольку это приложение предназначено только для тестирования пользовательского элемента управления, имеет смысл создать проект внутри проекта LabelTextBox. Теперь в проводнике решений Solution Explorer вы должны видеть два открытых проекта. Первый созданный проект, LabelTextbox, выделен полужирным. Это означает, что при попытке запуска решения отладчик будет пытаться использовать проект элемента управления в качестве стартового. Эта попытка окажется безуспешной, поскольку проект элемента управления не является самостоятельным. Чтобы исправить эту ситуацию, щелкните правой кнопкой мыши на имени нового проекта — LabelTextboxTest — и из контекстного меню выберите команду Set as Startup Project (Установить в качестве стартового проекта). Если теперь запустить решение, проект Windows-приложения будет запущен без каких-либо ошибок. 2. Теперь верхняя часть панели инструментов Toolbox должна содержать вкладку LabelTextBox Components (Компоненты LabelTextBox). Visual Studio распознает, что решение содержит компоненты библиотеки Windows Control Library, и что, вполне вероятно, элементы управления, предоставляемые этой библиотекой, желательно использовать в других проектах. Дважды щелкните на элементе управления ctlLabelTextbox, чтобы добавить его в форму. Обратите внимание,
524 Часть П. Программирование для Windows что узел References (Ссылки) в окне Solution Explorer развернут. Это обусловлено тем, что Visual Studio только что автоматически добавила ссылку на проект LabelTextBox. 3. В коде поищите компонент ctlLabel. Поиск нужно выполнять во всем проекте. В результате вы натолкнетесь на "фоновый" файл Form. Designer. cs, в котором Visual Studio скрывает большую часть генерируемого ею кода. Помните, что этот файл никогда не следует редактировать непосредственно. 4. Поместите точку останова на следующую строку: this.ctlLabelTextboxl = new LabelTextbox.ctlLabelTextbox() ; 5. Запустите код. Как и следовало ожидать, выполнение кода останавливается в установленной точке останова. Теперь выполните вход в код (при использовании раскладок клавиатуры, заданных по умолчанию, нажмите для этого клавишу <F11>). При входе в код будет выполнен переход к конструктору нового элемента управления, что и требуется для отладки компонента. Здесь же можно помещать дополнительные точки останова. Чтобы запустить приложение, нажмите клавишу <F5>. Расширение элемента управления LabelTextbox Теперь, наконец, можно протестировать свойства элемента управления. На рис. 16.18 показана надпись с именем элемента управления и текстовое поле, которое занимает остальную часть элемента управления. Обратите внимание, что элементы управления внутри элемента управления LabelTextbox перемещаются в соответствующие позиции при добавлении элемента управления в форму. Рис. 16.18. Расширенный элемент управления LabelTextbox Добавление дополнительных свойств В данный момент элемент управления позволяет выполнить не слишком многое, поскольку, к сожалению, он лишен возможности изменения текста в надписи и текстовом поле. Для устранения этого недостатка добавим два свойства: LabelText и TextboxText. Их добавление выполняется точно так же, как и двух предыдущих — откройте проект и добавьте следующий код: get { return mLabelText; } set mLabelText = value; lblTextBox.Text = mLabelText; MoveControls (); } public string TextboxText { get { return txtLabelText.Text;
Глава 16. Расширенные функциональные возможности Windows Forms 525 set { txtLabelText.Text = value; } } Нужно также объявить переменную-член mLabelText для хранения текста: private string mLabelText = ""; public ctlLabelTextbox () { Чтобы вставить текст, достаточно присвоить текст свойству Text элементов управления Label и TextBox и возвратить значения свойств Text. При изменении текста надписи необходимо вызывать метод MoveControls (), поскольку текст надписи может влиять на позицию текстового поля. И напротив, текст, вставленный в текстовое поле, не вызывает перемещение элементов управления; если длина текста превышает длину текстового поля, он исчезает из виду. Добавление дополнительных обработчиков событий Теперь пора подумать о том, какие события должен предоставлять элемент управления. Поскольку он является производным от класса UserControl, он наследует множество функциональных возможностей, не требующих специальной обработки. Однако существует ряд событий, обработку которых нежелательно выполнять стандартным образом. Примерами могут служить события KeyDown, KeyPress и KeyUp. Эти события необходимо изменить, поскольку пользователи будут ожидать их отправки при нажатии клавиши в текстовом поле. В существующем состоянии события отправляются, только если сам элемент управления находится в фокусе и пользователь нажимает клавишу. Чтобы изменить это поведение, необходимо выполнить обработку событий, отправленных текстовым полем, и передать их пользователю. Добавьте события KeyDown, KeyUp и KeyPress для текстового поля и введите следующий код: private void txtLabelText_KeyDown(object sender, KeyEventArgs e) OnKeyDown(e); private void txtLabelText_KeyUp(object sender, KeyEventArgs e) OnKeyUp(e); private void txtLabelText_KeyPress(object sender, KeyPressEventArgs e) OnKeyPress(e); Вызов метода ОпКеуХХХ влечет за собой вызов любых методов, подписанных на событие. Добавление нестандартного обработчика события При необходимости создания события, которое не существует ни в одном из базовых классов, придется проделать несколько больший объем работы. Создайте событие PositionChanged, которое будет происходить при изменении свойства Position.
526 Часть П. Программирование для Windows Для создания этого события требуется выполнение трех условий. □ Наличие подходящего делегата, который может быть использован для вызова методов, присваиваемых пользователем событию □ Пользователь должен иметь возможность подписки на событие, посредством присвоения ему метода. □ Мы должны вызвать метод, который пользователь присвоил событию. Используемый делегат — EventHandler, предоставляемый каркасом .NET. Как было описано в главе 12, он представляет собой специальный вид делегата, объявляемый его собственным ключевым словом, event. Следующая строка объявляет событие и позволяет пользователю подписаться на него: public event System.EventHandler PositionChanged; // Конструктор public ctlLabelTextbox() { Теперь остается только сгенерировать событие. Поскольку оно должно происходить при изменении свойства Position, мы генерируем его в средстве доступа к свойству Position: public PositionEnum Position { get { return mPosition; } set { mPosition = value; MoveControls () ; if (PositionChanged != null) // Проверка наличия подписчиков { PositionChanged (this, new EventArgsO) ; } } } Вначале убедитесь в наличии каких-либо подписчиков, проверив, не является ли null значением события PositionChanged. Если нет — тогда нужно вызвать методы. Подписка на новое нестандартное событие выполняется так же, как она выполнялась бы для любого другого, но при этом имеется небольшой нюанс: прежде чем событие будет отображено в окнах событий, необходимо построить элемент управления. После того как он построен, выберите элемент управления в форме в проекте LabelTextboxTest и дважды щелкните на событии PositionChanged в разделе Events (События) панели Properties. Затем добавьте следующий код в обработчик события: private void ctlLabelTextboxl_PositionChanged(object sender, EventArgs e) { MessageBox.Show("Changed"); } В действительности нестандартный обработчик события не делает ничего особенного — он просто указывает на то, что позиция изменилась.
Глава 16. Расширенные функциональные возможности Windows Forms 527 И, наконец, добавьте кнопку в форму, дважды щелкните на ней, чтобы добавить в проект обработчик ее события Click, а затем добавьте следующий код: private void buttonToggle_Click(object sender, EventArgs e) { ctlLabelTextboxl.Position = ctlLabelTextboxl.Position == LabelTextbox.ctlLabelTextbox.PositionEnum.Right ? LabelTextbox.ctlLabelTextbox.PositionEnum.Below : LabelTextbox.ctlLabelTextbox.PositionEnum.Right; } После запуска приложения позицию текстового поля можно будет изменять во время выполнения. При каждом перемещении текстового поля приложение вызывает событие PositionChanged и отображает окно сообщения messagebox. На этом создание примера завершается. Приложение можно было бы немного усовершенствовать, но я оставляю это в качестве самостоятельного упражнения. Резюме В этой главе мы начали с того, на чем остановились в предыдущей главе, и исследовали элементы управления MainMenu и ToolBar. Вы научились создавать приложения MDI и SDI и узнали, как меню и панели инструментов используются в этих приложениях. Затем мы перешли к созданию собственного элемента управления: разработке свойств, интерфейса пользователи и событий элемента управления. Следующая глава завершает рассмотрение форм Windows Forms, описывая особый тип форм, который мы лишь вскользь затронули до сих пор: обычные диалоговые окна Windows. Далее перечислены ключевые моменты, с которыми вы ознакомились в этой главе. □ Использование трех строковых элементов управления, которые позволяют работать с меню, панелями инструментов и строками меню в приложениях Windows Forms. □ Создание MDI-приложений, которые используются для еще большего расширения возможностей текстового редактора. □ Создание собственных элементов управления на основе существующих элементов управления. Упражнения 1. Используя пример LabelTextbox в качестве фундамента, создайте новое свойство MaxLength, хранящее максимальное количество символов, которые могут быть введены в текстовом поле. Затем создайте два новых события MaxLengthChanged и MaxLengthReached. Событие MaxLengthChanged должно генерироваться при изменении свойства MaxLength, а событие MaxLengthReached — когда в результате ввода символом пользователем длина текста в текстовом поле становится равной значению свойства MaxLength. 2. Элемент управления StatusBar включает свойство, которое позволяет пользователю дважды щелкнуть на поле в строке и тем самым запустить событие. Измените пример StatusBar, чтобы пользователь мог двойным щелчком на строке состояния выделять текст полужирным, курсивным и подчеркнутым шрифтом. Обеспечьте, чтобы текст Bold, отображаемый в панели инструментов, меню и строке состояния, всегда выделялся полужирным, когда эта опция активизирована, и не выделялся в противном случае. Выполните это же для опций Italic (Курсив) и Underlined (Подчеркнутый).
17 Использование обычных диалоговых окон В предшествующих трех главах мы рассмотрели различные аспекты программирования приложений Windows Forms и реализацию таких компонентов, как меню, панели инструментов и формы SDI и MDI. Теперь вы уже умеете отображать простые окна сообщений для получения информации от пользователя и создавать более сложные нестандартные диалоговые окна для запроса специфичной информации. Однако для выполнения таких обычных задач, как открытие и сохранение файлов, можно использовать готовые классы диалоговых окон, а не создавать собственные специализированные диалоговые окна. Этот подход предпочтительнее не только потому, что требует меньшего объема кода, но и поскольку использует знакомые диалоговые окна Windows, придавая приложению стандартный вид и образ действий. Каркас .NET Framework содержит классы, которые подключают диалоговые окна Windows для открытия и создания каталогов, открытия и сохранения файлов, обращения к принтерам и выбора цветов и шрифтов. В этой главе мы рассмотрим использование этих классов стандартных диалоговых окон. В частности, будут рассмотрены следующие вопросы. □ Использование классов OpenFileDialog и SaveFileDialog. □ Вы ознакомитесь с иерархией класса печати каркаса .NET и научитесь использовать классы PrintDialog, PageSetupDialog и PrintPreviewDialog для реализации печати и предварительного просмотра. □ Изменение шрифта и цвета с помощью классов FontDialog и ColorDialog. □ Использование класса FolderBrowserDialog. Обычные диалоговые окна Диалог — это окно, которое отображается в контексте другого окна. Диалоговое окно позволяет потребовать от пользователя ввод определенных данных до продолже-
Глава 17. Использование обычных диалоговых окон 529 ния выполнения программы. Обычное диалоговое окно — это окно, используемое для получения от пользователя информации, обычно требующейся большинству приложений, такой как имя файла, и являющееся частью операционной системы Windows. Классы, включенные в состав Microsoft .NET Framework, показаны на рис. 17.1. Все эти классы диалоговых окон, за исключением PrintPreviewDialog, являются производными от абстрактного базового класса CommonDialog, который содержит методы для управления обычными диалоговыми окнами Windows. Класс CommonDialog определяет перечисленные в табл. 17.1 методы и события, общие для всех классов обычных диалоговых окон. Таблица 17.1. Методы и события класса CommonDialog Общедоступные методы Описание и события экземпляра showDialog () Этот метод реализован на основе производного класса и предназначен для отображения обычного диалогового окна Reset () Каждый производный класс диалогового окна реализует метод Reset () для установки первоначальных, заданных по умолчанию значений всех свойств класса диалогового окна HelpRequest Это событие генерируется, когда пользователь щелкает на кнопке Help (Справка) в обычном диалоговом окне Все эти классы диалоговых окон служат оболочками для обычного диалогового окна Windows, делая его доступным для .NET-приложений. PrintPreviewDialog является исключением, поскольку добавляет в приложение Windows Form собственные элементы для управления предварительным просмотром печати и, следовательно, в действительности вообще не является диалоговым окном. Классы OpenFileDialog и Save File Dial од являются производными от абстрактного базового класса FileDialog, который добавляет файловые функции, общие для диалоговых окон открытия и закрытия файлов. Ниже приведен краткий обзор возможного применения различных диалоговых окон. □ OpenFileDialog служит для предоставления пользователям возможности просмотра и выбора файлов для открытия. Это диалоговое окно может быть сконфигурировано для выбора одного или нескольких файлов. □ Посредством SaveFileDialog пользователи могут указывать имя файла и отыскивать каталог для сохранения файлов. □ PrintDialog служит для выбора принтера и установки параметров печати. □ Для конфигурирования полей страницы обычно используют класс PageSetup Dialog. □ Класс PrintPreviewDialog обеспечивает просмотр на экране того, что будет напечатано на бумаге, предоставляя такие возможности, как увеличение при просмотре. □ Класс FontDialog выводит список всех установленных шрифтов Windows с информацией о стилях и размерах и предоставляет возможность просмотра для выбора нужного шрифта. □ Класс ColorDialog облегчает выбор цвета. □ Для выбора и создания каталогов можно использовать класс диалогового окна FolderBrowserDialog.
530 Часть II. Программирование для Windows |.» UU\ и 11,1, I, 1;?д » III. » Ыа В I i ® lit? 0 » 1 1 la t?J » 111 ill д.**г ® III I eg
Глава 17. Использование обычных диалоговых окон 531 Некоторые приложения (разработанные определенной компанией) не только не используют обычные диалоговые окна, но и не соответствуют рекомендациям по стилю построения нестандартных диалоговых окон. Эти диалоговые окна функционируют не единообразно, а некоторые их кнопки и другие элементы управления оказываются в непривычных местах — например, кнопки ОК и Cancel (Отмена) в различных диалоговых окнах могут меняться местами. Иногда такая непоследовательность наблюдается даже в рамках одного приложения. Это не только раздражает пользователя, но и увеличивает время, требуемое для выполнения задачи. Сохраняйте единообразие в диалоговых окнах, которые создаете и используете! Единообразия легко достичь посредством использования обычных диалоговых окон. Применение диалоговых окон Поскольку CommonDialog — базовый класс для классов диалоговых окон, все классы диалоговых окон можно использовать одинаково. Общедоступными методами экземпляра являются ShowDialog () и Reset (). Метод ShowDialog () вызывает защищенный метод экземпляра RunDialog () для отображения диалогового окна, возвращая экземпляр DialogResult, который содержит информацию о взаимодействии пользователя с диалоговым окном. И напротив, метод Reset () устанавливает первоначальные, заданные по умолчанию значения свойств класса диалогового окна. Следующий фрагмент кода демонстрирует пример использования класса диалогового окна (каждый из шагов подробнее будет рассмотрен позже): OpenFileDialog dig = new OpenFileDialog (); dig.Title = "Sample"; dlg.ShowReadOnly = true; if (dig.ShowDialog () == DialogResult.OK) { string fileName = dig.FileName; } 1. Код создает новый экземпляр класса диалогового окна. 2. Мы устанавливаем ряд свойств для включения и отключения необязательных функций и установки состояния диалогового окна. В данном случае мы устанавливаем значение свойства Title равным "Sample", а свойства ShowReadOnly— равным true. 3. Вызов метода ShowDialog () ведет к отображению диалогового окна, ожиданию ввода пользователя и реагированию на него. 4. Если пользователь нажимает кнопку ОК, диалоговое окно закрывается, и мы проверяем это, сравнивая результат диалогового окна со значением DialogResult .ОК. Затем можно извлечь значения из ввода пользователя, запрашивая значения конкретных свойств. В данном случае значение свойства FileName хранится в переменной fileName. Все действительно очень просто! Конечно, каждое диалоговое окно обладает собственными параметрами конфигурирования, которые мы рассмотрим в последующих разделах. При использовании диалогового окна из приложения Windows Forms в среде Visual Studio задача оказывается еще более простой. Windows Forms Designer (Визуальный конструктор форм Windows) создает код для инициализации нового экземпляра, а зна-
532 Часть П. Программирование для Windows чения свойств могут быть установлены из окна Properties (Свойства). Как будет показано, для этого достаточно вызвать метод ShowDialog () и обратиться к изменяемым свойствам. Файловые диалоговые окна Посредством файлового диалогового окна пользователь может выбирать дисковод и просматривать файловую систему для выбора нужного файла. Все что должно возвращать это диалоговое окно — имя файла, определенное пользователем. Класс OpenFileDialog позволяет пользователям выбирать файл, который требуется открыть, по имени. И наоборот, класс Save File Dial од позволяет пользователям указывать имя файла, который требуется сохранить. Эти диалоговые окна похожи, поскольку являются производными от одного абстрактного базового класса, хотя и обладают некоторыми свойствами, уникальными для каждого класса. В этом разделе мы вначале рассмотрим особенности класса OpenFileDialog, а затем выясним, чем класс SaveFileDialog отличается от него. После этого мы разработаем пример приложения, которое использует оба эти класса. OpenFileDialog Класс OpenFileDialog позволяет пользователям выбирать файл для открытия. Как было показано в предыдущем примере, новый экземпляр класса OpenFileDialog создается до вызова метода ShowDialog (): OpenFileDialog dig = new OpenFileDialog (); dig.ShowDialog() ; Запуск программы Windows-приложения, содержащей эти две строки кода, приведен к открытию диалогового окна, показанного на рис. 17.2. Как уже было показано, до вызова метода ShowDialog () можно установить свойства этого класса, изменяющие поведение и внешний вид диалогового окна или ограничивающие количество файлов, которые могут быть открыты. Некоторые возможные изменения описаны в последующих разделах. Jf Documents Jj Recently Changed k. Recent Places Ц Desktop Л Computer Jf. Pictures More » Folders o4 Properties jfl Program.cs S.mplrfd.to! J S«mp*ef ditor f orm < s J Simp»**drtoff orm D«-wgoef с s ^ Simple*ditorform I ■■■ fiterjame сжв с Рис. 17.2. Диалог открытия файла
Глава 17. Использование обычных диалоговых окон 533 Чтобы класс OpenFileDialog можно было использовать с консольными приложениями, требуется наличие ссылки на сборку System. Windows. Forms, а пространство имен System. Windows. Forms должно быть включено в число доступных. Заголовок диалогового окна Определенный по умолчанию заголовок диалогового окна OpenFileDialog — Open (Открытие). Однако Open — не всегда лучшее имя. Например, если в приложении требуется проанализировать журнальные фалы или получить размеры файлов, выполнить какую-либо обработку, а затем сразу после этого закрыть файлы, заголовок Analyze Files (Анализ файлов) подошел бы больше, поскольку файлы не остаются открытыми для пользователя. К счастью, заголовок диалогового окна можно изменять, устанавливая значение свойства Title. Сама среда Visual Studio предоставляет различные заголовки для диалоговых окон открытия файлов, что позволяет различать типы открываемых файлов: Open Project (Открытие проекта), Open File (Открытие файла) и Open Web Site (Открытие Web-сайта). Следующий сегмент кода демонстрирует методику установки другого заголовка диалогового окна: OpenFileDialog dig = new OpenFileDialog(); dig.Title = "Open File"; dig.ShowDialog() ; Указание каталогов Каталог, открываемый по умолчанию — это каталог, который был открыт пользователем при последнем выполнении приложения. Установка значения свойства InitialDirectory позволяет изменять это поведение. Значение свойства InitialDirectory по умолчанию — пустая строка, представляющая каталог пользователя, отображенный при первом использовании диалогового окна в приложении. При втором открытии диалогового окна отображенный каталог совпадает с тем, в котором хранится ранее открытый файл. Для выяснения имени ранее открытого файла обычное диалоговое окно Windows OpenFileDialog использует реестр. Никогда не используйте в приложении жестко закодированную строку каталога, поскольку данный каталог может отсутствовать в системе пользователя. Для получения специальных системных папок можно использовать статический метод GetFolderPath () класса System.Environment. Метод GetFolderPath () принимает член перечисления Environment. SpecialFolder, определяющий системный каталог, путь к которому нужно получить. В следующем примере кода общий каталог пользователей для шаблонов устанавливается в качестве InitialDirectory: string directory = Environment.GetFolderPath(Environment.SpecialFolder.Templates); dig.InitialDirectory = directory; Установка файлового фильтра Файловый фильтр определяет типы файлов, которые пользователь может выбирать для открытия. Простая строка фильтра выглядит следующим образом: Text Documents (*.txt) |*.txt|All Files I*.*
534 Часть П. Программирование для Windows Фильтр используется для отображения записей в списке Files of Type: (Файлы типа:). Например, Microsoft WordPad отображает записи, как показано на рис. 17.3. GO т ff_. Documents J, Recently Changed jfc, Recent Placet Ц Desktop ]*P Computer % PiLt-jres %i K! r More » •me Date modified Type E S» , fujfcsu Siemens Groove Workspace Templates I InsUntCOOVD Meine empf Мрм Dateien _j My Shapes , My Virtual Machines JEJMy WetoSKes | Security , SQL Server Management Studio E 28 items '-J Text Documents CM) Те* Documents • MS-DOS Formal fW I Unicode Text Documents ftxti Рис. 17.3. Файловый фильтр в Microsoft WordPad Фильтр состоит из нескольких сегментов, разделенных символом конвейера (|). Каждая запись состоит из двух строк, поэтому количество сегментов всегда является четным. Первая строка в каждой записи определяет текст, отображаемый в поле списка. Вторая строка указывает расширение файлов, которые будут отображаться в диалоговом окне. Строку фильтра можно определить в свойстве Filter, как показано в следующем коде: dig.Filter = "Text documents (*.txt)|*.txt|All Filesl*.*"; Наличие пробела перед или после фильтра не допускается. Указание неправильного значения свойства Filter ведет к исключению времени выполнения — System.ArgumentException — и отображению сообщения об ошибке The provided filter string is invalid (Указанная строка фильтра недопустима). Свойство Filterlndex указывает номер записи, выбираемой в списке по умолчанию. В случае WordPad выбором по умолчанию является Rich Text Format (* . rtf) (на рис. 17.3 эта запись выделена). При наличии нескольких доступных для выбора типов файлов, значение свойства Filterlndex можно установить для выбора нужного типа по умолчанию. Обратите внимание, что значения Filterlndex начинаются с единицы! Проверка правильности Класс OpenFileDialog может выполнять определенную автоматическую проверку правильности файла перед попыткой его открытия. Когда значение свойства ValidateNames установлено равным true, имя файла, введенное пользователем, проверяется на предмет соответствия допустимым именам файлов Windows. Щелчок на кнопке О К диалогового окна при указании недопустимого имени файла ведет к отображению диалогового окна, показанного на рис. 17.4. В этом случае пользователь должен исправить имя файла или щелкнуть на кнопке Cancel (Отмена), чтобы покинуть диалоговое окно OpenFileDialog. Такие символы, как \\, / и :, являются недопустимыми в именах файлов.
Глава 17. Использование обычных диалоговых окон 535 аЬсЛМ The file name is not valid. H5~l Puc. 17.4. Диалоговое окно, сообщающее о неверном имени файла Когда значение свойства ValidateNames установлено равным true, CheckFileExists и CheckPathExists можно использовать для дополнительной проверки. CheckPathExists проверяет существование указанного пути, a CheckFileExists — существование указанного файла. Если файл не существует, щелчок пользователя на кнопке О К ведет к отображению диалогового окна, показанного на рис. 17.5. Open •- * * File not found. Check the file name and try again. ». ЬшП 1 <* 1 J Puc. 17.5. Диалоговое окно, сообщающее о том, что файл не найден Значение всех этих трех свойств, используемое по умолчанию — true, поэтому проверка выполняется автоматически. Справка Класс OpenFileDialog поддерживает кнопку Help, которая по умолчанию невидима. Установка значения ShowHelp равным true ведет к отображению этой кнопки, после чего можно добавить обработчик события HelpRequest для отображения справочной информации пользователю. Результаты Метод ShowDialog () класса OpenFileDialog возвращает значение перечисления DialogResult. Перечисление DialogResult определяет члены Abort (Прервать), Cancel (Отменить), Ignore (Игнорировать), No (Нет), None (Отсутствует), OK, Retry (Повторить попытку) и Yes (Да). None — значение, используемое по умолчанию, которое остается установленным до тех пор, пока пользователь не закроет диалоговое окно. При щелчке на кнопке метод возвращает соответствующий результат. Класс OpenFileDialog позволяет возвращать только DialogResult.OK и DialogResult.Cancel. Если пользователь щелкает на кнопке ОК, свойство FileName обеспечивает доступ к выбранному имени файла. Если пользователь щелкает на кнопке Cancel, значение свойства FileName — всего лишь пустая строка. Если значение свойства Multiselect установлено равным true, что позволяет пользователю выбирать более одного файла, свойство FileNames возвращает строковый массив со всеми выбранными именами файлов. Обратите внимание, что порядок файлов в свойстве FileNames является обратным по отношению к порядку их выбора — т.е. первая строка в массиве FileNames представляет последний выбранный файл. Кроме того, свойство FileNames всегда содержит имя последнего выбранного файла.
536 Часть II. Программирование для Windows Следующий фрагмент кода демонстрирует извлечение нескольких имен файлов из диалогового окна OpenFileDialog: OpenFileDialog dig = new OpenFileDialog(); dlg.Multiselect = true; if (dlg.ShowDialogO == DialogResult.OK) foreach (string s in dig.FileNames) // Отображение имен файлов в списке. this.listBoxl.Items.Add(s); } } Метод ShowDialogO открывает диалоговое окно. Поскольку значение свойства Multiselect установлено равным true, пользователь может выбрать несколько файлов. Если все в порядке, щелчок на кнопке ОК завершает работу диалогового окна и возвращает значение DialogResult .ОК. Оператор foreach выполняет последовательный просмотр всех строк строкового массива, возвращенного из свойства FileNames, и отображает в списке каждый выбранный файл. Свойства класса OpenFileDialog Подводя итоги, рис. 17.6 показывает диалоговое окно OpenFileDialog и его свойства — из него видно, какие свойства оказывают влияние на тот или иной элемент интерфейса пользователя. В качестве демонстрации использования стандартных диалоговых окон в следующем практическом занятии мы создадим Windows-приложение простого текстового редактора SimpleEditor, которое позволяет пользователю загружать, сохранять и редактировать текстовые файлы. Далее в этой главе будет показано, как выполняется печать текстового файла. Следующее упражнение начинается с использования диалоговых окон Open и Save. InitialDirectory MultiSelect Date Ukm . SfcnpteEdltor ДОШод> .;.Simp»eEdito( r RteQime Rtaoftype Open м reed -onJy „ FileName, FileNames ShowReadOnly, ReadOnlyChecked Filter, Filterlndex ShowHelp Puc. 17.6. Диалоговое окно OpenFileDialog и его свойства
Глава 17. Использование обычных диалоговых окон 537 Практическое занятие Создание Windows-приложения простого текстового редактора 1. Создайте новое приложение Windows Forms по имени SimpleEditor в каталоге C:\BegVCSharp\Chapterl7. 2. Переименуйте сгенерированный файл Forml.cs в SimpleEditorForm.cs. Ответьте Yes (Да), чтобы переименовать все ссылки на форму. В результате Visual Studio 2008 изменит также имя класса на SimpleEditorForm. 3. В качестве значения свойства Text формы установите SimpleEditor, и измените значение свойства Size на 570,270. В качестве области для чтения и изменения данных файла будет служить многострочное текстовое поле, поэтому добавьте элемент TextBox из панели инструментов Toolbox в Windows Forms Designer. Текстовое поле должно быть многострочным и должно охватывать всю область приложения, поэтому установите значения соответствующих свойств, как указано в табл. 17.2. Таблица 17.2. Значения свойств текстового поля Свойство Значение (Name) Text Multiline Dock ScrollBars AcceptsReturn AcceptsTab textBoxEdit True Fill Both True True 4. Добавьте элемент управления MenuStrip в приложение. В качестве его имени установите mainMenu. Меню должно содержать пункт File с подменю New (Создать), Open... (Открыть...), Save (Сохранить) и Save As... (Сохранить как...), как показано на рис. 17.7. Рис. 17.7. Пункт File главного меню Многоточия (..) в свойстве Text пунктов меню Open и Save As указывают пользователям, что им придется ввести некоторые дополнительные данные, преж-
538 Часть II. Программирование для Windows &New &Open &Save Save &As. OnFileNew OnFileOpen OnFileSave OnFileSaveAs де чем действие будет выполнено. При выборе меню File, New и Save действия выполняются без дополнительного вмешательства пользователя. Имена элементов меню и значения свойства Text приведены в табл. 17.3. Для каждого из элементов меню нужно также определить обработчик события Click с именем, указанным в таблице. Чтобы определить обработчик события Click, выберите меню в диалоговом окне, щелкните на кнопке Events (События) в окне Properties (Свойства), выберите событие Click и введите имя метода обработчика. Таблица 17.3. Элементы меню File Имя элемента меню Свойство Text Метод обработчика miFile &File miFileNew miFileOpen miFileSave miFileSaveAs 5. Обработчик пункта меню &New должен очищать данные в текстовом поле, вызывая метод Clear () элемента управления TextBox: private void OnFileNew(object sender, System.EventArgs e) { fileName = "Untitled"; textBoxEdit.Clear(); } 6. Установите переменную-член filename в значение "Untitled". Эту переменную-член нужно объявить и инициализировать в классе SimpleEditorForm: public partial class SimpleEditorForm : Form { private string fileName = "Untitled"; Форма SimpleEditor должна позволять передачу имени файла в качестве аргумента во время запуска приложения. Переданное имя файла должно использоваться для открытия файла и отображения его содержимого в текстовом поле. 7. Измените реализацию конструктора SimpleEditorForm, чтобы он использовал строку при передаче имени файла: public SimpleEditorForm(string fileName) { InitializeComponent(); if (fileName != null) { this.fileName = fileName; OpenFile() ; } } 8. Теперь реализацию метода Main () в файле Program, cs можно изменить, чтобы ему можно было передавать аргумент: static void Main (string [] args) { string fileName = null; if (args.Length != 0) fileName = args[0];
Глава 17. Использование обычных диалоговых окон 539 Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run (new SimpleEditorForm(fileName)) ; } 9. Реализуйте метод OpenFile (), открывающий файл и заполняющий текстовое поле данными из файла: В действительности метод OpenFile () обращается к открываемому файлу и использует методы, которые не найти подробного отражения в этой главе, но обращение к файлу подробно освещено в главе 24. private void OpenFile () { try { textBoxEdit.Clear(); textBoxEdit.Text = File.ReadAllText(filename); } catch (IOException ex) { MessageBox.Show(ex.Message, "Simple Editor", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); Этот код использует для чтения файла класс Fi I e, принадлежащий пространству имен System. 10, поэтому в начало файла SimpleEdi tor Form, cs нужно добавить также следующую директиву using: using System.10; Класс File и пространство имен System. 10 наряду с другими классами доступа к файлам рассмотрены в главе 24. 10. Как было показано в главе 7, внутри Visual Studio можно определять параметры командной строки, предназначенные для отладки. Щелкните правой кнопкой мыши на проекте в панели Solution Explorer и из контекстного меню выберите опцию Properties. В левой части диалогового окна Properties выберите вкладку Debug (Отладка). В открывшемся диалоговом окне можно ввести аргументы командной строки. В данном случае в целях тестирования введите следующее: C:\BegVCSharp\Chapterl7\SimpleEditor\SimpleEditor\SimpleEditorForm.cs 11. Запустите приложение. Файл текущего проекта SimpleEditorForm.cs немедленно откроется и отобразится, как показано на рис. 17.8. "S ш uang Syetem. jsng System Ю uang System Wrtdows Forms namespace SmptoEAor pubfcc partial das« StnpteEcMorf omi Forni I private sbng ffeName - "Untitled". pubfcc SmpieEdtorFcmpnngffeName. ' 1 Puc. 17.8. Работающее приложение SimpleEdi tor
540 Часть II. Программирование для Windows Описание полученных результатов Первые шесть шагов просто определяют форму — этот процесс должен быть знаком, если вы достаточно внимательно прочли предыдущие две главы. Основная работа приложения начинается с шага 7. Добавление string [] в параметры метода Main () позволяет использовать любые аргументы командной строки, указываемые пользователем при запуске приложения: static void Main(string [ ] args) В методе Main (), используя свойство Length, мы проверяем, были ли аргументы переданы методу. Если хотя бы один аргумент был передан, в качестве первого аргумента устанавливается переменная fileName, которая затем передается конструктору SimpleEditorForm: { string fileName = null; if (args.Length != 0) fileName = args[0]; Application.EnableVisualStyles (); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new SimpleEditorForm(fileName)); } В конструкторе SimpleEditorForm мы проверяем, имеет ли уже переменная fileName набор значений. Если да, то переменная-член fileName устанавливается, и метод OpenFile () вызывается для открытия файла. Для открытия файла и заполнения текстового поля используйте отдельный метод OpenFile (), а не записывайте соответствующие функции непосредственно в конструкторе класса, поскольку метод OpenFile () может применяться и в других частях программы: if (fileName != null) { this.fileName = fileName; OpenFile (); } Метод OpenFile () выполняет считывание данных из файла. Для получения всех строк, возвращенных в массиве строк, мы используем статический метод ReadAllText (). Поскольку операции с файлом вполне могут генерировать исключения (например, вследствие отсутствия у пользователя соответствующего права доступа к файлу), код помещен в блок try. В случае возникновения исключения ввода-вывода программа открывает окно сообщения, информирующего пользователя о проблеме, но приложение продолжает выполняться: protected void OpenFile () { try { textBoxEdit.Clear(); textBoxEdit.Text = File.ReadAllText(fileName); } catch (IOException ex) { MessageBox.Show(ex.Message, "Simple Editor", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
Глава 17. Использование обычных диалоговых окон 541 Если при запуске приложения в качестве аргумента командной строки вводится несуществующее имя файла, программа отображает окно сообщения, показанное на рис. 17.9. Simple Editor СоиИ not find file I c:\8egVC SharpChapteM TSmipteEdrtonSm4**drtOf^\DetMig4e ■Г. Рис. 17.9. Окно с сообщением о несуществующем файле Теперь можно выполнять чтение файлов с помощью простого редактора, передавая ему имя файла при запуске приложения. Естественно, при этом лучше использовать классы обычных диалоговых окон, и мы добавим их в приложение в следующем практическом занятии. Практическое занятие Добавление и использование класса OpenFileDialog 1. В категории Windows Forms панели инструментов Toolbox найдите компонент OpenFileDialog. Перетащите его из Toolbox на серую область в нижней части панели Forms Designer. Измените три свойства: имя экземпляра на dlgOpenFile, значение свойства Filter на приведенную ниже строку и значение свойства Filterlndex на 2, чтобы определить Wrox Documents как опцию, выбираемую по умолчанию: Text Documents (*.txt) |*.txt|Wrox Documents (*.wroxtext) |*.wroxtext|All Files I*.* 2. Ранее мы добавили к пункту меню Open обработчик события Click, названный OnFileOpen. Чтобы с помощью Forms Designer добавить реализацию этого обработчика события, дважды щелкните на пункте меню Open. Код реализации отображения диалогового окна и чтения выбранного файла имеет следующий вид: private void OnFileOpen(object sender, System.EventArgs e) { if (dlgOpenFile.ShowDialog() == DialogResult.OK) { fileName = dlgOpenFile . FileName; OpenFileO; Описание полученных результатов Добавление компонента OpenFileDialog в область проектирования Windows Forms Designer ведет к добавлению нового приватного члена в класс SimpleEdi tor Form. Приватный член можно видеть в файле SimpleEditorForm. Designer. cs. Этот файл отображается только при щелчке на кнопке Show All Files (Показать все файлы) в окне проводника решений Solution Explorer:
542 Часть II. Программирование для Windows partial class SimpleEditorForm { private System.Windows.Forms.TextBox textBoxEdit; private System.Windows.Forms.MenuStrip mainMenu; private System.Windows.Forms.ToolStripMenuItem miFile; private System.Windows.Forms.ToolStripMenuItem miFileNew; private System.Windows.Forms.ToolStripMenuItem miFileOpen; private System.Windows.Forms.ToolStripMenuItem miFileSave; private System.Windows.Forms.ToolStripMenuItem miFileSaveAs; private System.Windows.Forms.OpenFileDialog dlgOpenFile; В области кода конструктора, сгенерированной средой Windows Forms, в методе InitializeComponent (), создается новый экземпляр этого класса OpenFileDialog и устанавливаются указанные свойства. Щелкните на символе + строки кода, сгенерированного Windows Forms Designer, а затем не символе + строки private void InitializeComponent (), чтобы увидеть следующий код: private void InitializeComponent () { this. textBoxEdit = new System.Windows .Forms .TextBox() ; this.mainMenu = new System.Windows.Forms.MenuStrip() ; this.miFile = new System.Windows.Forms.ToolStripMenuItem() ; this.miFileNew = new System.Windows.Forms.ToolStripMenuItem(); this.miFileOpen = new System.Windows.Forms.ToolStripMenuItem(); this.miFileSave = new System.Windows.Forms.ToolStripMenuItem(); this.miFileSaveAs = new System.Windows.Forms.ToolStripMenuItem(); this.dlgOpenFile = new System.Windows.Forms.OpenFileDialog(); // ... // // dlgOpenFile // this.dlgOpenFile.Filter = "Text Documents (*. txt) | *. txt|Wrox Documents" + "(*.wroxtext)|*.wroxtext|All Files|*.*"; this.dlgOpenFile.Filterlndex = 2; Конечно, происходящее в данном случае полностью совпадает с тем, чего можно было бы ожидать при перетаскивании на форму любого другого стандартного элемента управления, но при поддержке Windows Forms Designer мы смогли создать новый экземпляр класса OpenFileDialog и установить его свойства. Теперь диалоговое окно можно отобразить. Метод ShowDialog () отображает диалоговое окно File Open (Открытие файла) и возвращает кнопку, нажатую пользователем. Если пользователь нажимает что-либо кроме кнопки ОК, никакие другие действия выполнять не нужно. По этой причине проверка на соответствие значению DialogResult .0K выполняется в операторе if. Если пользователь отменяет диалоговое окно, ничего не должно происходить: if (dlgOpenFile.ShowDialog () == DialogResult.OK) { Затем получается выбранное имя файла, обращаясь к свойству FileName класса OpenFileDialog, и устанавливаем переменную-член f ileName в это значение. Именно это значение используется методом OpenFile (). Файл можно было бы открыть непосредственно, используя класс File, но поскольку мы уже располагаем методом OpenFile (), который открывает и считывает файл, мы используем следующий код: fileName = dlgOpenFile.FileName; OpenFile();
Глава 17. Использование обычных диалоговых окон 543 Теперь запустите программу SimpleEditor, показанную на рис. 17.10. На данный момент только пункты меню New и Open... являются действующими. Меню Save и Save As... будут реализованы в следующем разделе. "„ SimpleEditor Рис. 17.10. Приложение SimpleEdi tor с реализованными пунктами меню New и Open... Если выбрать пункт меню File^Open, откроется диалоговое окно OpenFileDialog (рис. 17.11) и можно будет выбрать файл. Скорее всего, у вас нет файлов с расширением . wroxtext. До сих пор возможности сохранять файлы не было, поэтому можно в диалоговом окне редактора выбрать другой тип файла для открытия или переименовать какой-то текстовый файл, присвоив ему расширение .wroxtext. [ «Open QjXj 1 i. « яирисжог > ып Favorite Links f Documents £ Recently Changed <£ Recent Places ■ Desktop ДР Computer Ш Pictures ф Music More » Folders л Nam, • Ими Fieoame ВИВ L ► Debug " | *f 11 Sean* Date modrtVd Type Sfae No items match your search. - |hto—i 1 *■" ыш- »! ** 1 rt.fi*) J 1 JM.. I 1 ^ Рис. 17.11. Диалоговое окно OpenFileDialog, отображающееся в результате выбора пункта меню F/'/e^Open Выберите текстовый файл, щелкните на кнопке Open, и текст отобразится в текстовом поле диалогового окна. Как видно на рис. 17.12, был выбран пример текстового файла Thinktecture. txt в локальной системе.
544 Часть II. Программирование для Windows [ %> Simple EdUoT" "ПЁ5ЖННП 1 Е» We have speaakzed n hdprtg developers and archtects create dotrtxied appbcabons whrch perform and * I I scale as necessary Our ckents work n some of the most chalengng enwonments of software deveiopmert today We have recognized ri the past that a short penod • maybe only a few days • of addtenal support. tramng or revwwrtg n the earfy phases of a project can make a big deference for Is success In the past five years we have helped a number of dfierent projects to start qucker an^J to avoid cntical problems nght from the start Рис. 17.12. Загрузка текстового файла Thinktecture. txt На данный момент возможно только чтение существующих файлов. Было бы замечательно, если бы можно было создавать новые файлы и изменять существующие. Для этого мы используем класс SaveFileDialog. SaveFileDialog Класс SaveFileDialog во многом подобен классу OpenFileDialog, и они имеют ряд общих свойств. Этот раздел посвящен свойствам, характерным для диалогового окна Save, а также различиям в использовании общих свойств. Заголовок диалогового окна Свойство Title позволяет устанавливать заголовок диалогового окна, подобно тому, как это имеет место в классе OpenFileDialog. Если никакое значение этого свойства не установлено, заголовок, используемый по молчанию — Save As (Сохранить как). Расширения файлов Расширения файлов служат для связывания файлов с приложениями. Рекомендуется всегда добавлять расширение к файлу — в противном случае Windows не будет знать, какое приложение следует использовать для открытия файла, а со временем эта информация, скорее всего, забудется. AddExtension — булевское свойство, которое автоматически добавляет расширение к имени файла, введенному пользователем. Значение этого свойства, используемое по умолчанию — true. Если пользователь вводит расширение файла, никакое дополнительное расширение не добавляется. Таким образом, если значение свойства AddExtension установлено равным true и пользователь вводит имя файла test, приложение сохранит файл под именем test.txt. Если же пользователь введет имя файла test.txt, файл по-прежнему будет сохраняться под именем test.txt,aHetest.txt.txt. Свойство Def aultExt определяет расширение файла, которое используется, если пользователь его не вводит. Если оставить значение этого свойства пустым, будет применяться расширение файла, определенное выбранным в данный момент свойством Filter. Если оба свойства Filter и Def aultExt установлены, Def aultExt используется независимо от выбранного значения Filter. Проверка допустимости Свойства ValidateNames, CheckFileExists и CheckPathExists служат для автоматической проверки допустимости имени файла, аналогично соответствующим свойствам класса OpenFileDialog. Различие между классами OpenFileDialog и SaveFileDialog состоит в том, что используемое по умолчанию значение свойства CheckFileExists класса SaveFileDialog — false, что означает возможность ввода имени совершенно нового сохраняемого файла.
Глава 17. Использование обычных диалоговых окон 545 Перезапись существующих файлов Как было показано, проверка допустимости имен файлов выполняется аналогично тому, как это делается в классе OpenFileDialog. Однако класс SaveFileDialog требует большего объема проверки и установки ряда дополнительных свойств. Если значение свойства Create Prompt установлено равным true, приложение запрашивает пользователя о необходимости создания нового файла. Если значение свойства OverwritePrompt установлено равным true, пользователь получает запрос о том, действительно ли нужно выполнить перезапись существующего файла. Используемое по умолчанию значение свойства OverwritePrompt — true, а свойства CreatePrompt — false. При использовании этих значений диалоговое окно, показанное на рис. 17.13, отображается при попытке пользователя сохранить уже существующий файл. Рис. 17.13. Диалоговое окно с запросом о перезаписи файла Свойства класса SaveFileDialog Свойства класса SaveFileDialog приведены на рис. 17.14. Title InitialDirectory FileName - -Filename Untitled S*ve«lype [T«tDocum««C-tX) Filter, Filterlndex Puc. 17.14. Свойства класса SaveFileDialog
546 Часть II. Программирование для Windows В следующем практическом занятии мы добавим диалоговое окно Save File (Сохранение файла) в пример приложения. .актическое занятие Добавление и использование класса SaveFileDialog 1. Так же как мы добавили в форму диалоговое окно OpenFileDialog, в нее можно добавить диалоговое окно SaveFileDialog. Выберите компонент SaveFileDialog в панели инструментов Toolbox и перетащите его на серую область Forms Designer. Измените имя на dlgSaveFile, FileName на Untitled (Без названия), Filter Index на 2, а значение свойства Filter — на следующую строку, как это ранее было выполнено для диалогового окна OpenFileDialog. (Поскольку этот редактор будет допускать сохранение файлов только с расширениями . txt и . wroxtext, маска файла * . * будет опущена.) Text Document (*.txt)|*.txt|Wrox Document (*.wroxtext)|*.wroxtext 2. Добавьте обработчик события Click пункта меню Save As с именем OnFileSaveAs. Следующий код будет отображать диалоговое окно SaveFileDialog с помощью метода ShowDialog (). Как и в случае диалогового окна OpenFileDialog, результаты его выполнения представляют интерес только, если пользователь щелкает на кнопке ОК. (Мы вызываем метод SaveFile (), который сохраняет файл на диске. Этот метод реализован на следующем шаге.) private void OnFileSaveAs(object sender, EventArgs e) { if (dlgSaveFile.ShowDialog() = DialogResult.OK) { fileName = dlgSaveFile.FileName; SaveFile(); } } 3. Добавьте в файл метод SaveFile (): private void SaveFile () { try { File.WriteAllText(fileName, textBoxEdit.Text); } catch (IOException ex) { MessageBox.Show(ex.Message, "Simple Editor", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } } Подобно методу OpenFileO, в данном случае мы используем класс File. Статический метод WriteAll () записывает строку в файл. Первый параметр определяет имя файла, а второй — строку, которая должна быть записана в файл. Классы, используемые для доступа к файлам, подробнее описаны в главе 24. 4. После компоновки проекта запустите приложение (выберите в Visual Studio команду меню Debug^Start (Отладка1^ Пуск)). Запишите какой-нибудь текст в текстовом поле, а затем выберите пункт меню File^Save As (рис. 17.15).
Глава 17. Использование обычных диалоговых окон 547 Рис. 17.15. Выбор пункта меню File^Save As Откроется диалоговое окно SaveFileDialog, показанное на рис. 17.16. Теперь можно сохранить файл и открыть его снова для внесения других изменений. ».errt« Link.-. J Documents More >» FoWm ,. bC0Oturp I tooVCSharp ,. Chaptarl7 i staple it i. bin ё MJSL a File доле Untitled StvtMlypc -• Pwc. 17.16. Диалоговое окно SaveFileDialog 5. Теперь можно выполнять операцию Save As (Сохранить как), но простая операция Save пока недоступна. Добавьте обработчик события Click пункта меню Save и добавьте следующий код: private void OnFileSave(object sender, EventArgs e) { if (fileNaxne = "Untitled") { OnFileSaveAs(sender, e) ; ) else { SaveFile() ,
548 Часть II. Программирование для Windows Описание полученных результатов Меню Save должно обеспечивать сохранение файла без открытия какого-либо диалогового окна с единственным исключением из этого правила: если пользователь создает новый документ, но не указывает имя файле, обработчик меню Save должен работать подобно обработчику меню Save As, отображая диалоговое окно Save File. Переменная-член filename позволяет легко проверить, открыт ли файл, либо имя файла все еще остается установленным в изначальное значение Untitled после создания нового документа. Если оператор if возвращает значение true, программа вызывает обработчик OnFileSaveAs (), ранее реализованный для меню Save As. Если же файл был открыт и пользователь выбирает меню Save, поток управления передается в блок else. При этом можно использовать тот же метод Save File (), который был реализован ранее. В Notepad, Word и других Windows-приложениях имя редактируемого в текущий момент файла отображается в заголовке приложения. В следующем практическом занятии мы добавим также и эту функциональную возможность. Установка заголовка формы 1. Создайте новую функцию-член SetFormTitle () и добавьте следующую ее реализацию: private void SetFormTitle () { Filelnfo filelnfo = new Filelnfo(flleName); Text = filelnfo.Name + " - Simple Editor"; } Класс Filelnfo служит для получения хранящегося в переменной fileName имени файла без префикса пути. Класс Filelnfo освещен в главе 24. 2. После установки переменной-члена fileName добавьте обращение к этому методу в обработчики OnFileNew (), OnFileOpen () и OnFileSaveAs (), как показано в следующих фрагментах кода: private void OnFileNew(object sender, System.EventArgs e) { fileName = "Untitled"; SetFormTitle(); textBoxEdit.Clear(); } private void OnFileOpen(object sender, System.EventArgs e) { if (dlgOpenFile.ShowDialogO == DialogResult.OK) { fileName = dlgOpenFile.FileName; SetFormTitle(); OpenFile(); private void OnFileSaveAs(object sender, System.EventArgs e) {
Глава 17. Использование обычных диалоговых окон 549 if (dlgSaveFile.ShowDialogO == DialogResult.OK) { fileName = dlgSaveFile.FileName; SetFormTitle(); SaveFileO ; } } Описание полученных результатов При каждом изменении имени файла свойство Text реальной формы изменяется на имя файла, к которому добавлено имя приложения. Теперь работа приложения начинается с открытия экрана, показанного на рис. 17.17, на котором, как следует из заголовка формы, выполняется редактирование файла sample. txt. Итак, теперь мы располагаем простым редактором, который позволяет открывать, создавать и сохранять файлы (а также их редактировать). Значит ли это, что работа завершена? В действительности, нет. Поскольку офисы, полностью обходящиеся без печатных документов, пока не существуют, в приложение нужно добавить определенные возможности печати. [ %> S«iiplt.txt Stan* Editor ЕЖНвП 1 Е» I ТЫ « а wmpte Не Л Рис. 17.17. Начальное окно приложения SimpleEdi tor Печать При печати приходится заботиться о множестве нюансов, таких как выбор принтера, установка параметров страниц и выполнение многостраничной печати. Классы пространства имен System. Drawing. Printing предоставляют огромную поддержку выполнения этих задач и позволяют без труда выполнять печать документов из приложений пользователей. Прежде чем приступить к рассмотрению класса PrintDialog, который позволяет выбирать принтер, кратко рассмотрим выполнение печати в среде каркаса .NET. В основе печати лежит класс PrintDocument, имеющий метод Print (), который начинает последовательность вызовов, завершающуюся вызовом метода OnPrintPage (), который отвечает за передачу выходных данных принтеру. Однако прежде чем изучать реализацию кода печати, вначале рассмотрим классы печати каркаса .NET. Архитектура печати Основные части архитектуры печати и взаимосвязь между классами и некоторыми свойствами и методами показаны на рис. 17.18.
550 Часть II. Программирование для Windows PrintDialog +ShowDialog() +Reset() PrinterSettings ^4 +lnstalledPrinters PageSetupDialog +ShowDialog() +Reset() PageSettings +PaperSize +PaperSource +Margins PrintDocument +Print() #OnBeginPrint()| #OnEndPrint() #OnPrintPage() PrintController #OnStartPrint() #OnStartPage() #OnEndPage() #OnEndPrint() +Print() +PrintLoop() PrintPageEventArgs ♦HasMorePages +PageBounds +MarginBounds Graphics +DrawString() +DrawLine() +DrawPath() Рис. 17.18. Архитектура печати Рассмотрим функциональные возможности этих классов. □ Класс PrintDocument является наиболее важным. На рис. 17.18 видно, что почти все остальные классы связаны с ним. Чтобы выполнить печать документа, нужно располагать экземпляром класса PrintDocument. Последовательность действий по печати, запускаемая этим классом, рассмотрена в следующем разделе. □ Класс PrintController управляет потоком задачи печати. Контроллер печати содержит события для запуска печати, для обработки каждой страницы и для завершения печати. Этот класс является абстрактным, поскольку реализация обычной печати отличается от реализации предварительного просмотра печатного документа. Конкретные классы, производные от PrintController — StandardPrintController и PreviewPrintController. Описания методов Print () и PrintLoop () отсутствуют в документации, поскольку эти методы являются внутренними методами сборки и могут вызываться только другими классами данной сборки, такими как класс PrintDocument. Однако эти методы помогут разобраться в процессе печати, и поэтому они нашли свое отражение в этой главе. □ Класс PrinterSettings может получать и устанавливать такие параметры конфигурации принтера, как двусторонняя печать, альбомная или книжная ориентация и количество копий. □ Класс PrintDialog содержит параметры выбора принтера для печати и способа конфигурирования PrinterSettings. Этот класс, подобно другим уже описанным классам диалоговых окон, является производным от CommonDialog. □ Класс PageSettings определяется размеры и поля страницы печати и то, является ли она черно-белой или цветной. Конфигурирование этого класса можно выполнять посредством класса PageSetupDialog, производным от CommonDialog.
Глава 17. Использование обычных диалоговых окон 551 Последовательность печати Теперь, когда вы узнали о ролях классов в архитектуре печати, рассмотрим основную последовательность процесса печати. Действия основных компонентов — приложения, экземпляра класса PrintDocument и класса PrintController — во времени показаны на рис. 17.19. Приложение должно вызвать метод Print () класса PrintDocument. Это запускает последовательность печати. Поскольку сам класс PrintDocument не отвечает за поток печати, задание передается классу PrintController посредством вызова метода Print () этого класса. PrintController предпринимает необходимое действие и информирует PrintDocument о начале печати, вызывая метод OnBeginPrint (). Если приложение должно выполнить какие-либо действия при запуске задания, необходимо зарегистрировать обработчик события в классе PrintDocument, чтобы соответствующая информация была доступна в классе приложения. В ситуации, изображенной на рис. 17.19, предполагается, что мы зарегистрировали обработчик OnBeginPrint (). Поэтому этот обработчик вызывается из класса PrintDocument. После завершения начального этапа PrintController выполняет вход в метод PrintLoop (), вызывая метод OnPrintPage () в классе PrintDocument для каждой печатаемой страницы. OnPrintPage () вызывает все обработчики события PrintPage. Такие обработчики нужно реализовать в каждом случае, иначе ничего не будет напечатано. На рис. 17.19 обработчик назван OnPrintPage (). Application +PrintDocument Печать OnBeginPrint() OnPrintPageO OnEndPrintO +PrintController Печать OnBeginPrint() OnPrintPageO OnStartPrintO OnStartPage() Метод PrintLoop() ^ повторяет свои действия для каждой страницы PrintLoopQ' OnEndPageQ OnEndPrintO OnEndPrintO Рис. 17.19. Выполнение печати
552 Часть II. Программирование для Windows После того, как последняя страница напечатана, PrintController вызывает метод OnEndPrint () в классе PrintDocument. При желании можно реализовать также обработчик, который будет вызваться на этом этапе. Главное, что следует запомнить — это то, что код печати можно реализовать в обработчике события PrintDocument. PrintPage. Этот обработчик будет вызываться для каждой страницы, предназначенной для печати. При наличии кода, который должен вызываться только один раз в рамках задания печати, нужно реализовать обработчики событий BeginPrint и EndPrint. Событие PrintPage Как вы уже знаете, необходимо реализовать обработчик события PrintPage. Делегат PrintPageEventHandler определяет аргументы обработчика: public delegate void PrintPageEventHandler(object sender, PrintPageEventArgs e); Как видно из этого фрагмента кода, мы получаем объект типа PrintPageEventArgs. Для ознакомления с основными свойствами этого класса, связанного с классами PageSettings и Graphics, обратитесь к схеме классов. Первый из этих двух классов позволяет устанавливать размеры и поля печатного листа, а также получать информацию об устройстве от принтера. И наоборот, класс Graphics позволяет получать доступ к контексту принтера и отправлять на принтер такие данные, как строки, линии и кривые. GDI (Graphics Device Interface — интерфейс графических устройств) позволяет выполнять вывод графических данных на таком устройстве, как жран или принтер. Технология прорисовки GDI+, используемая в каркасе .NET, представляет собой следующее поколение GDI, предлагая такие дополнительные функциональные возможности, как градиентные кисти и альфа-сопряжение. Прорисовка посредством интерфейса GDI+ и класса Graphics подробнее описана в главе 30. Если на этом этапе печать представляется вам сложной задачей, не беспокойтесь! Следующий пример должен вас убедить, что добавление возможностей печати в приложение — достаточно простая задача. Прежде чем добавлять диалоговое окно PrintDialog, необходимо добавить в приложение ряд пунктов меню печати. Добавьте в приложение SimpleEditor два разделителя и элементы меню Print (Печать), Print Preview (Просмотр печати), Page Setup (Параметры страницы) и Exit (Выход). Свойства Name и Text и методы обработчиков новых элементов меню представлены в табл. 17.4. Таблица 17.4. Значения свойств Name и Text и методы обработчиков новых элементов меню Имя элемента меню Свойство Text Обработчик miFilePrint &Print OnFilePrint miFilePrintPreview Print Pre&view OnFilePrintPreview miFilePageSetup Page Set&up OnFilePageSetup miFileExit E&xit OnExit Теперь меню должно выглядеть, как показано на рис. 17.20.
Глава 17. Использование обычных диалоговых окон 553 Save As Prtat-. Print Prevww. Pag« Setup- Exit J Рис. 17.20. Расширение меню File В следующем практическом занятии мы добавим в свое приложение функции печати, добавив в него компонент PrintDocument. Практическое зан Добавление компонента PrintDocument 1. Добавьте следующие директивы using в начальную часть кода, чтобы можно было использовать классы печати: using System.Drawing; using System.Drawing.Printing; 2. Перетащите компонент PrintDocument из панели инструментов Toolbox на серую область, расположенную под формой. Измените значение свойства Name на printDocument и добавьте обработчик события OnPrintPage к событию PrintPage, выбирая кнопку Events в окне Properties. Затем добавьте следующий код в реализацию обработчика события: private void OnPrintPage(object sender, PrintPageEventArgs e) { string[] lines = textBoxEdit.Text.Split('\n'); int i = 0; foreach (string s in lines) { } lines[i++] = s.TrimEnd('\r'); int x = 20; int у = 20; foreach (string line in lines) { e.Graphics.DrawString(line, new Font ("Arial" , 10), Brushes. Black, x, у) ; у += 15; } 3. Добавьте к событию Click меню Print обработчик, вызывающий метод Print () класса PrintDocument. При отсутствии допустимого принтера генерируется исключение типа InvalidPrinterException, которое перехватывается для отображения сообщения об ошибке: private void OnFilePrint(object sender, EventArgs e) try {
554 Часть II. Программирование для Windows printDocument.Print(); ) catch (InvalidPrinterException ex) { MessageBox. Show (ex .Message, "Simple Editor", MessageBoxButtons.OK, MessageBoxIcon.Error); } } Теперь можно скомпоновать и запустить приложение и напечатать документ. Естественно, чтобы пример работал, в системе должен быть установлен принтер. Описание полученных результатов Метод Print () объекта printDocument с помощью класса PrintController вызывает событие Print Page: printDocument.Print() ; В обработчике OnPrintPage () с помощью метода String. Split () и символа новой строки \п мы разделяем текст в текстовом поле на строки. Результирующие строки записываются в массив lines типа string: string[] lines = textBoxEdit.Text.Split('\nf) ; В зависимости от способа создания текстового файла, строки могут разделяться не только символом новой строки \п, но и символом возврата \г. Метод TrimEndO класса String удаляет символ \г из каждой строки: int i = 0; foreach(string s in lines) { lines[i++] = s.TrimEnd('\r'); } Второй оператор foreach в следующем коде выполняет итерацию по строкам и посредством обращения к е. Graphics . DrawString () отправляет каждую из них на принтер, е — переменная типа PrintPageEventArgs, а свойство Graphics связано с контекстом принтера. Контекст принтера позволяет выполнять прорисовку на устройстве печати. Класс Graphics предлагает ряд методов для выполнения прорисовки в этот контекст. Пока мы еще не можем выбирать принтер, поэтому приложение использует принтер, установленный по умолчанию (сведения о котором хранятся в системном реестре Windows). Для осуществления печатного вывода в методе DrawString () мы используем шрифт Arial размером в 10 пунктов и черную кисть. Позиция вывода определяется переменными х и у. Позиция по горизонтали фиксирована и равна 20 пикселям, а позиция по вертикали увеличивается с каждой строкой: int х = 20; int у = 20; foreach (string line in lines) { e. Graphics.DrawString (line, new Font("Arial", 10), Brushes.Black, x, y); У += 15; }
Глава 17. Использование обычных диалоговых окон 555 Пока следующие проблемы остаются нерешенными в коде печати. □ Многостраничная печать не работает. Если печатный документ состоит из нескольких страниц, на печать выводится только первая из них. Желательно было бы также обеспечить печать верхнего колонтитула (например, имени файла) и нижнего колонтитула (например, номера страницы). □ Значения границ страницы жестко определены в программе. Чтобы предоставить пользователям возможность устанавливать другие значения границ страницы мы используем класс PageSetupDialog. □ Выходные данные печати отправляются на принтер, заданный по умолчанию, устанавливаемый пользователем через панель управления. Предпочтительнее было бы, чтобы пользователь мог выбирать принтер в приложении. Для исправления этой ситуации мы используем класс PrintDialog. □ Приложение применяет фиксированный шрифт. Чтобы предоставить пользователю возможность выбора шрифта, можно использовать класс FontDialog, который подробнее описан в последующих разделах. Итак, продолжим реализацию процесса печати для устранения указанных недостатков. Печать нескольких страниц Событие PrintPage вызывается для каждой печатаемой страницы. В следующем практическом занятии мы информируем PrintController о том, что текущая напечатанная страница не была последней страницей, устанавливая значение свойства HasMorePages класса PrintPageEventArgs равным true. Изменение метода OnPrintPageO для печати нескольких страниц 1. Вначале объявите переменную-член lines типа string!] и переменную linesPrinted типа int в классе SimpleEditorForm: // Переменные для печати private string[] lines; private int linesPrinted; 2. Измените обработчик OnPrintPageO. В предыдущей реализации метода OnPrintPage () текст был разбит на строки. Поскольку метод OnPrintPage () вызывается с каждой страницей, и разбиение на строки требуется выполнить только один раз при запуске операции печати, удалите весь код из этого метода и замените его новой реализацией: private void OnPrintPage(object sender, PrintPageEventArgs e) { int x = 20; int у = 20; while (linesPrinted < lines.Length) { e.Graphics.DrawString (lines[linesPrinted++], new Font ("Arial", 10), Brushes.Black, x, y) ; у +- 15; if (у = e. PageBounds. Height - 80) {
556 Часть II. Программирование для Windows е. HasMorePages = true; return; } ) linesPrinted = 0; e.HasMorePages = false; } 3. Добавьте обработчик OnBeginPrint к событию BeginPrint объекта printDocument. Обработчик OnBeginPrint вызывается только один раз для каждого задания печати, и в нем нужно создать массив lines: private void OnBeginPrint(object sender, PrintEventArgs e) { lines = textBoxEdit.Text.Split('\n'); int i = 0; foreach (string s in lines) { lines[i++] = s.TrimEnd('\r'); } } 4. Добавьте обработчик события OnEndPrint к событию EndPrint объекта print Document. Здесь можно освободить ресурсы, выделенные в методе OnBeginPrint. В данном примере мы выделили массив строк и обратились к нему через ссылку на переменную lines. В методе OnEndPrint ссылка переменной lines устанавливается в значение null, поэтому сборщик мусора может очистить строковый массив: private void OnEndPrint(object sender, PrintEventArgs e) { lines = null; } 5. После компоновки проекта можно запустить задание печати для многостраничного документа. Описание полученных результатов Запуск задания печати с помощью метода Print () класса PrintDocument ведет к последовательному однократному вызову метода OnBeginPrint () и вызову метода OnPrintPage () для каждой страницы. В методе OnBeginPrint () текст, помещенный в текстовое поле, разбивается на массив строк. Каждая строка в массиве представляет одну строку, поскольку мы выполняем разбиение по символу новой строки (\п) и удаляем символ возврата каретки (\г), как и ранее: lines = textBoxEdit.Text.Split('\n'); int i = 0; foreach (string s in lines) { lines[i + + ] = s.TrimEnd('\r') ; } Метод OnPrintPage () вызывается после метода OnBeginPrint (). Печать должна продолжаться до тех пор, пока количество напечатанных строк остается меньше числа строк, предназначенных для печати. Свойство lines.Length возвращает количество строк в массиве lines. Значение переменной linesPrinted увеличивается на единицу с каждой строкой, отправленной на принтер:
Глава 17. Использование обычных диалоговых окон 557 while (linesPrinted < lines.Length) { e.Graphics.DrawString(lines[linesPrinted++], new Font("Arial", 10), Brushes.Black, x, y) ; После выполнения печати строки мы проверяем, не лежит ли заново вычисленная позиция по вертикали вне границ страницы. Кроме того, мы уменьшаем размеры границ на 80 пикселей, поскольку, в частности, нежелательно, чтобы печать выполнялась до самого края бумаги, т.к. многие принтеры в любом случае не могут выполнять такую печать. По достижении этой позиции значение свойства На sMo re Pages класса PrintPageEventArgs устанавливается равным true, чтобы поставить контроллер в известность о том, что метод OnPrintPage () должен быть вызван снова для другой предназначенной для печати страницы — вспомните, что объект PrintController содержит метод PrintLoop (), определяющий последовательность действий для каждой печатаемой страницы и прекращающий свое выполнение, если значение свойства HasMorePages равно false. (Значение этого свойства по умолчанию — false, что ведет к печати только одной страницы.) у += 15; if (у = е.PageBounds.Height - 80) е.HasMorePages return; true; Диалоговое окно PageSetupDialog В настоящий момент поля страницы жестко закодированы. Изменим приложение, чтобы пользователи могли устанавливать поля страниц. Для выполнения этой задачи доступен другой класс диалогового окна: PageSetupDialog. Этот класс позволяет конфигурировать размеры и источники бумаги, ориентацию и поля печатных страниц. А поскольку эти параметры зависят от используемого принтера, данное диалоговое окно позволяет также выбирать принтер. Основные свойства, которые активизируют или отключают конкретные параметры этого диалогового окна, а также свойства, применяемые для доступа к значениям, приведены на рис. 17.21. Эти свойства описаны в последующих разделах. AllowPaper- AllowOrientation - -Paper Source — Ortertebon ♦ Portra* Marojnt ЩШШЛШЩ — Left 10 lop Ю ВО* Ю Bottom 10 I <* ) Г<**И - PageSettings.PaperSize - PageSettings.PaperSource -AllowMargins - MinMargins PageSettings.Margins Рис. 17.21. Основные свойства диалогового окна PageSetupDialog
558 Часть II. Программирование для Windows Бумага Значение true свойства AllowPaper означает, что пользователи могут выбирать размеры и источник бумаги. Свойство PageSetupDialog.PageSettings .PaperSize возвращает экземпляр объекта PaperSize, свойства которого Height, Width и PaperName содержат информацию соответственно о высоте, ширине и названии формата бумаги. Свойство PaperName специфицирует такие имена, как Letter или А4. Свойство Kind возвращает перечисление, из которого можно извлечь значение перечисления PaperKind. Перечисление PaperKind состоит из значений множества различных видов бумаги, которые определяют размеры бумаги, такие как A3, А4, А5, Letter, LetterPlus и LetterRotated. Свойство PageSetupDialog.PageSettings.PaperSource возвращает экземпляр PaperSource, содержащий информацию об источнике бумаги принтера и подходящем для него типе бумаги (при условии правильной установки настроек принтера). Поля Установка значения свойства AllowMargins равным true позволяет пользователям устанавливать значение полей печатной страницы. Указывая значение свойства MinMargins, можно определить минимальные значения, которые сможет вводить пользователь. Информация о полях хранится в свойстве PageSetupDialog. PageSettings.Margins. Возвращенный объект Margins (Поля) содержит свойства Bottom (Нижнее), Left (Левое), Right (Правое) и Тор (Верхнее). Ориентация Свойство AllowOrientation определяет, могут ли пользователи осуществлять выбор между печатью в книжной и альбомной ориентации. Выбранное значение можно выяснить, запрашивая значение свойства PageSetupDialog.PageSettings. Landscape, которое является булевским значением, специфицирующим альбомный режим, если оно равно true, и книжный — если false. Принтер Свойство Allow Printer определяет, могут ли пользователи выбирать принтер. В зависимости от значения этого свойства кнопка Printer активизирована (true) или отключена (false). В свою очередь, обработчик этой кнопки открывает диалоговое окно PrintDialog, которое мы будем использовать следующим. В следующем практическом занятии мы добавим в приложение возможность конфигурирования параметров страницы для печати. актическое занятие! Добавление диалогового окна PageSetupDialog 1. Перетащите компонент PageSetupDialog из панели инструментов Toolbox на форму в окне Forms Designer. В качестве значения свойства Name установите dlgPageSetup, а в качестве значения свойства Document — printDocument, чтобы связать диалоговое окно с документом, который нужно напечатать. 2. Добавьте обработчик события Click к пункту меню Page Setup (Параметры страницы) и добавьте следующий код для отображения диалогового окна с помощью метода ShowDialog (). Возвращаемое значение метода ShowDialog () здесь можно не проверять, поскольку реализация обработчика события Click кнопки ОК уже устанавливает новые значения в соответствующем объекте PrintDocument:
Глава 17. Использование обычных диалоговых окон 559 private void OnFilePageSetup(object sender, EventArgs e) { dlgPageSetup.ShowDialog(); } 3. Теперь измените реализацию метода OnPrintPage (), чтобы использовать поля, установленные объектом PageSetupDialog. В коде мы устанавливаем значения переменных х и у в соответствии со значениями свойств MarginBounds .Left и MarginBounds.Top класса PrintPageEventArgs. Проверьте поля страницы с помощью свойства MarginBounds .Bottom: private void OnPrintPage(object sender, PrintPageEventArgs e) { int x = e.MarginBounds.Left; int у = e. MarginBounds. Top; while (linesPrinted < lines.Length) { e.Graphics.Drawstring(lines[linesPrinted++], new Font("Arial", 10), Brushes.Black, x, y) ; У += 15; if (y > = e.MarginBounds.Bottom) { e.HasMorePages = true; return; linesPrinted = 0; e.HasMorePages = false; 4. Теперь можно скомпоновать проект и запустить приложение. Выбор пункта меню File^Page Setup (Параметры страницы) ведет к открытию диалогового окна, изображенного на рис. 17.22. Оно позволяет изменять поля и выполнять печать с установленными полями. Рис. 17.22. Диалоговое окно PageSetupDialog, отображаемое после выбора пункта меню File^Page Setup
560 Часть II. Программирование для Windows Класс PrintDialog Класс PrintDialog позволяет пользователям выбирать принтер из числа установленных, количество копий и некоторые параметры принтера вроде ориентации и источника бумаги. Поскольку использование класса PrintDialog очень просто, в следующем практическом занятии мы сразу добавим диалоговое окно PrintDialog в приложение SimpleEditor. практическое занятие Добавление диалогового окна PrintDialog 1. Перетащите элемент управления PrintDialog из панели инструментов Toolbox на форму. В качестве значения свойства Name установите dlgPrint, а в качестве значения свойства Document — printDocument. Измените реализацию обработчика события Click меню Print следующим образом: private void OnFilePrint(object sender, EventArgs e) { try { } if (dlgPrint.ShowDialogO = DialogResult.OK) { printDocument.Print(); } catch (InvalidPrinterException ex) { MessageBox.Show(ex.Message, "Simple Editor", MessageboButtons.OK, MessageBoxIcon.Error); 2. Скомпонуйте и запустите приложение. Выбор пункта меню F\\e^>Print приводит к открытию диалогового окна PrintDialog. Теперь можно выбрать принтер, чтобы напечатать документ, как показано на рис. 17.23. - Print Select Pnrter % Add Printer 4QB Microsoft Oil •Р» Microsoft XP ^HPOftV HP OfficeJet 5600 series on trestunas « Statu» Reedy Location 1 Stock Comment Laser Pirter Page Range Sweets mjtw г*# Pages . Prrttotte fbjrbv or £op<es 1 т- i/jCoUta i pt )Гс—ё"Г ь* '1 Рис. 17.23. Выбор принтера для печати
Глава 17. Использование обычных диалоговых окон 561 Параметры диалогового окна PrintDlalog В приложении SimpleEditor мы не изменяли ни одного из свойств объекта PrintDialog, но это диалоговое окно также обладает рядом параметров. Диалоговое окно, изображенное на рис. 17.23, содержит три группы параметров: Printer (Принтер), Page Range (Диапазон страниц) и Copies (Количество копий). □ В группе Printer можно выбирать не только принтер, но и опцию Print to File (Печатать в файл). По умолчанию эта опция активизирована, но не отмечена флажком. Выбор этого флажка позволяет пользователю записывать печатный вывод в файл, а не выводить на принтере. Эту опцию можно отключить, устанавливая значение свойства AllowPrintToFile равным false. □ Если пользователь выбирает эту опцию, обращение к printDocument. Print () ведет к открытию диалогового окна, показанного на рис. 17.24, которое запрашивает имя файла для записи печатного вывода. Output Fie Name 1 OK | fcSS 1 Puc. 17.24. Окно печати в файл □ В разделе Page Range для выбора доступна только опция АН (Все) — по умолчанию опции Pages (Страницы) и Selection (Выделенный фрагмент) отключены. Реализация этих опций описана в следующем разделе. □ Группа Copies позволяет пользователю выбирать количество копий, которые должны быть напечатаны. Печать выбранного текста Установка значения свойства AllowSelection равным true позволяет пользователям печатать выбранный текст. Но для этого нужно изменить код печати так, чтобы печатался только выбранный текст. Эта функциональная возможность будет добавлена в следующем практическом занятии. практическое занятие Добавление выборки для печати 1. Добавьте выделенный код в обработчик Click кнопки Print: private void OnFilePrint(object sender, EventArgs e) { try { if (textBoxEdit.SelectedText != "") { dlgPrint.AllowSelection = true; } if (dlgPrint.ShowDialogO == DialogResult .OK) { printDocument.Print (); } } catch (InvalidPrinterException ex)
562 Часть II. Программирование для Windows MessageBox.Show(ex.Message, "Simple Editor", MessageboButtons.OK, MessageBoxIcon.Error); } } 2. В этой программе все строки, которые будут напечатаны, устанавливаются в обработчике OnBeginPrint (). Измените реализацию этого метода: private void OnBeginPrint (object sender, PrintEventArgs e) { char[] param = {'\n!}; if (dlgPr int. Pr inter Settings. PrintRange = Pr in tRange. Selection) { lines = textBoxEdit.SelectedText.Split(param); } else { lines = textBoxEdit.Text.Split(param); } int i = 0; char[] trimParam = {'\r'}; foreach (string s in lines) { lines[i++] = s.TrimEnd(trimParam); } } 3. Теперь можно скомпоновать и запустить приложение. Откройте файл, выберите какой-нибудь текст, откройте диалоговое окно из меню File,=>Print и выберите опцию Selection (Выделенный фрагмент) из группы Page Range. При выборе этой опции щелчок на кнопке Print будет приводить к печати только выбранного текста. Описание полученных результатов Значение AllowSelection устанавливается равным true только при наличии какого-то выбранного текста. Прежде чем диалоговое окно PrintDialog будет отображено, необходимо выполнить проверку для определения наличия какого-либо выбранного текста. Если это так, значение свойства SelectedText — не null. Если какой-либо текст выбран, значение свойства AllowSelection устанавливается равным true: if (textBoxEdit.SelectedText != "") { dlgPrint.AllowSelection = true; } Метод OnBeginPrint () вызывается при запуске каждого задания печати. Обращение к свойству printDialog. PrinterSettings . PrintRange позволяет выяснить, выбрал ли пользователь опцию Selection. Свойство PrintRange принимает значение из перечисления PrintRange: AllPages (Все страницы), Selection (Выбранный фрагмент) или SomePages (Некоторые страницы): if (printDialog.PrinterSettings.PrintRange == PrintRange.Selection) { Если выбранная опция — действительно PrintRange.Selection, мы получаем выбранный текст из свойства SelectedText объекта TextBox. Эта строка разбивается на части так же, как полный текст: lines = textBoxEdit.SelectedText.Split(param); }
Глава 17. Использование обычных диалоговых окон 563 Печать диапазонов страниц Печать диапазона страниц может быть реализована так же, как печать выделенного фрагмента. Кнопку переключателя можно активизировать, устанавливая значение свойства AllowSomePages равным true, что позволяет пользователям выбирать диапазон страниц для печати. Однако каков диапазон страниц в приложении SimpleEditor? Какая страница является последней? Последняя страница определяется свойством PrintDialog. PrinterSettings. ToPage. Откуда пользователю известно, какие номера страниц нужно печатать? В таких приложениях обработки документов, как Microsoft Word, пользователи могут выбирать опцию Print Layout (Разметка страниц) для просмотра на экране текста с отображением номеров страниц. Простой элемент управления TextBox, используемый в SimpleEditor, не отображает номера страниц, поэтому данную функциональную возможность мы не станем реализовывать в приложении. Конечно, возможность печати диапазона страниц можно было бы реализовать в качестве упражнения. Для этого значение свойства AllowSome Pages должно быть установлено в true. Перед отображением диалогового окна PrintDialog можно также установить значение свойства PrinterSettings . FromPage равным 1, а значение PrinterSettings .ToPage — равным максимальному номеру страниц. Свойства класса PrintDialog Свойства, рассмотренные в этом разделе, и их влияние на внешний вид диалогового окна PrintDialog показаны на рис. 17.25. PrinterSettings. PrintRange AllowSelection AllowPrintToFile, PrintToFile PrinterSettings.Copies PrinterSettings. FromPage, ToPage Puc. 17.25. Свойства класса PrintDialog Предварительный просмотр Опцию меню Print Preview (Предварительный просмотр) можно применять для предоставления пользователям возможности просмотра документа в том виде, каком он будет напечатан. В среде .NET реализация опции Print Preview не представляет сложности — для предварительного просмотра документа внутри формы в том виде, каком он будет напечатан, можно использовать класс PrintPreviewControl. Класс
564 Часть II. Программирование для Windows PrintPreviewDialog представляет собой диалоговое окно, которое является оболочкой элемента управления. Класс PrintPreviewDialog Если ознакомиться со свойствами и списком наследования в MSDN-документации класса PrintPreviewDialog, легко выяснить, что в действительности он представляет собой класс Form и не является оболочкой для обычного диалогового окна — этот класс является производным от System.Windows . Forms .Form и с ним можно работать, как с формами, созданными в главе 15. В следующем практическом занятии мы добавим класс PrintPreviewDialog в приложение SimpleEditor. фительного просмотр, в Microsoft Word и WordPad отлича- ^WSfcWtf-ftfcffS*-***. , : *П" * ... tf , _ -5 ', ..^ .;! -£.:-1%*У-iffr •"■ 1ШШ^т^ШШМЩ11Ш jviewDialog тем, что в этих приложениях _ ^ ■ ■ — совещенном диалоговом о™, а в пивно* Добавление класса Pr intPreviewDialog 1. Добавьте компонент PrintPreviewDialog из панели инструментов Toolbox в рабочую область Windows Forms Designer. Установите dlgPrintPreview в качестве значения свойства Name, a printDocument в качестве значения свойства Document. 2. Добавьте и реализуйте обработчика события Click пункта меню Print Preview: private void OnFilePrintPreview(object sender, EventArgs e) { dlgPrintPreview.ShowDialog(); } 3. Запустите приложение, чтобы увидеть окно предварительного просмотра печати, показанное на рис. 17.26. Рис. 17.26. Диалоговое окно предварительного просмотра печати Класс PrintPreviewControl Работа функции предварительного просмотра в Microsoft Word и WordPad отличается от работы в PrintPreviewDialog тем, что в этих приложениях содержимое просмотра отображается не в собственном диалоговом окне, а в главном приложения.
Глава 17. Использование обычных диалоговых окон 565 Для достижения такого же эффекта класс PrintPreviewControl можно поместить на форму. Объект printDocument должен быть установлен в качестве свойства Document, а значение свойства Visible должно быть установлено равным false. Когда требуется отобразить предварительный просмотр, значение свойства Visible достаточно установить в true. Тогда элемент управления PrintPreviewControl окажется перед остальными элементами управления, как показано на рис. 17.27. Рис. 17.27. Элемент управления Print Pre vi e wCon trol Как видно из заголовка и единственного пункта меню File, в данном случае отображается главное окно приложения SimpleEditor. Но для выполнения увеличения, печати и одновременного отображения нескольких страниц текста в элемент управления класса PrintPreviewControl нужно добавить ряд элементов. Для обеспечения доступа к этим функциональным возможностям можно использовать специальную панель инструментов. Как видно из четырехстраничного предварительного просмотра, показанного на рис. 17.28, эти функциональные возможности уже реализованы в классе PrintPreviewDialog. Рис. 17.28. Четырехстраничный предварительный просмотр Диалоговые окна FontDialog и ColorDialog Последние диалоговые окна, которые будут рассмотрены в этой главе — FontDialog и ColorDialog.
566 Часть II. Программирование для Windows В данной главе эти диалоговые окна рассмотрены с точки зрения установки шрифта и цвета. Подробно классы Font и Color освещены в главе 30. FontDialog FontDialog позволяет пользователям выбирать шрифт, включая его стиль, размер и цвет. Свойства, которые изменяют поведение элементов в диалоговом окне, приведены на рис. 17.29. AllowVectorFonts AllowVerticalFonts FixedPitchOnly ShowEffects ShowColor AllowScriptChange Рис. 17.29. Свойства класса Fon tDialog Использование диалогового окна FontDialog Это диалоговое окно можно использовать так же, как предыдущее. В окне Windows Forms Designer компонент диалогового окна можно перетащить из панели инструментов Toolbox на форму, что приведет к созданию экземпляра FontDialog. Код, использующий класс FontDialog, подобен следующему: if (dlgFont.ShowDialogO == DialogResult.OK) { textBoxEdit.Font = dlgFont.Font; } Диалоговое окно FontDialog отображается вызовом метода ShowDialogO. Если пользователь щелкает на кнопке ОК, метод возвращает значение DialogResult .ОК. Выбранный шрифт может быть прочитан в свойстве Font класса FontDialog. Затем этот шрифт передается свойству Font объекта TextBox. Свойства класса FontDialog Вы уже видели на рис. 17.29 свойства класса FontDialog, а использование этих свойств описано в табл. 17.5. ShowApply MinSize MaxSize
Глава 17. Использование обычных диалоговых окон 567 Таблица 17.5. Свойства класса FontDialog Свойство Описание AllowVectorFonts AllowVerticalFonts FixedPitchOnly MaxSize MinSize ShowApply ShowColor ShowEffects AllowScriptChange Булевское значение, которое определяет возможность выбора векторных шрифтов в списке шрифтов. Значение этого свойства, используемое по умолчанию — true Булевское значение, которое определяет возможность выбора вертикальных шрифтов в списке шрифтов. Вертикальные тексты используются в дальневосточных странах. Вполне вероятно, что вертикальный шрифт не установлен в вашей системе. Значение этого свойства, используемое по умолчанию — true Установка этого свойства ведет к отображению в списке только шрифтов с фиксированной высотой. В таком шрифте все символы имеют одинаковый размер по высоте. Значение этого свойства, используемое по умолчанию — false Значение этого свойства определяет максимальный размер шрифта, который может выбирать пользователь Подобно свойству MaxSize, свойство MinSize определяет минимальный размер шрифта, который может выбирать пользователь Если кнопка Apply (Применить) должна отображаться, значение свойства ShowApply должно быть установлено равным true. Нажимая кнопку Apply, пользователи могут наблюдать обновление шрифта в приложении, не закрывая диалоговое окно шрифта По умолчанию опция выбора цвета не отображается в диалоговом окне. Если требуется, чтобы пользователи могли выбирать цвет шрифта в диалоговом окне шрифта, значение этого свойства должно быть установлено равным true По умолчанию пользователи могут устанавливать флажки Strikeout (Зачеркнутый) и Underline (Подчеркнутый) для манипулирования шрифтом. Если отображение этих опций нежелательно, установите значение свойства ShowEffects равным false Установка значения свойства AllowScriptChange равным false препятствует изменению пользователями начертания шрифта. Доступные начертания зависят от выбранного шрифта. Например, шрифт Arial поддерживает начертания Western (Латиница), Hebrew (Иврит), Arabic (Арабский), Greek (Греческий), Turkish (Турецкий), Baltic (Балтийский), Central European (Центрально-европейский), Cyrillic (Кириллица) и Vietnamese (Вьетнамский) Активизация кнопки Apply В отличие от рассмотренных ранее диалоговых окон, FontDialog поддерживает кнопку Apply (Применить), которая по умолчанию не отображается. Если пользователь нажимает кнопку Apply, диалоговое окно остается открытым, но выбранный шрифт будет применен. Выбирая элемент управления FontDialog в Windows Forms Designer, значение свойства ShowApply в окне Properties можно установить равным true. Но каким образом становится известно о нажатии кнопки Apply пользователем? Диалоговое окно остается открытым, поэтому метод ShowDialog () не выполнит возврат. Вместо этого можно добавить обработчик к событию Apply класса FontDialog. Для этого щелкни-
568 Часть И. Программирование для Windows те на кнопке Events (События) в окне Properties (Свойства) и введите имя метода обработчика события Apply. Как видно из следующего кода, я ввел имя OnApplyFontDialog. В этом обработчике доступ к выбранному шрифту диалогового окна FontDialog можно получить, используя переменную-член класса FontDialog: private void OnApplyFontDialog(object sender, System.EventArgs e) { textBoxEdit.Font = dlgFont. Font; ColorDialog Класс ColorDialog предоставляет значительно меньше возможностей конфигурирования, чем FontDialog, и позволяет конфигурировать дополнительные цвета, если ни один из основных цветов не подходит. Для этого используют свойство AllowFullOpen. Часть диалогового окна, предназначенную для конфигурирования дополнительных цветов можно также автоматически раскрывать с помощью свойства FullOpen. Если значение свойства AllowFullOpen установлено равным false, значение свойства FullOpen будет игнорироваться. Свойство SolidColorOnly ограничивает выбор только чистыми (сплошными) цветами. Свойство CustomColors можно применять для получения и установки значений конфигурируемых нестандартных цветов. Диалоговое окно установки цвета и свойства, которые влияют на него, можно видеть на рис. 17.30. FullOpen AllowFullOpen- Qustomcotoa гггггггг ггггггггг Qeftne Curtwp Coton С*поЫ ■ Низ |160 Bed 0 Sal 0 &mn 0 u**tS*J U*n 0 Bye О CustomColors Рис. 17.30. Свойства класса ColorDialog Использование диалогового окна ColorDialog Элемент управления ColorDialog можно перетащить из панели инструментов Toolbox на поверхность Windows Forms Designer, как это делалось с другими диалоговыми окнами. Метод ShowDialog () отображает диалоговое окно до тех пор, пока пользователь не нажмет кнопку ОК или Cancel. Прочитать выбранный цвет можно с помощью свойства Color объекта диалогового окна, как показано в следующем фрагменте кода:
Глава 17. Использование обычных диалоговых окон 569 if (dlgColor.ShowDialogO == DialogResult.OK) { textBoxEdit.ForeColor = dlgColor.Color; Свойства класса ColorDlalog Свойства, используемые для изменения внешнего вида диалогового окна, кратко описаны в табл. 17.6. Таблица 17.6. Свойства класса ColorDialog Свойство Описание AllowFullOpen FullOpen AnyColor CustomColors SolidColorOnly Установка значения этого свойства равным false отключает кнопку Define Custom Colors (Определение нестандартных цветов), тем самым препятствуя определению нестандартных цветов пользователями. Значение этого свойства, используемое по умолчанию — true Установка значения свойства FullOpen равным true перед отображением диалогового окна ведет к его открытию с автоматически развернутой опцией выбора нестандартного цвета Установка значения этого свойства равным true ведет к отображению всех доступных цветов в списке основных цветов Это свойство позволяет заранее определять массив нестандартных цветов и считывать нестандартные цвета, определенные пользователем При установке значения этого свойства равным true пользователь может выбирать только сплошные цвета Диалоговое окно FolderBrowserDialog FolderBrowserDialog — простое диалоговое окно, предназначенное для получения имен каталогов от пользователя или создания каталогов. Лишь несколько свойств доступно для конфигурирования этого диалогового окна, показанного на рис. 17.31. Description Browse For Fotoer В сын Public ;■ Computer »> Network .*} Control Panel * RecydeBIn RootFolder ShowNewFolderButton Puc. 17.31. Свойства класса FolderBrowserDialog
570 Часть II. Программирование для Windows Свойство Description (Описание) можно использовать для определения текста, который будет отображаться над древовидным представлением. Свойство Root Folder определяет папку, с которой должен начинаться поиск папок пользователем. Это свойство позволяет устанавливать значение из перечисления Environment. SpecialFolder. Свойство ShowNewFolderButton определяет, разрешено ли пользователю создавать новую папку посредством диалогового окна. Использование диалогового окна обзора папок Элемент управления FolderBrowserDialog можно перетащить из панели инструментов Toolbox на поверхность Windows Forms Designer, как мы делали это с другими диалоговыми окнами. Метод ShowDialog () отображает диалоговое окно до тех пор, пока пользователь не нажмет кнопку ОК или Cancel. Считывание выбранного пользователем пути можно выполнять, обращаясь к свойству SelectedPath, как показано в следующем фрагменте кода: dlgFolderBrowser.Description = "Select a directory"; if (dlgFolderBrowser.ShowDialog() == DialogResult.OK) { MessageBox.Show("The folder " + dlgFolderBrowser.SelectedPath + " was selected"); } Свойства диалогового окна обзора папок Свойства, влияющие на поведение диалогового окна, кратко описаны в табл. 17.7. Таблица 17.7. Свойства класса FolderBrowserDialog Свойство Описание Description Свойство Description позволяет определять текст, который отображается над древовидным представлением диалогового окна RootFoider Свойство RootFolder позволяет устанавливать путь, с которого должен начинаться обзор SelectedPath Свойство SelectedPath возвращает путь к каталогу, выбранному пользователем ShowNewFolderButton Установка значения свойства ShowNewFolderButton равным true предоставляет пользователю возможность создания новой папки Резюме Эта глава была посвящена использованию классов диалоговых окон в приложениях. Мы рассмотрели, как открывать и сохранять файлы, а после ознакомления с классами печати .NET Framework вы научились добавлять в свои приложения возможности печати. Подводя итоги, напомним, что в приложении SimpleEditor были использованы следующие классы диалоговых окон. □ OpenFileDialog — позволяет пользователям открывать файл. □ FileSaveDialog — служит для запроса имени файла для сохранения данных. □ PrintDialog — служит для определения конфигураций принтера и параметров печати. □ PageSetupDialog — служит для изменения полей печатной страницы.
Глава 17. Использование обычных диалоговых окон 571 □ PrintPreviewDialog — служит для просмотра печатной страницы, что позволяет пользователям заранее видеть, как она будет выглядеть. □ FolderBrowserDialog — служит для выбора и создания каталогов. В этой главе были также описаны основные свойства классов Font Dial од и Col or Dial од. В приведенных ниже упражнениях вы можете расширить функциональные возможности приложения SimpleEditor, включив в него и эти диалоговые окна. В следующей главе показано, как Windows-приложения, подобные SimpleEditor, могут быть развернуты в целевых системах с помощью технологии ClickOnce и программы установки Windows. Упражнения Поскольку диалоговые окна FontDialog и ColorDialog работают подобно другим рассмотренным в этой главе диалоговым окнам, их добавление в приложение SimpleEditor — задача несложная. 1. Предоставьте пользователям возможность изменения шрифта, используемого в текстовом поле. Добавьте в главное меню новый пункт меню F&ormat, а в него подменю &Font.... Добавьте обработчик для этого элемента меню. Затем с помощью Windows Forms Designer добавьте в приложение диалоговое окно FontDialog. Реализуйте отображение этого диалогового окна в обработчике элемента меню и установите свойство Font текстового поля в соответствии с выбранным шрифтом. Необходимо также изменить реализацию метода OnPrintPage (), чтобы он использовал выбранный шрифт для печатного вывода. В предыдущей реализации мы создали новый объект Font в методе Drawstring () объекта Graphics. Теперь же используйте шрифт объекта textBoxEdit, обращаясь к его свойству Font. Помните о проблеме расположения шрифта при выборе пользователем большого размера шрифта. Во избежание частичного перекрытия расположенных одна над другой строк измените фиксированное значение, использованное для изменения вертикальной позиции строк. Для достижения этой цели лучше всего применять размер шрифта для изменения шага по вертикали: используйте свойство Height класса Font. 2. Еще одним прекрасным усовершенствованием приложения было бы изменение цвета шрифта. Добавьте меню Format второе подменю: Color.... Добавьте к этому пункту меню обработчик, который открывает диалоговое окно ColorDialog. Если пользователь нажимает кнопку ОК, установите значение свойства ForeColor текстового поля в качестве выбранного цвета диалогового окна ColorDialog. В методе OnPrintPage () обеспечьте, чтобы выбранный цвет использовался только в том случае, если принтер поддерживает цветную печать. Для проверки, поддерживает ли принтер цветную печать, воспользуйтесь свойством PageSettings. Color аргумента PrintPageEventArgs. Объект кисти с цветом, выбранным для текстового поля, можно создать с помощью следующего кода: Brush brush = new SolidBrush(textBoxEdit.ForeColor); Затем эту кисть можно применять в качестве аргумента в методе DrawString () вместо ранее использованной черной кисти.
18 Развертывание Windows-приложений Существует несколько способов развертывания Windows-приложений. Простые приложения можно устанавливать с помощью общей утилиты развертывания хсору, но для установки сотен клиентских приложений этот вид развертывания не подходит. Для такой ситуации существует две возможности: применение развертывания ClickOnce либо программы установки Microsoft Windows. При развертывании ClickOnce инсталляция приложения осуществляется щелчком на ссылке на некоторый Web-сайт. В ситуациях, когда пользователь должен выбрать каталог для установки приложения или когда требуется изменение каких-либо записей системного реестра, следует применять развертывание с помощью программы установки Windows. В этой главе освещены оба варианта инсталляции Windows-приложений. В частности, будут рассмотрены следующие вопросы. □ Основы развертывания. □ Развертывание ClickOnce. □ Типы проектов развертывания и установки Visual Studio. □ Функциональные средства программы установки Windows. □ Создание пакетов программы установки Windows с помощью Visual Studio 2008. Обзор процесса развертывания Развертывание — это процесс инсталляции приложений в целевых системах. Традиционно такая инсталляция осуществлялась с помощью программы установки. 1ри необходимости инсталляции ста или тысячи клиентских приложений процесс ( тановки может занимать очень много времени. Для облегчения этой задачи системой администратор может создать пакетные сценарии, автоматизирующие выпол-
Глава 18. Развертывание Windows-приложений 573 нение необходимых действий. Однако особенно если логика приложения регулярно изменяется, возможно возникновение проблем, связанных с клиентами, которые не имеют доступа к сети, а также с несовместимостью различных версий библиотек. В результате возникает ситуация так называемого ада DLL (DLL hell). "Ад DLL' описывает проблемы, которые возникают, когда каждое из нескольких установленных приложений требует свою версию одной и той же DLL. Если одно приложение устанавливает DLL, которая замещает собой другую версию этой же DLL, приложения, нуждающиеся в замещенной DLL, могут оказаться функционировать. В связи с этими проблемами многие компании преобразуют свои внутрисетевые приложения в Web-приложения, несмотря на то, что Windows-приложения предлагают интерфейс пользователя с существенно большими возможностями. Web-приложения нужно развернуть только на сервере, а клиент автоматически получает новейший интерфейс пользователя. В среде .NET ситуация "ада DLL' предотвращается за счет использования приватных и общедоступных сборок. Приватные сборки копируются с каждым приложением, что полностью исключает возможность возникновения конфликта со сборками из других приложений. Сборки общего использования имеют строгие имена, которые включают номер версии. Несколько версий одной и той же сборки могут сосуществовать в одной системе. Среда .NET 1.0 поддерживала технологию, получившую название бесконтактного развертывания. В случае применения бесконтактного развертывания пользователи получали возможность автоматически инсталлировать приложение, щелкая на ссылке на Web-странице. Однако бесконтактное развертывание .NET 1.0 было связано с рядом проблем безопасности и не обладало многими функциональными средствами, необходимыми для множества клиентских приложений — что, в общем-то, и понятно, поскольку это была первая версия технологии. Означенные проблемы были решены в технологии развертывания ClickOnce, реализованной в .NET 2.O. Подобно бесконтактному развертыванию, развертывание ClickOnce позволяет устанавливать приложение посредством щелчка на ссылке внутри Web-страницы. Пользователь клиентской системы не нуждается в административных привилегиях, поскольку приложение инсталлируется в каталоге, выделенном данному пользователю. Применение технологии ClickOnce позволяет устанавливать приложения со сложным интерфейсом пользователя. Приложение инсталлируется на компьютере клиента, поэтому по завершении установки необходимость сохранения соединения с клиентской системой отпадает. Иначе говоря, приложение может использоваться в автономном режиме. Итак, теперь пиктограмма приложения доступна в меню Start (Пуск), проблемы безопасности легче поддаются решению, а приложение может быть легко удалено. Замечательная особенность технологии ClickOnce состоит в том, что обновления могут выполняться автоматически при запуске клиентского приложения или в фоновом режиме во время его работы. Однако развертывание ClickOnce сопряжено с рядом ограничений: эта технология не может применяться, если компоненты совместного использования требуется установить в глобальном кэше сборок, если приложение нуждается в СОМ-компонентах, которые требуют изменений в системном реестре, или если пользователи должны иметь возможность выбора каталога для инсталляции приложения. В подобных случаях следует использовать программу установки Windows, предлагающую традиционный способ инсталляции Windows-приложений. Однако прежде чем приступить к работе с пакетами программы установки Windows, рассмотрим развертывание ClickOnce.
574 Часть II. Программирование для Windows Развертывание ClickOnce При использовании технологии развертывания ClickOnce запуск программы установки в клиентской системе не требуется. Пользователю клиентской системы достаточно щелкнуть на ссылке на Web-странице, и приложение будет автоматически установлено. По завершении установки приложения клиентская система может работать в автономном режиме — ей не требуется доступ к серверу, с которого приложение было установлено. Установка ClickOnce может быть выполнена с Web-сайта, точки доступа UNC совместного использования или места хранения файла (например, компакт-диска). При использовании технологии ClickOnce приложение устанавливается в клиентской системе, доступно посредством ярлыков быстрого доступа меню Start (Пуск) и может быть удалено из системы через диалоговое окно Add/Remove Programs (Установка и удаление программ). Развертывание ClickOnce описывается файлами манифестов. Манифест приложения описывает приложение и требуемые им разрешения. Манифест развертывания описывает конфигурацию развертывания, такую как политики обновления. В практических примерах раздела, посвященного технологии развертывания ClickOnce, мы выполним конфигурирование ClickOnce-развертывания простого редактора, который был создан в главе 17. Для успешного развертывания сборки по сети требуется, чтобы манифест, использованный при установке, обладал сертификатом, который указывает пользователю, устанавливающему приложение, организацию, которая создала программу установки. В следующем практическом занятии мы создадим сертификат, связанный с манифестами развертывания ClickOnce. Практическое занятие Подписание Манифестов ClickOnce 1. Откройте в Visual Studio пример приложения SimpleEditor, созданный в главе 17. Если это не было сделано, создайте приложение самостоятельно, скопировав его из загружаемых файлов. 2. В окне Solution Explorer (Проводник решений) выберите Properties (Свойства) проекта, а затем выберите вкладку Signing (Подписание), показанную на рис. 18.1. Application Build Bu*Jt*MH Vgnth.ChckOnc. Resourai S«fMM Setting* Rcftrtftctftttr4 i Sifl/wno £> S»gnth«»»w»o% S«cunty Puofcth Cod* AIM** . ■I ■ i t. 0>tUK)Qtb'f Рис. 18.1. Вкладка Signing окна свойств проекта
Глава 18. Развертывание Windows-приложений 575 3. Установите флажок Sign the ClickOnce Manifests (Подписать манифесты ClickOnce). 4. Щелкните на кнопке Create Test Certificate (Создать тестовый сертификат), чтобы создать тестовый сертификат, связанный с манифестами ClickOnce. В ответ на запрос введите пароль для сертификата. Этот пароль необходимо запомнить для выполнения последующих настроек. Затем щелкните на кнопке ОК. 5. Щелкните на кнопке More Details (Дополнительные сведения), чтобы ознакомиться с информацией о сертификате (рис. 18.2). ^j^————i—————— in |. „ [Ч. I сттш» "I QH Certtficate МолмЬоп ТЬ» СА Hoot cetVtKMe to not tmterf. To епаЫе trust. ■tstal Нм cwtiftcat* ID the Treated Root Certiftcatton Author**?* «tore. lMM«4toe fv«bove£hre teaucdby: fvatoove&ra VaMfrwn 01.10.2007 to 30.09 2008 f You have • private hey that correspond» to th* certAcate j Learn more about £"crtr*Mtd J* Рис. 18.2. Просмотр информации о сертификате Описание полученных результатов Сертификат предназначен для того, чтобы пользователь, устанавливающий приложение, мог идентифицировать создателя установочного пакета. Ознакомившись с сертификатом, пользователи могут решать, можно ли доверять данному пакету установки с точки зрения требований безопасности. Только что созданный тестовый сертификат не предоставляет пользователю реальную информацию о степени доверия, и, как будет показано далее, при его использовании пользователь получает предупреждение о том, что ему нельзя доверять. Такой сертификат предназначен только для тестирования. Прежде чем приложение будет готово к развертыванию, придется получить реальный сертификат от такого центра сертификации, как VeriSign. Если приложение развертывается только внутри локальной сети организации, сертификат можно получить также с локального сервера сертификации, если таковой установлен. Сервер сертификации Microsoft Certificate Server может быть установлен в системе Windows Server 2003 или Windows Server 2008. При наличии такого сертификата, его можно конфигурировать, щелкая на кнопке Select from File (Выбрать из файла) на вкладке Signing. В следующем практическом занятии мы выполним конфигурирование требований к разрешениям сборки. При установке сборки в клиентской системе необходимо подтверждение требуемых разрешений.
576 Часть II. Программирование для Windows Практическое зам ятие Определение требований к разрешениям 1. В окне Solution Explorer выберите пункт Properties для проекта, перейдите на вкладку Security (Безопасность), показанную на рис. 18.3, а затем установите флажок Enable ClickOnce Security Settings (Активизировать параметры безопасности ClickOnce). t»»t /о«д Ок*0пе* «epiKttion tquim * or» Щ Cntoif CIKkO<K< Stcunty ittnnqt Tbii it «p»rti* trust м ■ - Рис. 18.3. Вкладка Security окна свойств проекта 2. Установите переключатель This Is A Partial Trust Application (Это приложение заслуживает частичного доверия). 3. Выберите зону (Custom), поскольку приложение требует особых разрешений безопасности. 4. Щелкните на кнопке Calculate Permissions (Вычислить разрешения), чтобы вычислить разрешения, необходимые приложению. 5. Если в результате вычисления разрешение FilelOPermission не будет включено в число выбранных, измените настройки, чтобы его включить. Описание полученных результатов Безопасность — важный аспект приложений .NET. Для определения действий, разрешенных сборкам, каркас .NET использует систему безопасности, основанную на свидетельствах. С одной стороны, ресурсы должны быть защищены. Примерами таких ресурсов являются файлы и каталоги, сетевые порты и переменные среды. Для всех этих ресурсов существуют разрешения .NET, которые предоставляют доступ к ним. Один из таких примеров — класс FilelOPermission, который можно использовать для предоставления доступа ко всей файловой системе или к конкретным файлам и каталогам. С другой стороны, необходимо определить, кому разрешено использовать эти ресурсы. В данном случае сборки сгруппированы по различным категориям, таким как установленные локально и загруженные из сети. Можно также определить категорию сборок, созданных конкретным изготовителем. Для получения более подробной информации о безопасности на основе свидетельств обратитесь к книге "С# 2008 и платформа .NET3.5 для профессионаловп (ИД "Вильяме", 2009г.).
Глава 18. Развертывание Windows-приложений 577 Кнопка Calculate Permissions на вкладке свойств Security в Visual Studio запускает анализ кода, используемого приложением, для проверки требований к разрешениям приложения. Результат этого анализа — манифест приложения, включающий все требуемые разрешения. При использовании Visual Studio 2008 в разделе Properties окна Solution Explorer можно найти манифест приложения, названный арр.manifest. Содержимое этого файла имеет следующий вид: <?xml version=.0" encoding="utf-8"?> <asmvl:assembly manifestVersion="l.0" xmlns="urn:schemas-microsoft-com:asm.vl" xmlns:asmvl="urn:schemas-microsoft-com:asm.vl" xmlns:asmv2="urn:schemas- microsof t-com:asm.v2" xmlns:xsi="http://www.w3.org/200l/XMLSchema-instance"> <assemblyldentity version="l.0.0.0" name="MyApplication.app" /> <trustInfo xmlns="urn:schemas-microsoft-comrasm.v2"> <security> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <!-- UAC Manifest Options If you want to change the Windows User Account Control level replace the requestedExecutionLevel node with one of the following. Если хотите изменить уровень Windows User Account Control (Управление учетной записью пользователя Windows, замените узел requestedExecutionLevel одним из следующих. <requestedExecutionLevel level="asInvoker" /> <requestedExecutionLevel level="requireAdministrator" /> <requestedExecutionLevel level="highestAvailable" /> If you want to utilize File and Registry Visualization for backward compatibility then delete the requestedExecutionLevel node. Если для обеспечения обратной совместимости хотите использовать функцию File and Registry Virtualization (Виртуализация файлов и реестра), удалите узел requestedExecutionLevel. --> <requestedExecutionLevel level="asInvoker" /> </requestedPrivileges> <applicationRequestMimmum> <defaultAssemblyRequest permissionSetReference="Custom" /> <PermissionSet class="System.Security.PermissionSet" version="l" ID="Custom" SameSite="site"> <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="l" Unrestricted="true" /> <IPermission class="System.Security.Permissions.ReflectionPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="l" Unrestricted="true" /> <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="l" Flags="UnmanagedCode, Execution, ControlEvidence" /> <IPermission class="System.Security.Permissions.UIPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version=Ml" Window="AllWindows" Clipboard="OwnClipboard" /> <IPermission class="System.Security.Permissions.KeyContainerPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="l" Unrestricted-"true" /> </PermissionSet> </applicationRequestMinimum> </security> </trustInfo> </asmvl:assembly>
578 Часть II. Программирование для Windows XML-элемент <applicationRequestMinimum> определяет все необходимые разрешения приложения. Класс FilelOPermission требуется, т.к. приложение выполняет чтение и запись файлов, используя классы из пространства имен System. 10. Выбирая другую зону в диалоговом окне Security, можно проверить, нуждается ли приложение в разрешениях, которые недоступны внутри данной зоны. После того как требования безопасности определены, можно приступить к публикации приложения, создав манифест развертывания. Это легко выполнить с помощью мастера публикации Publish Wizard, как показано в следующем практическом занятии. Дополнительные параметры конфигурирования публикации 1. Выберите вкладку Publish (Публикация) в окне свойств проекта. Щелкните на кнопке Options (Параметры), чтобы открыть диалоговое окно Publish Options (Параметры публикации), показанное на рис. 18.4. Введите имя программного продукта, имя издателя, URL-адрес страницы поддержки и адрес Web-страницы развертывания. 2. Сконфигурируйте параметры обновления, щелкнув на кнопке Updates (Обновления) и установив флажок The Application Should Check for Updates (Приложение должно проверять наличие обновлений), как показано на рис. 18.5. "Ш Publisher name Wro* Press Product rjame Simple Editor Support URl httpi/V'www.wro». со ж Deployment geb page publish.htm И Automatically generate deployment web page after every pubHsh J fipen deployment web page after publish U L_! Blocfc application from being activated via a URl И ЙЙ Use 'deploy' file extension | Allow yRL parameters to be passed to application P '■!'; For CD installations, automatically start Setup when CO is inserted В ±*vtif'"s uploaded to a web server !*] Use application manifest for trust information 1 <* li Рис. 18.4. Диалоговое окно Publish Options DEEX. V The application should check for updates Choose when the application should check for updates: After the application starts Choose this option to speed up application start time. Updates will not be installed until the next time the application is run. # Before the application starts Choose this option to ensure that users who are connected to the network always run with the latest updates. Specify how f»eou<n»:> the dedication should ihccK. foi updates ф Che'i evti> tunt :».< application Jarfs) p) Pj Specify a minimum required version for this application Major Update location frf different than publish location» -«L _^^L. Рис. 18.5. Установка флажка The Application Should Check for Updates Практическое занятие Использование мастера публикации 1. Запустите Publish Wizard (Мастер публикации), выбрав пункт меню Builds Publish SimpleEditor (Компоновка1^Опубликовать SimpleEditor). Введите путь к Web- сайту http: //localhost/SimpleEditor, как показано на рис. 18.6. Щелкните на кнопке Next (Далее).
Глава 18. Развертывание Windows-приложений 579 Publish Wizard Where do you want to publish the application? Specify the location to publish this application: http7/k>calhost/SimpleEdrtor| You may publish the application to a web site, FTP server, or tile path. Examples: Disk path, c:\deploy\myapplication File share \*servermyapplication FTP server ftp://ttp.microsoft.com/myapplicatton Web site: http:' www.microsoft.com/myapplication Puc. 18.6. Ввод пути к Web-сайту Чтобы опубликовать приложение на Web-сервере в системе Windows Vista, среда разработки Visual Studio 2008 должна быть запущена в расширенном режиме с административными привилегиями. Сервер Internet Information Server (IIS) должен быть установлен. Если IIS не установлен, выберите публикацию в локальной файловой системе. 2. На втором шаге мастера Publish Wizard выберите Yes, this application will be available online or offline (Да, это приложение будет доступно по сети или автономно), как показано на рис. 18.7. Щелкните на кнопке Next. Publish Wizard ...МУ Will the »ppl i cation Ы ava.laol. off I in.? d* ca • Yes. this application is available online or offline A shortcut will be added to the Start Menu, and the application can be umnstalled via Add/Remove Programs. f • No, this application is only available online No shortcut will be added to the Start Menu. The application will be run directly from the publish location. DC Cancel Puc. 18.7. Второй шаг мастера Publish Wizard
580 Часть II. Программирование для Windows 3. Последний диалог отображает итоговую информацию, поскольку теперь мы готовы к выполнению публикации (рис. 18.8). Щелкните на кнопке Finish (Готово). Publish Wizard Ready to Publish! The mzara will now publish the application based on your choices Ф |The application will be published to http: tarabove/SimpleEdrtor/ When this application is installed on the client machine, a shortcut will be added to the Start Menu, and the application can be uninstalled via Add/Remove Programs. Finish _ Рис. 18.8. Все готово к публикации Описание полученных результатов Мастер Publish Wizard создает Web-сайт на локальном Web-сервере Internet Information Services. Сборки приложений (исполняемые файлы и библиотеки), а также манифесты приложения и развертывания, файл setup.exe и образец Web-страницы, publish.htm, копируются на Web-сервер. Как показано в этом разделе, манифест развертывания описывает информацию установки. При использовании Visual Studio Solution Explorer манифест развертывания можно открыть, открывая файл SimpleEditor.application в окне Solution Explorer. Связь с манифестом приложения описывается XML-элементом <dependentAssembly>: <assemblyldentity name="SimpleEditor.application" version="l.0.0.0" publicKeyToken=4fe8381193333a9" language="en-US" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.vl" /> description asmv2:publisher="Wrox Press" asmv2:product="SimpleEditor" asmv2:supportUrl="http://www.wrox.com" xmlns="urn:schemas-microsoft-com:asm.vl" /> <deployment install="true" mapFileExtensions="true"> <subscription> <update> <beforeApplicationStartup /> </update > </subscription> </deployment> <dependency> <dependentAssembly dependencyType=" install" codebase= "^plication Files\SimpleEditor_l_0_0_0\SinyleEditor. exe.manifest" size="8241"> <assemblyIdentity name= "SimpleEditor.exe" version="l. 0.0.0" publicKeyToken=" 7815ea8568895047" language="neutral" processorArchitecture="msil" type="Win32" />
Глава 18. Развертывание Windows-приложений 581 <hash> <dsig:Transforms> <dsig:Transform Algorithm= "urn: schemas-microsoft-com:HashTransforms. Identity" /> </dsig:Transforms > <dsig:DigestMethod Algorithm= "http://www.w3.org/2000/09/xmldsig#shal" /> <dsig: DigestValue> yMedux2GteUPg/c/LbS9WgXXeqs= </dsig:DigestValue> </hash> </dependentAssembly> </dependency> Осуществляя выбор, как показано на рис. 18.7, мы указываем, что приложение будет доступно по сети и автономно. В результате приложение инсталлируется в клиентской системе и становится доступным из меню Start. Кроме того, пользователи смогут удалять приложение, используя ярлык Add/Remove Programs панели инструментов. Если же вместо этого указать, что приложение должно быть доступно только по сети, для его загрузки с сервера и локального запуска пользователям придется всегда щелкать на ссылке Web-сайта. Файлы, принадлежащие приложению, определяются выводом проекта. Чтобы увидеть файлы приложения на вкладке Publish его диалогового окна Properties, щелкните на кнопке Application Files (Файлы приложения). Откроется диалоговое окно Application Files (рис. 18.9). Рис. 18.9. Диалоговое окно Application Files Компоненты, необходимые для работы приложения, определяются в диалоговом окне Prerequisites (Необходимые компоненты), которое показано на рис. 18.10 и доступно посредством щелчка на кнопке Prerequisites (Необходимые компоненты). Как видно из рисунка, для приложений .NET 3.5 необходимый компонент каркаса .NET Framework 3.5 обнаруживается автоматически. Это диалоговое окно позволяет выбирать другие необходимые компоненты. Теперь можно установить приложение, выполнив действия, описанные в следующем практическом занятии.
582 Часть II. Программирование для Windows [у! Create setup program to install prerequisite components Choose which prerequisites to install: . , ' J . jj Microsoft Data Access Components 2Л 5 NET Framework 2.0 D61 ЩVisual C- • Runtime Libraries (x86) 9 Windows Installer 31 9 .NET Framework 3 S 1 Crystal Reports for Visual Studio Codename 'Orcas' 5 Microsoft Visual Studio 2005 Report Viewer «1 1 1 J: == A Check Microsoft Update for more redistributable components Specify the install location for prerequisites • Download prerequisites from the component vendor s web site Download prerequisites from the same location as my application Download prerequisites from the following location: Puc. 18.10. Диалоговое окно Prerequisites практическое занятие Установка приложения 1. Откройте Web-страницу publish.htm, показанную на рис. 18.11. 0S QQ ;в-.м..а ШЯК _ а -я в - #'1^йр-в"*' Wrox Press Simple Editor Name: Simple Editor Version: 1.0.0.1 Publisher: Wrox Press The following prerequisites are required: • Windows Installer 3 1 • NET Framework 3.5 If these components are already metaled, you can launch the appbcatoon now. Otherwise, dick the button below to nstafl the prerequisites and run the application. ' % Local | \10OX Puc. 18.11. Web-страница publish, htm 2. Щелкните на кнопке Install (Установить), чтобы установить приложение. Откроется окно с предупреждением безопасности (рис. 18.12).
Глава 18. Развертывание Windows-приложений 583 [ Application Install - Security Warning Ц.. 1 1 1 Publisher cannot be verified. к-^Э^ Are you sure you want to install this application? Ир Name: Simple Editor From: tar above Publisher Unknown Publisher install ( DMllMtai | ^% While applications from the Internet can be useful, they can potentially harm your ^§7 computer, tf you do not trust the source, do not install this software. More Information.. t IJ Puc. 18.12. Окно с предупреждением безопасности 3. Щелкните на ссылке More Information (Дополнительные сведения), чтобы ознакомиться с проблемами безопасности, связанными с приложением. Прочтите категории предупреждений этого диалогового окна, показанного на рис. 18.13. ф Publisher The publisher of this application cannot be verified. Only run applications from publishers that you trust 9 Machine Access This application will gain access to additional resources on your computer, such as your file system or your network connection This may put your computer at nsk. Do not approve unless you trust the application's publisher. \j Installation This application will be installed on your computer. It will add a shortcut to your Start menu and will be added to the Add or Remove Programs list 9 Locabon This application comes from the Local Network. Only run applications from locations that you trust. Puc. 18.13. Дополнительные сведения, связанные с безопасностью 4. После ознакомления с информацией этого диалогового окна, щелкните на кнопке Close (Закрыть), а затем на кнопке Install диалогового окна Application Install (Установка приложения). Описание полученных результатов При открытии файла publish.htm целевое приложение проверяется на предмет наличия исполняющей среды .NET версии 3.5. Эта проверка выполняется JavaScript- функцией, встроенной в HTML-страницу. Если исполняющая среда отсутствует, она инсталлируется вместе с клиентским приложением. При использовании настроек публикации, установленных по умолчанию, исполняющая среда копируется на Web-сервер. Щелчок на ссылке установки приложения ведет к открытию манифеста развертывания для инсталляции приложения. Затем пользователь информируется о ряде возможных проблем безопасности, связанных с приложением. Если пользователь щелкает на кнопке ОК, приложение устанавливается.
584 Часть II. Программирование для Windows Обновления При использовании ранее сконфигурированных параметров обновления клиентское приложение автоматически выполняет проверку наличия новой версии на Web- сервере. В следующем практическом занятии мы рассмотрим такой сценарий на примере приложения SimpleEditor. Практическое заня Обновление приложения 1. Внесите в приложение SimpleEditor изменения, проявляющиеся немедленно — такие как цвет фона текстового поля. 2. Скомпонуйте приложение и щелкните на кнопке Publish Now (Опубликовать немедленно) в разделе Publish окна свойств проекта. 3. Не щелкайте на ссылке publish. htm Web-страницы; вместо этого запустите приложение из меню Start. После запуска приложения откроется диалоговое окно Update Available (Доступно обновление), показанное на рис. 18.14 и содержащее запрос о необходимости загрузки новой версии. Щелкните на кнопке ОК, чтобы загрузить новую версию. После запуска новой версии приложения его текстовое поле будет окрашено в выбранный цвет. ( II.,,. 1 Update Available ; Application update A new version of Simple Editor is available. Do you want to download it now? Name: S«4P*tbdrtof From: farabove 1 <ж LiMMHHMiMBHiHHi .11 € a* ► || Рис. 18.14. Диалоговое окно Update Available Описание полученных результатов Политика обновления определяется XML-элементом <update> в манифесте развертывания. Ее можно изменить, щелкая на кнопке Updates на вкладке настроек Publish. He забудьте, что доступ к настройкам Publish открывается из окна свойств проекта. Диалоговое окно Application Updates (Обновление приложения) показано на рис. 18.15. Используйте это диалоговое окно для указания того, должно ли клиентское приложение вообще искать доступные обновления. Если проверка наличия обновлений требуется, можно определить, должна ли проверка выполняться перед запуском приложения или в фоновом режиме во время его работы. Если обновление должно выполняться в фоновом режиме, можно указать временной интервал между обновлениями: с каждым запуском приложения или через указанное количество часов, дней или недель.
Глава 18. Развертывание Windows-приложений 585 ДОГ J The application should check for updates Choose when the application should check tor updates After the application starts Choose this option to speed up application start time. Updates will not be installed until the next time the application is run. • Before the application starts Choose this option to ensure that users who are connected to the network always run with the latest updates : daylsl H Specrfy a minimum required version for this application *| Update location (rf different than publish location): Puc. 18.15. Диалоговое окно Application Updates Типы проектов установки и развертывания Visual Studio Откройте диалоговое окно Add New Project (Добавить новый проект) в Visual Studio и в панели Project Types (Типы проектов) выберите тип Setup and Deployment (Установка и развертывание). Окно будет иметь вид, показанный на рис. 18.16. 3 Project types Web Office Database LINQ to XSD Preview Test WCF Workflow Database Projects Other Languages Other Project Types Setup and Deployment Database Extensibility T»<t Ргл)»г*< Templates v.sual Studio installed templates _л Setup Project i ^Merge Module Project : JP CAB Project И My Templates I ^Search Online Templates ч*. web Setup Project 3a Setup wizard $ Smart Device CAB Project - э(Н Create a Windows Installer project to which files can be added Name: Setupl Location: C:\BegVCSharp\Chapterl8\SimpleEdrtor LZal Puc. 18.16. Окно добавления нового проекта после выбора типа Setup and Deployment Типы проектов и предоставляемые ими возможности описаны в следующем списке. □ Шаблон Setup Project (Проект установки) — тот, который мы будем использовать. Этот шаблон применяется для создания пакетов программы установки Windows, поэтому его можно использовать для развертывания Windows-приложений.
586 Часть II. Программирование для Windows □ Шаблон Web Setup Project (Проект Web-установки) можно использовать для установки Web-приложений. Этот шаблон проекта используется в главе 23. □ Шаблон Merge Module Project (Проект модуля слияния) предназначен для создания модулей слияния программы установки Windows. Модуль слияния — это файл программы установки, который может включаться в несколько установочных пакетов программы установки Windows. Модуль слияния можно создать для его включения в пакеты установки компонентов, которые должны устанавливаться более чем одной установочной программой. Одним из примеров модуля слияния служит сама исполняющая среда .NET. Она поставляется в модуле слияния, поэтому может быть включена в пакет программы установки приложения. Мы используем модуль слияния в примере приложения. □ Мастер установки (Setup Wizard) — пошаговое средство выбора других шаблонов. Прежде всего, необходимо ответить на вопрос, нужно создать программу установки приложения или же распространяемый пакет. В зависимости от выбора будет создан пакет программы установки Windows, модуль слияния или САВ-файл. □ Шаблон Cab Project (Кабинетный проект) позволяет создавать кабинетные (CAB) файлы. САВ-файлы можно использовать для слияния нескольких сборок в один файл и его сжатия. Поскольку САВ-файлы могут быть сжаты, Web-клиент получает возможность загрузки файла меньшего размера с сервера. Создание компонентов выходит за рамки этой книги, поэтому мы не будем создавать кабинетные проекты. Для получения информации по созданию компонентов .NET, предназначенных для загрузки с Web-сервера, обратитесь к книге "С# 2008 и платформа .NET 3.5 для профессионалов" (ИД иВильямс", 2009г.). □ Шаблон Smart Device Cab Project (CAB-проект интеллектуального устройства) можно использовать для создания пакета программы установки приложений интеллектуальных устройств. Архитектура программы установки Microsoft Windows До появления программы установки Windows программистам приходилось создавать специализированные программы установки. Мало того, что построение таких установочных программ было не только более трудоемким, так еще и многие из них не соответствовали правилам Windows. Часто системные DLL оказывались замещенными более старыми версиями, поскольку программа установки не выполняла проверку версий. Кроме того, часто каталог, в который копировались файлы приложения, выбирался неправильно. Например, если при использовании жестко закодированной строки каталога, такой как C:\Program Files, системный администратор изменял применяемую по умолчанию букву диска, или установка выполнялась в интернациональной версии операционной системы, в которой этот каталог имел иное имя, установка оказывалась неудачной. Первая версия программы установки Windows была реализована в качестве составной части пакета Microsoft Office 2000 и в виде дистрибутивного пакета, который мог включаться в пакеты других приложений. Версия 1.1 программы установки была выпущена вместе с Windows 2000 и в нее была добавлена поддержка для регистрации компонентов СОМ+. В версию 1.2 была добавлена поддержка механизма защиты файлов Windows ME. Версия 2.0 была первой версией, которая включала поддержку для установки сборок .NET, а также поддержку 64-разрядной версии Windows. В Visual Studio 2008 используется версия 3.1.
Глава 18. Развертывание Windows-приложений 587 Термины программы установки Windows Для успешной работы с программой установки Windows нужно ознакомиться с некоторыми терминами, применяемыми при описании технологии программы установки Windows: пакетами, функциональными средствами и компонентами. В контексте программы установки Windows понятие компонента отличается от компонента в контексте .NET Framework. Компонент программы установки Windows — это всего лишь отдельный файл (или несколько файлов, логически связанных друг с другом). Таким файлом может быть исполняемый файл, DLL-библиотека или даже простой текстовый файл. Как видно на рис. 18.17, пакет состоит из одной или более функциональных средств. Пакет — это отдельная база данных Microsoft Installer (Программа установки Microsoft), или MSI. Функциональное средство — это возможности продукта с точки зрения пользователя, и оно может состоять из других функциональных средств и компонентов. Компонент представляет взгляд разработчика на установку. Это наименьший модуль установки, состоящий из одного или более файлов. Следует различать функциональные средства и компоненты, поскольку один компонент может быть включен в несколько функциональных средств (как Компонент 2 на рисунке). Одна функциональное средство не может входить в состав нескольких других функциональных средств. Рассмотрим реальный пример функциональных средств, которым вы уже должны располагать: Visual Studio 2008. С помощью опции Add/Remove Programs (Установка и добавление программ) панели управления можно изменять установленные функциональные средства Visual Studio после установки, щелкая на кнопке Uninstall/Change (Удалить/Изменить), как показано на рис. 18.18. Пакет 1—I Функциональное I средство А Компонент 1 Компонент 2 Функциональное средство В Функциональное средство С Компонент 3 Рис. 18.17. Состав пакета
588 Часть И. Программирование для Windows Q^*ff"i"ciMrt^n » mpi^i »»■»—ttmttmbmm *\*t}\ Vc^ P: Рггс. 18.18. Окно установки и удаления программ Щелкая на кнопке Uninstall/Change, можно открыть Мастер обслуживания (Maintenance Wizard) Visual Studio 2008. Это хороший способ увидеть функциональные средства в действии. Выберите опцию Add or Remove Features (Добавить или удалить функциональные средства), чтобы открыть список функциональных средств пакета Visual Studio 2008 (рис. 18.19). Пакет Visual Studio 2008 включает в себя следующие функциональные средства: Language Tools (Языковые средства), .NET Framework SDK (Набор средств разработки ПО для .NET), Crystal Reports for Visual Studio 2008 (Отчеты Crystal Reports для Visual Studio 2008), Tools for Redistributing Applications (Средства распространения приложений) и Server Components (Компоненты сервера). Функциональное средство Language Tools содержит подсредства Visual Basic, Visual C++ и Visual C#. Рис. 18.19. Список функциональных средств пакета Visual Studio 2008
Глава 18. Развертывание Windows-приложений 589 Преимущества программы установки Windows Программа установки Windows предоставляет несколько преимуществ. □ Функциональное средство может быть установлено, не установлено или объявлено. При использовании функции объявления пакет устанавливается при первом использовании. Возможно, у вас уже возникала ситуация, когда программа установки Windows запускалась при работе с Microsoft Word. При использовании еще не установленного объявленного функционального средства Word она будет автоматически установлена при первом же обращении к ней. □ В случае повреждения приложения оно может быть самостоятельно восстановлено с помощью функции восстановления пакетов программы установки Windows. □ В случае неудачи инсталляции программа установки выполнит автоматический откат. После неудачной установки все остается в том же состоянии, каком пребывало до ее запуска: в системе не остается никаких дополнительных записей системного реестра, файлов и т.п. □ Функция отмены установки удаляет все связанные с ней файлы, записи системного реестра и т.п. — иными словами, приложение может быть полностью удалено. Из системы удаляются все временные файлы, а системный реестр восстанавливается. Для получения информации о скопированных файлах и записях системного реестра можно просмотреть таблицы файла базы данных MSI. Создание установочного пакета для приложения SimpleEditor В этом разделе мы используем созданное в главе 17 решение для приложения SimpleEditor, чтобы средствами Visual Studio 2008 создать пакет программы установки Windows. Естественно, при выполнении описанных действий можно использовать любое другое разработанное приложение Windows Forms. Нужно только изменить некоторые из используемых имен. Планирование установки Прежде чем приступить к построению программы установки, необходимо спланировать ее содержимое. Продумайте ответы на перечисленные ниже вопросы. □ Какие файлы требуются для приложения? Естественно, приложению требуется исполняемый файл и, возможно, сборки определенных компонентов. Нам не нужно будет определять все зависимости между этими элементами, поскольку они включаются автоматически. Другими необходимыми файлами в числе прочих могут быть файл документации, файл readme. txt, файл лицензионного соглашения, шаблон документа, рисунки и файлы конфигурации. Иначе говоря, должны быть известны все необходимые файлы. Для приложения SimpleEditor, разработанного в главе 17, требуется исполняемый файл, а также файлы readme . rtf и license, rtf и растровое изображение из сайта Wrox Press, которое будет отображаться в диалоговых окнах установки.
590 Часть II. Программирование для Windows □ Какие каталоги должны использоваться? Установка файлов приложения должна выполняться в каталог Program Files\Имя_приложения. Имя каталога Program Files зависит от языкового варианта операционной системы. Кроме того, администратор может выбирать другие пути установки данного приложения. Точное знание местоположения этого каталога не обязательно, поскольку для его отыскания существует специальная функция API. Программа установки позволяет помещать файлы в специальную заранее определенную папку в каталоге Program Files. Стоит подчеркнуть еще раз: ни при каких обстоятельствах каталоги не должны быть жестко закодированы. В различных международных версиях ОС эти каталоги обладают различными именами. Даже если приложение поддерживает только англоязычную версию Windows (что не слишком разумно), не исключено, что системный администратор переместил эти каталоги на другие диски. Исполняемый файл приложения SimpleEditor будет установлен в заданный по молчанию каталог приложения, если только пользователь не выберет другой путь. □ Как пользователь должен получать доступ к приложению? Например, можно поместить команду быстрого доступа к исполняемому файлу в меню Start, либо поместить соответствующую пиктограмму на рабочий стол. Если хотите поместить пиктограмму на рабочий стол, выясните, устраивает ли это пользователя. В соответствии с рекомендациями по Windows XP рабочий стол должен оставаться как можно более свободным. Поэтому приложение SimpleEditor должно быть доступно из меню Start. □ Что служит носителем для распространения? Должны ли пакеты установки быть помещены на компакт-диск, гибкие диски или в сетевой каталог совместного использования? □ На какие вопросы должны ответить пользователи? Должны ли они принять условия лицензионного соглашения, прочесть файл ReadMe или ввести путь для установки? Требуются ли для установки какие-то дополнительные опции? Диалоговые окна, по умолчанию предоставляемые программой установки Visual Studio 2008, вполне достаточны для проекта программы установки Visual Studio 2008, который будет создан в остальной части этой главы. Мы запросим каталог, в который должна быть установлена программа (пользователь может выбрать путь, отличающийся от заданного по умолчанию), отобразим файл ReadMe и предложим пользователю принять условия лицензионного соглашения. Создание проекта Теперь, когда состав пакета установки известен, программу установки Visual Studio 2008 можно использовать для создания проекта программы установки и добавления всех файлов, предназначенных для установки. В следующем практическом занятии мы используем мастер Project Wizard (Мастер проекта) для конфигурирования проекта.
Глава 18. Развертывание Windows-приложений 591 Практическое занятие Создание проекта программы установки Windows 1. Откройте файл решения проекта SimpleEditor, созданный в главе 17. Проект установки будет добавлен в существующее решение. Если решение, описанное в главе 17, не было создано, его можно найти в загружаемом коде. 2. Добавьте в решение проект установки SimpleEditor Setup, выполнив команду меню File^Add Projects New Project (Файл ^Добавить проекте Новый проект), как показано на рис. 18.20, и щелкните на кнопке ОК. Add Naw Protect гтг Project types Web *> Office Smart Device Acropolis UNQ to XSD Preview Templates: Visual Studio installed templates Z2- Setup Project _j Merge Module Project фСАВ Project Test { .J] Search Online Templates... WCF Workflow Database Projects Other Languages Other Project Types Setup and Deployment Database Extensibility ZcsLBtoiects— ' »A Web Setup Project jj Setup Wizard ajl Smart Device CAB Project 3®Ш Create a Windows Installer project to which files can be added Name: StmpleEdrtorSetup Location: C\BegVCSharp\Chaptefl8\Simple£drtor Рис. 18.20. Добавление проекта установки SimpleEdi tor Setup Свойства проекта На данный момент мы располагаем только файлом проекта для решения установки. Файлы, которые требуется установить, уже должны быть определены, но потребуется также сконфигурировать свойства проекта. Для этого необходимо разобраться с опциями Packaging (Упаковка) и Bootstrapper (Начальный загрузчик). Упаковка Установка запускается из MSI, но с помощью трех опций диалогового окна, показанного на рис. 18.21, можно определить способ упаковки файлов, которые предназначены для установки. Это диалоговое окно открывается посредством щелчка правой кнопки мыши на проекте SimpleEditorSetup и выбора опции Properties из контекстного меню. Рассмотрим опции раскрывающегося списка Package Files (Упаковать файлы). □ Опция As Loose Uncompressed Files (В виде отдельных несжатых файлов) сохраняет все файлы программы и данных в их первоначальном виде. Никакое сжатие не выполняется. □ Опция In Setup File (В файл установки) объединяет и сжимает все файлы в один файл MSI. Эта опция может быть замещена для отдельных компонентов в пакете.
592 Часть II. Программирование для Windows При помещении всех файлов в один файл MSI необходимо убедиться, что размер программы установки соответствует предполагаемому целевому носителю, такому как компакт-диски или гибкие диски. Если общий размер предназначенных для установки файлов превышает емкость одного гибкого диска, можно попытаться изменить параметр сжатия, выбирая опцию Optimized for Size (Оптимизировать для размера) из раскрывающегося списка Compression (Сжатие). Если файлы по-прежнему не умещаются на выбранном носителе, выберите для упаковки следующую опцию. Третий способ упаковки файлов — использование кабинетного файла (файлов). При использовании этого метода файл MSI служит только для загрузки и установки файлов CAB. Применение файлов CAB позволяет устанавливать размеры файлов такими, чтобы установку можно было выполнять с компакт-дисков или гибких дисков (для выполнения установки с гибких дисков размеры файлов можно устанавливать равными 1440 Кбайт). SimpleEditofSetup Property Pages Configuration Actrve<Debugj Configuration Properties Output file name Package files Compression Installation URL i Л Debug S.mpleEdrtorS [*"**** Optimized for speed Configuration Manager etup.rmi Ы si =1 C-jstom ... Рис. 18.21. Опции запуска установки Необходимые компоненты В этом же диалоговом окне можно конфигурировать необходимые компоненты, которые должны быть установлены до установки приложения. Щелчок на кнопке Settings (Настройки), расположенной рядом с текстовым полем Prerequisites URL (URL-адрес необходимых компонентов) ведет к открытию диалогового окна Prerequisites (Необходимые компоненты), показанного на рис. 18.22. Как видите, .NET Framework 2.0 выбрано в качестве необходимого компонента по умолчанию. Если клиентская система не имеет установленной версии .NET Framework, она будет инсталлирована из программы установки. Как видно из следующего перечня, можно выбирать также и другие необходимые компоненты. □ Windows Installer 3.1. Windows Installer 3.1 требуется для пакетов программ установки, созданных с помощью Visual Studio 2008. Если целевая система — Windows Vista или Windows Server 2008, программа установки уже присутствует в ней. При использовании более ранних систем нужная версия Windows Installer может отсутствовать, поэтому эту опцию можно выбрать, чтобы включить Windows Installer 3.1 в пакет.
Глава 18. Развертывание Windows-приложений 593 Prerequisites 4 Create setup program to install prerequisite components Choose which prerequisites to install ^Microsoft Data Access Components 2Л «J NET Framework 2.0 (*36| *J Visual C« - Runtime Libraries (x86) j «5w,ndowsimta|,er31 J 5 -NET Framework 3.5 yjkh/stal Reports for Visual Studio Codename Orcas Zjkbcwiott Visual Studio 2005 Report Viewer Спер M'»o>P« l^ptjatc »W Ф9ГС Вdistrrbutable WTOPHCnU Specify the install location for prerequisites • Download prerequisites from the component vendor s web site Download prerequisites from the same location as my application Download prerequisites from the following location Г <* |Н5р5Н Рис. 18.22. Диалоговое окно Prerequisites □ SQL Server 2005 Express Edition SP2. Если нужно, чтобы база данных была установлена в клиентской системе, компонент SQL Server 2005 Express Edition можно включить в пакет программы установки. Доступ к серверу SQL Server посредством ADO.NET описан в главе 24. □ Visual C++ Runtime Libraries (x86) (Библиотеки времени выполнения Visual C++). Если приложение использует "родные" библиотеки, которые требуют наличия библиотек времени выполнения C++, этот компонент можно добавить в качестве необходимого. □ Crystal Reports for .NET. Crystal Reports позволяет создавать графические отчеты. Это приложение подробнее описано в книге Полный справочник по Crystal Reports XI (ИД "Вильяме", 2006 г.). □ Microsoft Data Access Components 2.8 (Компоненты доступа к данным Microsoft). Microsoft Data Access Components (MDAC) включают в себя поставщик OLE DB, драйверы ODBC и Microsoft SQL Server Network Libraries (Сетевые библиотеки Microsoft SQL Server), используемые для доступа к базам данных. MDAC версии 8.0 — составная часть Windows Server 2003. При использовании .NET 2.0 компонент MDAC не требуется. При использовании поставщика данных .NET для SQL Server требуется установка только поставщиков данных OLEDB и ODBC. Практическое занятие Конфигурирование ПрОвКТЭ 1. Измените рассмотренную опцию Prerequisites на странице Property, включив компонент Windows Installer 3.1, чтобы обеспечить возможность установки приложения в системах, в которых программа установки Windows Installer 3.1 не доступна. Измените также имя выходного файла на WroxSimpleEditor .msi, как помазано на рис. 18.23. Затем щелкните на кнопке ОК. 2. В окне Properties установите значения свойств проекта, как указано в табл. 18.1. % .а )
594 Часть II. Программирование для Windows Рис. 18.23. Изменение имени выходного файла Таблица 18.1. Значения свойств проекта Свойство Значение Author Description Keywords Manufacturer ManufacturerUrl Product Name SupportUrl Title Version Wrox Press Simple Editor to print and edit text files. Installer, Wrox Press, Simple Editor Wrox Press http://www.wrox.com Simple Editor http://p2p.wrox.com Installation Demo for Simple Editor 1.0.0 Редакторы установки Среда Visual Studio 2008 предоставляет шесть редакторов для проекта установки. Выбор нужного редактора можно осуществить, открывая проект развертывания и выбирая пункт меню Vlew1^Editor (Вид^Редактор). □ Редактор File System Editor (Редактор файловой системы) используется для добавления файлов в пакет установки. □ Редактор Registry Editor (Редактор реестра) позволяет создавать записи системного реестра для приложения. □ Редактор File Types Editor (Редактор типов файлов) позволяет регистрировать для приложения конкретные расширения файлов. □ Редактор User Interface Editor (Редактор интерфейса пользователя) позволяет добавлять и конфигурировать диалоговые окна, отображаемые во время установки программного продукта. □ Редактор Custom Actions Editor (Редактор пользовательских действий) позволяет запускать пользовательские программы во время установки и удаления.
Глава 18. Развертывание Windows-приложений 595 □ Редактор Launch Conditions Editor (Редактор условий запуска) позволяет указывать требования к приложению — например, необходимость наличия исполняющей среды .NET. Редактор File System Editor Редактор File System Editor позволяет добавлять файлы в пакет установки и конфигурировать каталоги, в которые они должны быть инсталлированы. Для открытия этого редактора используйте опцию меню View1^ Editors File System (Вид•=>Редакторе Файловая система). При этом автоматически открывается ряд предварительно определенных специальных папок, как показано на рис. 18.24. 14 Application Folder '2Л User i Desktop Д| User's Programs Menu Рис. 18.24. Предварительно определенные специальные папки □ Папка Application (Приложение) служит для хранения исполняемых файлов и библиотек. Ее местоположение определяется как [ProgramFilesFolder] \ [Изготовитель] \ [Имя_продукта]. В англоязычных системах вместо спецификатора [ProgramFilesFolder] подставляется строка C:\Program Files. Каталоги [Изготовитель] и [Имя_продукта] определяются свойствами проекта Manufacturer и ProductName. □ Если хотите поместить пиктограмму на рабочий стол, можно использовать папку User' s Desktop (Рабочий стол пользователя). По умолчанию путь к этой папке — С: \Users\имя_пользователя\Desktop или C:\Users\All Users\ Desktop, в зависимости от того, выполняется установка для одного или для всех пользователей. □ Обычно пользователь будет запускать программу из меню All Programs (Все программы). По умолчанию путь к этой папке — С: \Documents and Settings\nwi_ пользователях Start Menu\ Programs. Ярлык приложения можно поместить в это меню. Имя ярлыка должно включать название компании и имя приложения, чтобы пользователь мог легко идентифицировать приложение (например, Microsoft Excel). Некоторые приложения создают подменю, из которого можно запускать более одного приложения — например, Microsoft Visual Studio 2008. Как указано в рекомендациях Windows, многие программы делают это без веских причин, помещая в это меню программы, которые не нужны. В эти меню не следует помещать программу удаления, поскольку данное функциональное средство доступно из пиктограммы Add/ Remove Programs (Установка и удаление программ) панели управления и должна использоваться оттуда. Файл справки также не следует помещать в это меню, поскольку он должен быть доступен из приложения. Таким образом, для многих приложений достаточно поместить ярлык приложения непосредственно в меню All Programs. Цель перечисленных ограничений — предотвращение загромождения меню Start чересчур большим количеством элементов. Прекрасную справочную информацию по этому вопросу можно найти в статье, посвященной спецификациям приложений для Microsoft Windows Vista. Эти документы можно найти на Web-странице www.microsoft.com/ win logo. Дополнительные папки можно добавить, щелкая правой кнопкой мыши и
596 Часть II. Программирование для Windows выбирая из контекстного меню команду Add Special Folder (Добавить специальную папку). Некоторые из этих папок перечислены ниже. □ Global Assembly Cache Folder (Папка глобального кэша сборок) — это папка, в которую можно инсталлировать сборки совместного использования. Global Assembly Cache (Глобальный кэш сборок) (GAC) используется для сборок, которые должны совместно использоваться несколькими приложениями. □ User's Personal Data Folder (Папка персональных данных пользователя) — заданная по умолчанию папка для хранения документов пользователя. Путь к ней по умолчанию — С: \Users\ [имя_пользователя] \Му Documents. Этот путь — заданный по молчанию каталог, используемый Visual Studio для хранения проектов. □ Ярлыки, помещенные в меню User's Send To Menu (Меню "Отправить..." пользователя) расширяет контекстное меню Send To (Отправить...) при наличии выбранного файла. Как правило, это контекстное меню позволяет пользователю отправить файл в соответствующее место назначения, такое как гибкий диск, адресат электронной почты или папка My Documents (Мои документы). Добавление элементов в специальные папки Элементы в специальную папку можно добавить, выбирая папку, а затем выбирая команду меню Actions Add Special Folder (Действие1^ Добавить специальную папку). При этом можно выбирать опции Project Output (Вывод проекта), Folder (Папка), File (Файл) или Assembly (Сборка). Добавление вывода проекта ведет к автоматическому добавлению сгенерированных выходных файлов и файла .dll или .ехе, в зависимости от того, является ли добавленный проект библиотекой компонентов или приложением. Выбор опции Project Output либо Assembly ведет к автоматическому добавлению в папку всех зависимостей (всех сборок, на которые имеются ссылки). Свойства файла При выборе свойств файла в папке можно устанавливать описанные ниже свойства. В зависимости от типов файлов некоторые их этих свойств не применимы, а некоторые дополнительные свойства не нашли своего отражения в табл. 18.2. Таблица 18.2. Свойства файла Свойство Описание Condition Это свойство позволяет определить условие, при котором выбранный файл должен быть установлен. Оно может быть полезно, если данный файл должен добавляться только для конкретной версии операционной системы, или если пользователь должен осуществить выбор в диалоговом окне Exclude Если установка данного файла не требуется, значение этого свойства может быть установлено равным True. В результате файл может оставаться в проекте, но не устанавливаться. Файл может быть исключен из установки при заведомом отсутствии какой-либо зависимости от него или если он уже присутствует в каждой системе, в которой приложение будет развертываться PackageAs Это свойство позволяет изменить заданный по умолчанию способ добавления файла в пакет программы установки. Например, если в конфигурации проекта способ добавления указан как In Setup File (В файл установки), это свойство позволяет для конкретного файла изменить способ добавления на Loose (Свободный), чтобы данный файл не добавлялся в файл базы данных MSI. Это полезно, например, если нужно добавить файл ReadMe, который пользователь должен прочесть до начала установки. Очевидно, что этот файл не должен сжиматься, даже если все остальные файлы сжаты
Глава 18. Развертывание Windows-приложений 597 Окончание табл. 18.2 Свойство Описание Permanent Установка значения этого свойства равным True означает, что файл останется на целевом компьютере после удаления программного продукта. Это свойство можно использовать для файлов конфигурации. Вы могли встречаться с подобной ситуацией при инсталляции новой версии Microsoft Outlook: при конфигурировании Microsoft Outlook, его удалении и последующей повторной установке. При этом необходимость повторного конфигурирования приложения отсутствует, поскольку конфигурация последней установки не удаляется Readonly Это свойство при установке устанавливает для файла атрибут "только для чтения" vital Это свойство означает, что файл совершенно необходим для установки данного продукта. Если установка данного файла оказывается неудачной, вся установка отменяется, и программа установки выполняет откат Добавление файлов в пакет программы установки 1. Посредством пункта меню Project^Add1^Project Output (Проект^Добавить^Вывод проекта) добавьте основной вывод проекта SimpleEditor в папку Application проекта программы установки. В диалоговом окне Add Project Output Group (Добавить группу вывода проекта) выберите опцию Primary Output (Основной вывод), как показано на рис. 18.25. Add Project Output Group тг Project SimpleEditor Localized resources Debug Symbols Content Files Source Files Documentation Files «'CjLj - r J ^ ► Configuration Description: Contains the DLL or EXE built by the project. Puc. 18.25. Диалоговое окно Add Project Output Group Щелкните на кнопке OK, чтобы добавить основной вывод проекта SimpleEditor в папку Application автоматически открывающегося редактора File System Editor. В данном случае основным выводом является файл SimpleEditor.exe. 2. Дополнительные файлы — файлы логотипа, лицензионного соглашения и ReadMe. В среде редактора File System Editor в папке Application создайте подкаталог Setup. Это можно выполнить, выбирая папку Application, а затем выбирая пункт меню Action^Add1^Folder (Действие^Добавить1^Папка).
598 Часть II. Программирование для Windows В Visual Studio меню Action доступно только при выборе каких-либо элементов в окнах редакторов установки. Если элемент выбран в окне Solution Explorer (Проводник решений) или Class View (Представление классов), меню Action не доступно. 3. Добавьте файлы wroxlogo.bmp, wroxsetuplogo.bmp, readme.rtf и license.rtf в папку Setup, щелкая на ней правой кнопкой мыши и выбирая пункт Add^File (Добавить1^ Файл) из контекстного меню. Эти файлы доступны в загружаемом коде для данной главы, но их легко создать самостоятельно. Текстовые файлы можно заполнить информацией лицензионного соглашения и файла ReadMe. Свойства этих файлов, которые будут использованы в диалоговых окнах программы установки, можно не изменять. Размер растрового изображения, хранящегося в файле wroxsetuplogo.bmp, должен быть равным 500 пикселей по ширине и 70 пикселей по высоте. 420 левых пикселей растрового изображения должны содержать только фоновое изображение, поскольку текст диалоговых окон установки будет перекрывать эту область. 4. Добавьте файл readme. txt в папку Application. Этот файл должен быть доступен для прочтения пользователями до начала установки. Установите значение свойства PackageAs равным vsdpaLoose, чтобы предотвратить сжатие этого файла в пакет программы установки. Чтобы воспрепятствовать изменению этого файла, установите значение его свойства Readonly равным true. Теперь проект содержит два файла ReadMe: readme.txt и readme.rtf. Файл readme. txt может быть прочтен пользователем, устанавливающим приложение, до начала установки. Файл readme.rtf предоставляет определенную информацию в диалоговых окнах установки. 5. Перетащите файл demo.wroxtext в папку User's Desktop (Рабочий стол пользователя). Этот файл должен устанавливаться только после положительного ответа пользователя на вопрос о действительной необходимости выполнения установки, поэтому установите значение свойства Condition этого файла равным CHECKB0XDEM0. Условие CHECKB0XDEM0 - то, которое может быть установлено пользователем. Значение должно быть записано прописными буквами. Файл инсталлируется, только если условие CHECKB0XDEM0 установлено в true. Позже мы определим диалоговое окно, в котором это свойство будет устанавливаться. 6. Чтобы программа была доступной через меню Starts Programs, требуется ярлык для программы SimpleEditor. В папке Application выберите элемент Primary Output from SimpleEditor (Основной вывод SimpleEditor) и откройте меню Actions Create Shortcut to Primary output from SimpleEditor (Действие^Создать ярлык для основного вывода SimpleEditor). Установите Wrox Simple Editor в качестве значения свойства Name созданного ярлыка и перетащите этот ярлык в меню User's Programs (Программы пользователя). Редактор File Types Editor Если приложение использует нестандартные типы файлов и нужно зарегистрировать эти расширения для файлов, которые должны запускать приложение при двойном щелчке на них, можно использовать редактор File Types Editor (Редактор типов файлов), выбирая опцию меню View1^ Editors File Types (Вид^Редактор^Типы файлов).
Глава 18. Развертывание Windows-приложений 599 Редактор File Types Editor с добавленным нестандартным расширением показан на рис. 18.26. Rte Type CSimpteEdttorSetup) I J? File Types on Target Machine _.jt WfOK.StmpleEdrtof.Teirt (Wrortert) •> ГОУЯЯ Рис. 18.26. Редактор File Types Editor с добавленным нестандартным расширением Редактор File Types Editor позволяет конфигурировать расширение файла, которое должно обрабатываться в приложении. Свойства расширений файлов описаны в табл. 18.3. Таблица 18.3. Свойства расширений файлов Свойство Описание Name Command Description Extensions Icon Добавляет полезное имя, описывающее тип файла. Это имя отображается в окне редактора File Types Editor и записывается в системный реестр. Следует выбирать уникальное имя. Пример для типов файлов . doc — Word. Document .12. Использование идентификатора программы (ProgiD), как в примере файла Word, не обязательно. Можно применять также простой текст, подобный wordhtmlf ile, который используется для расширения файла .dochtml Свойство Command позволяет указывать исполняемый файл, который должен запускаться при открытии пользователем файла данного типа Добавляет описание Это свойство определяет расширение файла, в котором приложение должно быть зарегистрировано. Расширение файла будет зарегистрировано в соответствующем разделе системного реестра Указывает пиктограмму, которая должна отображаться для данного расширения файла Создание действий После создания типов файлов в редакторе File Types Editor можно добавить необходимые действия. Действие, автоматически добавляемое по умолчанию — Open (Открыть). Однако можно добавить дополнительные действия, такие как New (Создать) и Print (Печать), или любые другие, подходящие для данной программы. Вместе с действиями необходимо определить свойства Arguments и Verb. Свойство Arguments указывает аргументы, передаваемые программе, которая зарегистрирована для данного расширения файла. Например, %1 означает, что имя файла передается приложению. Свойство Verb указывает действие, которое должно быть выполнено. С действием печати может быть передано свойство /print, если приложение его поддерживает. Следующее практическое занятие посвящено добавлению действия в программу установки SimpleEditor. Нам необходимо зарегистрировать расширение файла, чтобы программу SimpleEditor можно было использовать из проводника Windows для открытия файлов с расширением .wroxtext. После выполнения такой регистрации на этих файлах можно будет дважды щелкнуть для их открытия, и приложение SimpleEditor будет запускаться автоматически.
600 Часть II. Программирование для Windows Практическое занятие УстаНОВКЭ раСШИрвНИЯ фаЙЛЭ 1. Используя пункт меню View1^ Editors File Types, запустите редактор File Types Editor. Посредством меню Action^Add File Type (Действие^Добавить тип файла) добавьте новый тип файла, установив свойства, как показано в табл. 18.4. Поскольку изменение принадлежности расширения файла . txt приложению Notepad нежелательно, используйте расширение .wroxtext. Таблица 18.4. Значения свойств нового типа файла Свойство Значение (Name) Wrox.SimpleEditor.Text Command Primary output from SimpleEditor Description Wrox Text Documents Extensions Wroxtext Можно также установить свойство Icon, чтобы определить пиктограмму для открытия файла, и тип MIME. Оставьте заданные по умолчанию значения свойств действия Open, чтобы имя файла передавалось в качестве аргумента приложения. Редактор Launch Condition Editor 1=41лжтшговтаяг 'Cl Search Target Machine Uj Launch Conditions Щ .NET Framework Launch condrti...pteEditorsetup) p1)e| Редактор Launch Condition Editor позволяет указывать требования к целевой системе, которые должны быть удовлетворены, прежде чем установка сможет быть выполнена. Запустите редактор Launch Condition Editor (рис. 18.27), выбирая пункт меню View1^ Editors Launch Conditions (Вид1^ Рис. 18.27. Редактор РедакторОУсловия запуска). j , г ,. . р*.. Редактор предоставляет два раздела для указания требований: Search Target Machine (Искать на целевом компьютере) и Launch Conditions (Условия запуска). В первом разделе можно указывать конкретный файл или запись реестра, которые нужно искать. Второй раздел определяет сообщение об ошибке, отображаемое при неудаче поиска. Ниже приведены некоторые условия запуска, которые можно определять посредством меню Action. □ Условие File Launch Condition (Условие запуска файла) выполняет в целевой системе поиск указанного файла перед запуском установки. □ Условие Registry Launch Condition (Условие запуска реестра) позволяет перед запуском установки потребовать проверку записей реестра. □ Условие Windows Installer Launch Condition (Условие запуска программы установки Windows) позволяет выполнить поиск компонентов программы установки Windows, которые обязательно должны присутствовать. □ Условие .NET Framework Launch Condition (Условие запуска каркаса .NET) проверяет наличие установленного каркаса .NET Framework в целевой системе. □ Условие Internet Information Services Launch Condition (Условие запуска IIS) проверяет наличие установленных информационных служб Internet. Добавление этого условия запуска добавляет поиск конкретной записи реестра, определенной при установке информационных служб Internet, а также условие проверки конкретной версии.
Глава 18. Развертывание Windows-приложений 601 По умолчанию условие .NET Framework Launch Condition установлено, а его свойствам присвоены заранее определенные значения: [VSDNETMSG], являющееся заранее определенным сообщением об ошибке, установлено в качестве значения свойства Message. Если каркас .NET Framework 3.5 не установлен, отображается сообщение, информирующее пользователя о необходимости установки .NET Framework. По умолчанию свойство InstallUrl установлено в значение http://go.microsoft.com/ fwlink/?linkid=7 6617, что позволяет пользователям без труда запустить установку каркаса .NET Framework. Редактор User Interface Editor Редактор User Interface Editor позволяет определять диалоговые окна, отображаемые при конфигурировании установки. В них можно информировать пользователей об условиях лицензионного соглашения и запрашивать пути установки и другую информацию для конфигурирования приложения. В следующем практическом занятии мы запустим редактор User Interface Editor, используемый для конфигурирования диалоговых окон, которые открываются во время инсталляции приложения. практическое занятие запуск редактора User Interface Editor 1. Запустите редактор User Interface Editor, выбирая меню View=> Editors User Interface (Вид^ Редакторе Интерфейс пользователя). 2. Используйте User Interface Editor для установки свойств предопределенных диалоговых окон. Автоматически сгенерированные диалоговые окна и два доступных режима установки показаны на рис. 18.28. User Interlace (SimpteEditorSetup) •эВ2Э g Start ^ Welcome  Installation Folder J Confirm Installation 5) Progress ^3 Progress .^J End  Finished £| Administrative Install -^ Start j7{ Welcome Щ Installation Folder 1 Confirm Installation 3) Progress _3 Progress 5 End J Finished Рис. 18.28. Автоматически сгенерированные диалоговые окна и два доступных режима установки Описание полученных результатов Как показано на рис. 18.28, в данном случае доступны два режима установки: Install (Инсталляция) и Administrative Install (Административная инсталляция). Как правило, режим Install используется для установки приложения в целевой системе. Режим Administrative Install позволяет устанавливать образ приложения в сетевом каталоге совместного использования. После этого пользователи могут устанавливать приложение из сети. Оба режима установки имеют три этапа, на которых диалоговые окна могут отображаться: Start (Начало), Progress (Ход работ) и End (Завершение). Рассмотрим диалоговые окна, определенные по умолчанию. □ Диалоговое окно Welcome (Приветствие) отображает пользователю приветственное сообщение. Заданный по умолчанию текст приветствия можно заменить собственным сообщением. Пользователи могут только отменить установку или щелкнуть на кнопке Next (Далее). □ Во втором диалоговом окне, Installation Folder (Папка инсталляции), пользователи могут выбрать папку, в которой должно быть установлено приложение. При добавлении нестандартных диалоговых окон (их мы рассмотрим ниже) они должны быть добавлены перед этим диалоговым окном.
602 Часть П. Программирование для Windows □ Диалоговое окно Confirm Installation (Подтверждение инсталляции) — последнее отображаемое перед запуском установки. □ Диалоговое окно Progress (Ход работ) отображает элемент управления протекания процесса, позволяющий пользователям наблюдать за протеканием установки. □ По завершении установки открывается диалоговое окно Finished (Инсталляция завершена). Диалоговые окна, определенные по умолчанию, открываются автоматически во время установки, даже если редактор User Interface Editor ни разу не был открыт, но эти диалоговые окна должны быть сконфигурированы для отображения полезных сообщений, соответствующих приложению. В следующем практическом занятии мы выполним конфигурирование диалоговых окон, которые открываются при установке приложения. В данном случае мы опустим ветвь Administrative Install и выполним только конфигурирование ветви типичной установки. Практическое занятие Конфигурирование ДИаЛОГОВЫХ ОКОН, определенных по умолчанию 1. Выберите диалоговое окно Welcome. Окно Properties содержит три свойства этого диалогового окна: BannerBitmap, CopyrightWarning и WelcomeText. Выберите свойство BannerBitmap, щелкая на опции Browse (Обзор) поля со списком и выбирая файл wroxsetuplogo.bmp в папке Application Folder\Setup. Хранящееся в этом файле растровое изображение будет отображаться в верхней части данного диалогового окна. Заданный по умолчанию текст свойства CopyrightWarning следующий: WARNING: This computer program is protected by copyright law and international treaties. Unauthorized duplication or distribution of this program, or any portion of it, may result in severe civil or criminal penalties, and will be prosecuted to the maximum extent possible under the law. ПРЕДУПРЕЖДЕНИЕ. Эта компьютерная программа защищена законами о защите авторских прав и международными соглашениями. Несанкционированное копирование или распространение этой программы или любой ее части может повлечь суровое административное или криминальное наказание, и будет подвергаться максимальному преследованию в рамках действующего законодательства. Этот текст отображается также в диалоговом окне Welcome. Если хотите отобразить более строгое предупреждение, измените этот текст. Свойство WelcomeText определяет больший объем текста, чем отображается в диалоговом окне. Его значение, определенное по умолчанию, выглядит следующим образом: The installer will guide you through the steps required to install [ProductName] on your computer. Программа установки поможет выполнить действия, необходимые для инсталляции [ProductName] на вашем компьютере. Этот текст также можно изменить. Строка [ProductName] будет автоматически замещен свойством ProductName, которое было определено в свойствах проекта. 2. Выберите диалоговое окно Installation Folder. Это диалоговое окно имеет два свойства: BannerBitmap и InstallAllUsersVisible. Значение второго свойства, используемое по умолчанию — true. Если его установить равным false,
Глава 18. Развертывание Windows-приложений 603 инсталляция приложения будет возможной только для пользователя, который зарегистрирован в системе во время выполнения инсталляции. Измените значение свойства BannerBitmap на файл wroxsetuplogo.bmp, как это было выполнено для диалогового окна Welcome. Поскольку это свойство позволяет отображать растровое изображение в каждом диалоговом окне, измените свойство BannerBitmap и для всех остальных диалоговых окон. Дополнительные диалоговые окна Если вы разработали нестандартное диалоговое окно, программа установки Visual Studio Installer позволяет добавить его в процесс инсталляции. Вообще говоря, для этого требуется более сложный инструмент, такой как InstallShield или Wise for Windows — но программа установки Visual Studio Installer позволяет добавлять и настраивать многие заранее определенные диалоговые окна посредством использования экрана Add Dialog (Добавление диалога). Выбор опции Start sequence (Процесс запуска) в редакторе User Interface Editor и выбор опции меню Action^Add Dialog (Действие1^Добавить диалог) ведет к отображению диалогового окна Add Dialog (Добавление диалога), показанного на рис. 18.29. Все эти диалоговые окна являются конфигурируемыми. | Add Dialog Э RadioButtons B button») 3 Checkboxes @ 3 license Agreement [ D RadioButtons C buttons) 3 Customer Information Ш Read Me m RadioButtons D buttons) 3 Textboxes (A) ~3 Register User m Checkboxes (A) 3 Teitboxes (B) 3 Splash If 1 В 1] 3 Checkboxes (B) m Textboies (Q Cancel J J Puc. 18.29. Д диалогового окна Add Dialog Доступны диалоги, содержащие два, три или четыре переключателя, диалоги с флажками, отображающие до четырех флажков, и диалоги с текстовыми полями, отображающие до четырех текстовых полей. Эти диалоги можно конфигурировать, устанавливая их свойства. Ниже кратко рассмотрены некоторые из диалогов. □ Диалоговое окно Customer Information (Информация о клиенте) запрашивает пользователей об их имени и названии организации, а также серийный номер продукта. Если серийный номер продукта не предоставляется, текстовое поле Serial Number (Серийный номер) можно скрыть, устанавливая значение свойства ShowSerialNumber равным false. □ Диалоговое окно License Agreement (Лицензионное соглашение) позволяет пользователям принять условия лицензионного соглашения до начала процесса инсталляции. Файл лицензионного соглашения определяется свойством LicenseFile.
604 Часть II. Программирование для Windows □ В диалоговом окне Register User (Регистрация пользователя) пользователи могут щелкнуть на кнопке Register Now (Зарегистрировать), чтобы запустить программу, которая определена свойством Executable. Пользовательская программа может пересылать данные на сервер FTP или передавать данные по электронной почте. □ Диалоговое окно Splash (Заставка) отображает экран заставку перед началом установки, используя при этом растровое изображение, указанное свойством SplashBitmap. В следующем практическом занятии мы добавим несколько дополнительных диалоговых окон, таких как Read Me (Важная информация), License Agreement (Лицензионное соглашение) и Checkboxes (Флажки). Добавление других диалоговых окон 1. Выбирая команду меню Actions Add Dialog, добавьте в процесс запуска диалоговые окна Read Me, License Agreement и Checkboxes (А). Посредством перетаскивания определите последовательность открытия диалоговых окон в процессе запуска следующим образом: Welcome - Read Me - License Agreement - Checkboxes (A) - Installation Folder - Confirm Installation. 2. Для всех этих диалоговых окон сконфигурируйте свойство BannerBitmap, как это было сделано ранее. В качестве значения свойства Readme File диалогового окна Read Me установите readme.rtf — файл, который ранее был добавлен в каталог Application Folder\Setup. 3. В качестве значения свойства LicenseFile диалогового окна License Agreement установите LicenseFile. 4. Используйте диалоговое окно Checkboxes (А) для запроса пользователей о необходимости установки файла demo.wroxtext (который был помещен в папку Desktop пользователя). Измените свойства этого диалогового окна в соответствии с табл. 18.5. Таблица 18.5. Значения свойств диалогового окна Checkboxes (A) Свойство Значения BannerText Optional Files BodyText Installation of optional files CheckboxlLabel Do you want a demo file put on to the desktop? CheckboxlProperty CHECKBOXDEMO Checkbox2Visible False Checkbox3Visible False Checkbox4Visible False Значение свойства CheckboxlProperty устанавливается таким же, как и свойства Condition файла demo.wroxtext — это значение свойства Condition было установлено ранее при добавлении файла в пакет с помощью редактора File System Editor. Если пользователь установит этот флажок, значение свойства CHECKBOXDEMO будет равным
Глава 18. Развертывание Windows-приложений 605 true, и файл будет инсталлирован. В противном случае значение будет равным false, и файл инсталлирован не будет. Значение свойства CheckboxXVisible других флажков устанавливается равным false, поскольку нам нужен только один флажок. Компоновка проекта Теперь можно выполнить следующее практическое занятие, чтобы приступить к компоновке проекта программы установки. Практическое занятие |<ОМПОНОВКа проекта 1. Чтобы создать пакет программы установки Microsoft, щелкните правой кнопкой мыши на проекте SimpleEditorSetup и выберите пункт Build (Компоновать) из контекстного меню. 2. В случае успешной компоновки файлы setup.exe и WroxSimpleEditor. msi, а также readme.txt появятся в каталоге Debug (Отладка) или Release (Публикация) (в зависимости от настроек компоновки). Описание полученных результатов Исполняемый файл Setup.exe запускает инсталляцию MSI-файла базы данных по имени WroxSimpleEditor .msi. Все файлы, добавленные в проект программы установки (за одним исключением), объединены и сжаты в файл MSI, поскольку в свойствах проекта было указано паковать файлы в файл установки (Package Files in Setup File). Единственное исключение — файл readme.txt, свойство PackageAs которого было изменено, чтобы его можно было прочесть непосредственно перед установкой приложения. Пакет установки каркаса .NET Framework можно найти в подкаталоге DotNetFx. Инсталляция Теперь можно приступить к инсталляции приложения SimpleEditor. Дважды щелкните на файле Setup.exe или выберите файл WroxSimpleEditor .msi. Щелкните правой кнопкой мыши, чтобы открыть контекстное меню, и выберите из него пункт Install (Инсталлировать). Установку можно запустить также из среды Visual Studio 2008, щелкая правой кнопкой на открытом проекте установки в окне Solution Explorer и выбирая из контекстного меню пункт Install. Как показано в последующих разделах, все диалоговые окна содержат логотип Wrox, а вставленные диалоговые окна Read Me и License Agreement открываются с определенными при их конфигурировании файлами. Рассмотрим процесс инсталляции. Окно Welcome Первое открывающееся диалоговое окно — Welcome (Приветствие), показанное на рис. 18.30. При этом отображается логотип Wrox, который был вставлен установкой свойства BannerBitmap в соответствующее значение. Отображаемый текст определен свойствами WelcomeText и CopyrightWarning. Заголовок этого диалогового окна определяется свойством ProductName, установленным в свойствах проекта.
606 Часть II. Программирование для Windows р) Simple Editor Welcome to the Simple Editor Setup Wizard 1-ia; в П The rotate wi guide you through the steps requied to rotal SmpJe Edtoi on you computer WARNING Tht$ computer program и protected by copyright law and nteinational treaties Unauthorized duplication or dutrfcution of the program, or any portion of it. may resuft r\ severe crv4 or cnmnal penaties. and wl be prosecuted to the maxmum extent potsfcte under the law Puc. 18.30. Диалоговое окно Welcome Окно Read Me После щелчка на кнопке Next (Далее) открывается диалоговое окно Read Me (рис. 18.31). Оно отображает содержимое файла readme.rtf, который был сконфигурирован посредством установки свойства ReadmeFile. j0 Simple Editor  ю Simple Editor Information A Readme.txt Here should be soma information ahoat tlm Simple: P-<litor end the installation» Puc. 18.31. Диалоговое окно Read Me Окно License Agreement Третье открывающееся диалоговое окно — окно лицензионного соглашения License Agreement. Для него сконфигурированы только свойства BannerBitmap и LicenseFile. Переключатели принятия условий лицензионного соглашения добавляются автоматически. Как видно на рис. 18.32, кнопка Next остается отключенной до тех пор, пока не будет выбран переключатель I Agree (Я принимаю условия). Это функционирование диалогового окна определено автоматически.
Глава 18. Развертывание Windows-приложений 607 jtf Simple Editor License Agreement A Please take a moment to read the bcense agreement now If you accept the terms below, cbck '1 Agree", then "Next" Otherwise cfcck "Caned" License Information [here should be some license information about the product I • I Do Not Agree I Agree Canod "] Г~~<В«ск" 1 N*« Puc. 18.32. Диалоговое окно License Agreement Необязательные файлы Принятие условий лицензионного соглашения и щелчок на кнопке Next ведет к отображению диалогового окна Checkboxes (А), показанного на рис. 18.33. При этом должен отобразиться текст, который был определен свойствами Banner Text, BodyText и checkboxlLabel. Остальные флажки невидимы, поскольку значения соответствующих свойств CheckboxVisible установлены в false. Установка флажка приведет к инсталляции файла demo. wroxtext на рабочий стол. j# Simple Editor Optional Files A Instalatron of optional hies Do you want a demo fie put on to the deektop7 Na*> Puc. 18.33. Диалоговое окно Checkboxes (A) Окно Select Installation Folder В диалоговом окне Select Installation Folder (Выберите папку установки), показанном на рис. 18.34, пользователи могут выбрать путь для установки приложения. Для этого диалогового окна можно устанавливать только свойство BannerBitmap.
608 Часть II. Программирование для Windows Путь для установки, отображаемый по умолчанию: [Program Files]\[Изготовитель]\[Имя_продукта] Пользователи могут также указывать, должна установка приложения выполняться для любого пользователя или же только для зарегистрированного пользователя. В зависимости от ответа на этот вопрос ярлык программы будет помещен в персональный каталог пользователя или в каталог All Users (Все пользователи). [ jtf Simple Editor Select Installation Folder The ustalet wi nstal Smpie Edto to the fofc*wng fotdet To nstal r, this (older ckk "Next" To nstal to a dfleren» folder entei it below EoWec |CAPtogram Fie»\Wrox Piess\S«rple EdtoA С kwtal Smple EAorfor yoursef. orfor anyone who usee th» computer Everyone • Justine j Cancel s Л A wrox » cbek "Biowse" Biome ОЛСоА.. | 1 N-> И Puc. 18.34. Диалоговое окно Select Installation Folder Окно Disk Cost Щелчок на кнопке Disk Cost (Объем, занимаемый на диске) ведет к открытию диалогового окна Disk Cost, показанного на рис. 18.35. В нем отображается дисковое пространство всех жестких дисков и вычисляется дисковое пространство, необходимое для установки компонентов приложения на каждом из дисков. Это облегчает пользователю выбор диска, на котором должно быть установлено приложение. J0 Simple Editor D»k SfMCt —ёт- The 1st below includes the drives you can ratal Smpie Ed*o» to. along with each drives avaiable volume Du* Size _j: 74GB SK: 111GB 1<1 ■ AvaiaNe 19GB на i Requ« 1021*1 0 » OK Puc. 18.35. Диалоговое окно Disk Cost Окно Confirm Installation Диалоговое окно Confirm Installation (Подтверждение установки), показанное на рис. 18.36, — последнее, которое открывается перед началом установки. Оно не
Глава 18. Развертывание Windows-приложений 609 содержит никаких дополнительных вопросов и предоставляет последнюю возможность отменить установку до ее начала. & Simp*. Editor l»i^; В1П Confirm Installation /^L ! The mialei и ready to rtstal S«nple Eckto» on you computer Cbck "NeMt" to start the rtttalabon Ствё <9тк Ne*> Puc. 18.36. Диалоговое окно Confirm Installation Окно Progress Диалоговое окно Installing (Выполняется установка), показанное на рис. 18.37, отображает элемент управления протекания процесса во время установки, показывая пользователю, что установка продолжается, и давая приближенное представление о ее продолжительности. Редактор — небольшая программа, поэтому это диалоговое окно завершит свою работу очень быстро. & Svnpte Editor Installing Simple Editor ! Simple E*or в berng ftttaled Please warf с i ncei Рас» U;ai с 1] Щ 1 ЫЫ Puc. 18.37. Диалоговое окно Installing Окно Installation Complete После успешной установки откроется последнее диалоговое окно — Installation Complete (Установка завершена), — показанное на рис. 18.38.
610 Часть П. Программирование для Windows Installation Complete Simple Edtor has been succenfuly rotated Cfcfc "СЫе" to exit Pteate u*e Wndow» Update to check for any cnbcal updates to the NET Fiamewofk < Вас* I Qo- ) Рис. 1838. Диалоговое окно Installation Complete Выполнение приложения Редактор можно запустить, выбирая меню Start"=>AII Programs1^Wrox Simple Editor (Пуск^Все программы1^Простой редактор Wrox. Поскольку мы зарегистрировали расширение файла, существует и другой способ запуска приложения: двойной щелчок на файле с расширением .wroxtext. Если во время инсталляции флажок в диалоговом окне Optional Files был установлен, ярлык файла demo. wroxtext можно найти на рабочем столе. В противном случае такой файл можно создать с помощью уже инсталлированного приложения SimpleEditor. Удаление приложения Если хотите избавиться от приложения Wrox Simple Editor, можно воспользоваться пиктограммой Add/Remove Programs панели управления. Выберите SimpleEditor и щелкните на кнопке Remove (Удалить). Резюме В этой главе описано использование технологии развертывания ClickOnce и функционирование программы установки Windows, включая создание пакетов программы установки с помощью Visual Studio 2008. Программа установки Windows позволяет легко выполнять стандартизованные установки, отмены и восстановления приложений. ClickOnce — новая технология, которая облегчает установку Windows-приложений без необходимости регистрации в качестве системного администратора. Эта технология позволяет без труда выполнять как развертывание, так и обновление клиентских приложений. Если требуется больше функциональных средств, чем предоставляет технология ClickOnce, в этом поможет программа установки Windows. Программа установки Visual Studio 2008 ограничена в своей функциональности и не обладает всеми возможностями программы установки Windows, но для многих приложений ее функций вполне достаточно. Несколько редакторов позволяют конфигурировать сгенерированный файл программы установки Windows. Редактор File System Editor позволяет указывать все файлы и ярлыки. Редактор Launch Conditions Editor может определять некоторые А
Глава 18. Развертывание Windows-приложений 611 обязательные компоненты. Редактор File Types Editor служит для регистрации расширений файлов для различных приложений. Редактор User Interface Editor позволяет легко адаптировать диалоговые окна, используемые в процессе установки. В этой главе рассмотрены следующие вопросы. □ Особенности и побудительные причины использования развертывания ClickOnce. □ Конфигурирование развертывания ClickOnce для Windows-приложений. □ Создание проекта установки с помощью Visual Studio. □ Использование редакторов File System Editor, File Types Editor, Launch Condition Editor и User Interface Editor проекта установки. Упражнения 1. В чем преимущества развертывания по технологии ClickOnce? 2. Как определяются обязательные разрешения при использовании развертывания по технологии ClickOnce? 3. Что определяется манифестом ClickOnce? 4. Когда необходимо использовать программу установки Windows? 5. Какие редакторы можно использовать для создания пакета программы установки Windows с помощью Visual Studio?
ЧАСТЬ Программирование для Web В ЭТОЙ ЧАСТИ... Глава 19. Основы программирования для Web Глава 20. Расширенное программирование для Web Глава 21. Web-службы Глава 22. Программирование с использованием Ajax Глава 23. Развертывание Web-приложений
Основы программирования для Web Windows Forms— это специальная технология, предназначенная для написания приложений, работающих в среде Windows; с помощью ASP.NET можно создавать Web-приложения, которые будут отображаться в любом браузере. ASP.NET позволяет создавать Web-приложения почти так же, как и обычные Windows-приложения. Это возможно благодаря серверным элементам управления, которые абстрагируют HTML-код и имитируют поведение элементов управления Windows. Естественно, между Windows- и Web-приложениями существует множество отличий, которые объясняются отличиями между технологиями HTTP и HTML, применяемыми для создания Web-приложений. В этой главе будет рассказано о принципах программирования Web-приложений на основе ASP.NET, использовании элементов управления Web, управлении состоянием (которое резко отличается от управления состоянием в Windows-приложениях), механизме аутентификации, чтении и записи информации в базу данных. В частности, в этой главе будут рассматриваться следующие темы. □ Что происходит на сервере, когда он получает HTML-запрос от клиента. □ Как создать простую Web-страницу. □ Как используются серверные элементы управления Web на Web-странице (и просмотр генерируемого ними HTML-кода). □ Как добавляются обработчики событий для действий, выполняемых пользователями. □ Как используются элементы управления проверкой для выполнения проверки данных, вводимых пользователями. □ Как осуществляется управление состоянием при помощи различных способов (например, посредством ViewState, cookie-наборов, объектов session, application и cache).
Глава 19. Основы программирования для Web 615 □ Как используются средства аутентификации и авторизации, предлагаемые элементами управления ASP.NET. □ Как отображается и обновляется информация, хранящаяся в базе данных. Обзор Web-приложения вынуждают Web-сервер посылать HTML-код клиенту. Этот код отображается в Web-браузере, таком как Internet Explorer. Когда пользователь вводит URL-адрес в строке адреса браузера, Web-серверу посылается HTTP-запрос. HTTP-запрос содержит имя запрашиваемого файла и следующую дополнительную информацию: строка, идентифицирующая клиентское приложение, языки, которые поддерживаются на стороне клиента, и дополнительные данные, принадлежащие запросу. Web-сервер возвращает HTTP-отклик, содержащий HTML-код, на основе которого Web-браузер показывает пользователю текстовые окна, кнопки и списки. О протоколе HTTP вы можете прочитать в главе 32. ASP.NET — это технология динамического создания Web-страниц с помощью кода на стороне сервера. Эти Web-страницы могут быть созданы разнообразными программами, схожими с клиентскими программами для Windows. Вместо того чтобы непосредственно работать с HTTP-запросами и откликами и вручную создавать HTML-код для отправки клиенту, можно использовать такие элементы управления, как TextBox, Label, ComboBox и Calendar, которые сами создают HTML-код. Исполняющая среда ASP.NET Чтобы использовать ASP.NET для Web-приложений в клиентской системе, необходим простой Web-браузер. Вы можете применять Internet Explorer, Opera, Netscape Navigator, Firefox или любой другой Web-браузер, поддерживающий HTML. При этом устанавливать платформу .NET в системе клиента не нужно. В серверных системах исполняющая среда ASP.NET должна быть инсталлирована. Если в вашей системе установлена служба Internet Information Services (IIS), сервер сконфигурирует исполняющую среду ASP.NET во время инсталляции платформы .NET Framework. В процессе разработки нет необходимости работать с IIS, поскольку Visual Studio имеет собственную среду ASP.NET Web Development Server, подходящую для тестирования и отладки приложений. Давайте рассмотрим типичный Web-запрос, поступающий от браузера, чтобы показать, как работает исполняющая среда ASPNET (рис. 19.1). Допустим, клиент запрашивает на сервере файл default .aspx. Все Web-страницы ASP.NET обычно имеют расширение файла .aspx. Поскольку это расширение файла зарегистрировано в IIS, т.е. известно среде ASP.NET Web Development Server, в действие вступает исполняющая среда ASP.NET и рабочий процесс ASP.NET В момент поступления первого запроса на файл default.aspx запускается синтаксический анализатор ASP.NET, компилятор компилирует файл вместе с файлом С#, который связан с файлом .aspx, после чего создается сборка. Эта сборка затем компилируется в "родной" код компилятором JIT исполняющей среды .NET. Сборка содержит класс Раде, который вызывается для возврата HTML-кода клиенту. После этого производится уничтожение объекта Раде. Сборка хранится для последующих запросов, так что компилировать ее заново нет необходимости.
616 Часть III. Программирование для Web Web-браузер ( Интернет L «( J* ^-v_^- IIS Рабочий процесс ASP.NET Синтаксический анализатор Выполнение Компилятор Рис. 19.1. Действие среды выполнения ASP.NET Создание простой страницы В следующем упражнении мы попытаемся создать простую Web-страницу. В примере приложения, на которое мы будем ссылаться в этой и следующей главе, будет создан простой сайт Event Web, посетители которого могут регистрироваться на получение уведомлений о событиях. Практическое занят*] СоЗДЭНИе ПРОСТОЙ Web-СТрЭНИЦЫ 1. Создайте новый Web-сайт, выбрав в Visual Studio пункт меню File^New^Web Site (Файл^Новый1^Web-сайт). В диалоговом окне New Web Site (Новый Web-сайт), показанном на рис. 19.2, в качестве языка (комбинированное окно списка Language (Язык)) выберите Visual C#, а в комбинированном окне списка Location (Расположение) — элемент File System (Файловая система). При выборе расположения HTTP будет использоваться IIS и будет создан виртуальный каталог. (Выбор файловой системы влияет на используемую локальную файловую систему.) Присвойте Web-сайту имя EventRegistrationWeb. New ШЪ Sit. Templates ; Visual Studio .mulled templates Л ASP NET Web Srte jft ASP NET Web Service ! Щ ASP NET Crystal Reports Web Sit* A WCF Service My Templates | J Search Online Templates.. •*, Empty Web Srte ,£3 ASP.NET Reports Web Srte A blank ASP.NET web site (.NET Framework 3S> Location: File System *\ « BegVCSharp WettPtogrammtng E»entRegistiationWeb 0. Puc. 19.2. Диалоговое окно New Web Site
Глава 19. Основы программирования для Web 617 2. После того как Web-сайт будет создан, в окне редактора будет открыт файл default.aspx. В одной части этого редактора будет показано окно Source View (Представление исходного кода), в другой — Design View (Представление проекта). 3. Таблицу удобно использовать для организации элементов управления. Щелкните на окне Design View и добавьте таблицу, выбрав пункт меню Tabled Insert Table (Таблица^Вставить таблицу). В диалоговом окне Insert Table (Вставка таблицы) установите количество строк равным пяти, а столбцов — двум (рис. 19.3). Miiart Tabtt Rows: |5 Boat: Crfpaddng: | CeJapaong: Sue: 1 Color: ColapseL 1 Background ! Color: 1 HlXebackg 1 -3* Columns: Dab* DefauH |1 ~ |2 V ГО Ш Ш border U Ш В ЗД*«* Ы Iю0 Speofy Г" V V round racture j Set as default for notables ^' .. ..f._ и Л Mdth: —— © In pnceb • In percent height: In percent Ll—aW 1 [Property.. C~< 1 Рис. 19.3. Создание таблицы в диалоговом окне Insert Table 4. Добавьте в таблицу четыре элемента управления Label, три элемента управления TextBox, а также элементы управления DropDownList и Button, как показано на рис. 19.4. Рис. 19.4. Добавление элементов управления 5. Установите свойства для элементов управления, как описано в табл. 19.1. 6. В элементе управления DropDownList выберите свойство Items в окне Properties (Свойства) и введите строки SQL Server 2008 and XML, Office 2007 and XML и Introduction to ASP.NET в окне редактора Listltem Collection Editor (Редактор коллекций Listltem), как показано на рис. 19.5.
618 Часть III. Программирование для Web Таблица 19.1. Значения свойств элементов управления Тип элемента управления Идентификатор (ID) Текст Label Label Label Label DropDownList TextBox TextBox TextBox Button labelEvent labelFirstName labelLastName labelEmail dropDownListEvents textFirstName textLastName textEmail buttonSubmit Event: First name: Last name: Email: Submit Listltem Loilection Editor Members TTotllce 200? end XML II Introduction to ASP-NCT Щ SQL Senrer 2001 and XMl B'operties: а вид, n. Enabled True Selected False Text SQL Server 2008 and X Value SQL Server 20M and X I Рис. 19.5. Окно редактора Listltem Collection Editor 7. Переключите редактор на представление Source View и проверьте, является ли сгенерированный код таким же, как показано ниже: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx. cs"Inherits="_Default" %> <!D0CTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtmll/DTD/xhtmll-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Untitled Page</title> <style type="text/css"> .stylel { } width: 100%; </style> </head> <body> <form id="forml" runat="server"> <div > <table class="stylel">
Глава 19. Основы программирования для Web 619 <tr> <td> <asp:Label ID="labelEvent" runat="server" Text="Event:"> </asp:Label> </td> <td> <asp:DropDownList ID="dropDownListEvents" runat="server"> <asp:ListItem>SQL Server 2008 and XML</asp:ListItem> <asp:ListItem>Office 2007 and XML</asp:ListItem> <asp:ListItem>Introduction to ASP.NET</asp:ListItem> </asp:DropDownList> </td> </tr> <tr> <td> <asp:Label ID="labelFirstName" runat="server" Text="First name:"></asp:Label> </td> <td> <asp:TextBox ID="textFirstName" runat="server"x/asp:TextBox> </td> </tr> <tr> <td> <asp:Label ID="labelLastName" runat="server" Text="Last name:"></asp:Label> </td> <td> <asp:TextBox ID="textLastName" runat="server"x/asp:TextBox> </td> </tr> <tr> <td> <asp:Label ID="labelEmail" runat="server" Text="Email: "> </asp:Label> </td> <td> <asp:TextBox ID="textEmail" runat="server"x/asp:TextBox> </td> </tr> <tr> <td> &nbsp; </td> <td> <asp:Button ID="buttonSubmit" runat="server" Height=6px" Text="Submit" /> </td> </tr> </table> </div> </form> </body> </html>
620 Часть III. Программирование для Web 8. Запустите Web-приложение, выбрав пункт меню Debug^Start Without Debugging (Отладка ^Запуск без отладки). Когда вы запускаете приложение, автоматически запускается и среда ASP.NET Web Development Server. В панели задач проводника Windows вы найдете значок ASP.NET Web Development Server. Дважды щелкните на этом значке, чтобы открыть диалоговое окно, пример которого показан на рис. 19.6. Это диалоговое окно показывает физические и логические пути Web- сервера, а также номер порта, на котором Web-сервер производит прослушивание. Это диалоговое окно можно использовать для останова Web-сервера. [*] ASP.MET Development Server-Poet 5W11 iJLJl Root URL I PWt I Virtual Path j Phy»c«iPath ^^^^^■ hto //tocdhoat 5891l/Ev^ntRcoatrabonWcb {58911 | "Evert RooMtrahonWeb |c ^Bвg\^Shaф\WвbProgramп«ng\EvвrtRвoяtraЬonVVвb\ L *• у Рис. 19.6. Параметры Web-сервера Когда вы запускаете приложение, в окне браузера Internet Explorer отображается Web-страница, показанная на рис. 19.7. Чтобы просмотреть HTML-код, выберите пункт меню View^Source (Вид"^Исходный код). Вы увидите, что серверные элементы управления преобразуются в чистый HTML-код. i--.eL.SJ' ^yj'fc w <* aunttttdPaot |«t|x!UiVeSee/r*i : a -^ 0 Event First na Lastna. Emai SQL Server 2008 and XML Puc. 19.7. Простая Web-страница Описание полученных результатов В первой строке файла default. aspx содержится директива страницы: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> Эта директива определяет язык программирования и используемые классы. Свойство AutoEventWireup="true" автоматически связывает обработчики событий с определенными именами методов, как будет показано ниже.
Глава 19. Основы программирования для Web 621 Часть Inherits="_Default" означает, что класс, динамически генерируемый из файла ASPX, является наследником класса Default. Этот базовый класс определен в файле отделенного кода Default. aspx. cs, который указан в свойстве CodeFile. Visual Studio позволяет использовать файл отделенного кода с отдельными файлами CS или встраивать код, когда операторы С# могут помещаться непосредственно в файл ASPX. Создавая новый элемент с помощью пункта меню File^New^File (Файл^Новый^Файл), вы можете указать, будет ли данный код помещен в отдельный файл (см. рис. 19.8.). 1 : Templates: : Visual Studio installed templates ^J Web For» 1 *p A0O.NET Entity Data Model j) AJAX Client Library : В AJAX-enabled WCF Service ^| Class Diagram | ф] Global Application Class | ! jjJUNQ to SQL Classes j ^Resource File (j SQL Database i ; ^WCFServxe jjxMLF.Ie j 2 Crystal Report A form fof Web Applications ! Name; Defauit2.aspx j Language: Visual C» П Master Page .jcj AlAx Client Behavior H AJAX Master Page ^Browser File Aj DataSet •) HTML Page ijj) Report ,7J Site Map Aj Style Sheet _^ 0.eb Configuration File jy XML Schema » V Place code in Q Select master i Web User Control 4] AJAX Client Control §§ AJAX Web Form «£ Class «J Generic Handler jJJScnptFHe <J Report Wizard £ Skin File jj Text File Sj Web Service J XSLT File separate file page Hi я И шЩ\\ dl *i 11 Add Рис. 19.8. Создание нового элемента Разделяя пользовательский интерфейс и код в файлах ASPX и CS, вы тем самым улучшаете возможности для поддержки кода. Код, который генерируется в файле Default. aspx. cs, импортирует некоторые пространства имен и включает частичный класс Default. Автоматически генерируемый класс в файле Default.aspx является наследником класса Default. Чуть позже в этой главе мы добавим код обработчика в файл CS. using System; using System.Data; using System.Configuration; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using System.Xml.Linq; public partial class _Default : System.Web.UI.Page protected void Page_Load(object sender, EventArgs e)
622 Часть III. Программирование для Web Назначение ключевого слова partial в предыдущем коде рассматривается в главе 10. Ниже показан код страницы ASPX. Клиент получает простой HTML-код как есть, в его первоначальном виде; в нем всего лишь был удален атрибут runat="server" из дескриптора <head>: <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Untitled Page</title> <style type="text/css"> .stylel { width: 100%; } </style> </head> <body> Вы увидите также другие HTML-элементы с атрибутом runat="server", например, <form>. Посредством атрибута runat="server" серверный элемент управления ASP.NET связывается с дескриптором HTML. Этот элемент управления может использоваться для написания кода на стороне сервера. "За кулисами" элемента <form> находится объект, имеющий тип System.Web.UI .HtmlControls.HtmlForm. Этот объект содержит имя переменной forml, как определено в атрибуте id. Переменная forml может использоваться для вызова методов и свойств класса Html Form. Объект Html Form создает дескриптор <f orm>, который посылается клиенту: <form id="forml" runat="server"> Естественно, атрибут runat не посылается клиенту. Стандартные элементы управления, которые вы перенесли из панели инструментов Toolbox в окно Forms Designer, имеют элементы, код которых начинается со строки <asp: — <asp:Label> и <asp:DropDownList>. Это не что иное, как серверные элементы управления ASP.NET, связанные с классами .NET в пространстве имен System.Web.UI .WebControls. Элемент управления <asp:Label> представлен классом Label, a <asp: DropDownList> — классом DropDownList: <td> <asp:Label ID="labelEvent" runat="server" Text="Event:"></asp:Label> </td> <td> <asp:DropDownList ID="dropDownListEvents" runat="server"> <asp:ListItem> SQL Server 2008 and XML </asp:ListItem> <asp:ListItem> Office 2007 and XML </asp:ListItem> <asp:ListItem> Introduction to ASP.NET </asp:ListItem> </asp:DropDownList> </td> <asp: Label> не посылает элемент <asp: Label> клиенту, поскольку он не является действительным элементом HTML. Вместо этого <asp:Label> возвращает дескриптор <span>. Подобным образом <asp:DropDownList> возвращает элемент <select>, a <asp:TextBox> — элемент <input type="text">. ASP.NET имеет классы элементов управления интерфейса пользователя в пространствах имен System. Web. UI. HtmlControls и System. Web. UI. WebControls. Каждое из этих пространств имен имеет несколько похожих элементов управления, известных также как серверные элементы управления HTML и серверные элементы управления Web. В качестве примеров можно упомянуть серверный элемент управления HTML
Глава 19. Основы программирования для Web 623 Html Input Text и серверный элемент управления Web Text Box. Серверные элементы управления HTML обладают методами и свойствами, похожими на методы и свойства серверных элементов управления Web, поскольку доступ к ним может быть осуществлен из кода JavaScript на клиентской HTML-странице. Какой вы выберете элемент управления для работы — зависит от ваших предпочтений. Однако благодаря серверным элементам управления Web, вы откроете для себя гораздо более сложные элементы управления, такие как Calendar, DataGrid и Wizard. Серверные элементы управления В табл. 19.2 перечислены наиболее важные серверные элементы управления Web, доступные в ASP.NET, а также код HTML, который они возвращают. Таблица 19.2. Важные серверные элементы управления Web Элемент управления Код HTML Описание Label Literal TextBox Button LinkButton ImageButtin HyperLink ListBox <span> статический текст <input type="text"> <input type="submit"> <a href="javascript: dopostbackO "> <input type="image"> <a> DropDownList <select> <select size=""> Возвращает элемент span, содержащий текст Возвращает простой статический текст. С помощью этого элемента управления можно преобразовывать содержимое в зависимости от того, какое приложение используется на стороне клиента Возвращает HTML-код <input type="text">, благодаря чему пользователь может вводить некоторые значения. Вы можете написать обработчик событий, который будет работать на стороне сервера в случае изменений текста Посылает значения формы на сервер Создает анкерный дескриптор, который включает код JavaScript для обратной отправки серверу Генерирует дескриптор, имеющий тип image, для отображения изображения, на которое указывает ссылка Создает простой анкерный дескриптор, который ссылается на Web-страницу Создает дескриптор select, благодаря чему пользователь может видеть один элемент и выбрать один из множества элементов, щелкая на раскрывающемся списке Создает дескриптор выбора с атрибутом size, который будет показывать множество элементов одновременно
624 Часть III. Программирование для Web Окончание табл. 19.2 Элемент управления Код HTML Описание CheckBox <input type="checkbox"> RadioButton Image Calendar TreeView <input type="radio"> <img src=""> <table> <div><table> Возвращает элемент input, имеющий тип checkbox, для отображения кнопки, которую можно выделять/снимать выделение. Вместо того чтобы использовать CheckBox, вы можете использовать checkBoxList, который создает таблицу, состоящую из множества элементов checkbox Возвращает элемент input, имеющий тип radio. Благодаря этому элементу переключателя, можно выбрать только одну кнопку из группы. Подобно CheckBoxList, элемент типа RadioButtonList предлагает список кнопок Возвращает дескриптор img для отображения файла формата GIF или JPG на стороне клиента Отображает полный календарь, в котором можно выбрать дату, изменить месяц и т.п. Для создания выходных данных генерируется HTML-таблица с кодом JavaScript Возвращает дескриптор div, включающий множество дескрипторов table, в зависимости от его содержимого. Для открытия и закрытия дерева на стороне клиента используется код JavaScript Обработчики событий Серверные элементы управления Web могут включать обработчики событий, вызов которых производится на стороне сервера. Элемент управления Button может включать событие Click; элемент управления DropDownList предлагает событие SelectedlndexChanged, а элемент управления TextBox — событие TextChanged. События на сервере возникают только в том случае, если возникает обратная отправка. Если в текстовом окне происходит изменение значения, событие TextChanged не возникает немедленно вслед за этим изменением; оно инициируется только тогда, когда произойдет отправка формы на сервер, а это возможно лишь после нажатия кнопки Submit (Отправить). Прежде чем вызывать обработчик события, исполняющая среда ASP.NET проверяет, изменилось ли состояние элемента управления. Например, если в элементе управления DropDownList был выбран другой пункт, возникнет событие SelectedlndexChanged; другой пример— событие TextChanged возникает в том случае, если произошло изменение значения в текстовом окне. Если необходимо, чтобы событие изменения было немедленно отправлено на сервер (например, в случае изменения выбранного пункта в элементе управления DropDownList), можете присвоить свойству AutoPostback значение true. Таким образом, код JavaScript на стороне клиента используется для немедленной отправки данных формы на сервер. Естественно, вследствие этого возрастает объем сетевого трафика, поэтому данное средство следует применять с осторожностью.
Глава 19. Основы программирования для Web 625 Проверка старых значений на основе новых значений в элементе управления производится посредством поля ViewState — скрытого поля, которое посылается браузеру вместе с содержимым страницы. При отправке страницы клиенту это поле будет содержать те же значения, что и значения в элементах управления на форме. При обратной отправке на сервер поле ViewState посылается вместе с новыми значениями элементов управления. Таким образом, его можно проверить и узнать, были ли изменены значения, и по результатам проверки вызвать обработчик события. До настоящего момента наше приложение посылало клиенту только простую страницу. Теперь мы постараемся выяснить, что можно сделать с данными, которые вводит пользователь. В нашем первом примере данные, введенные пользователем, отображаются сначала на той же странице, после чего для отображения открывается другая страница. В следующем упражнении вы научитесь отображать данные, введенные пользователем. Отображение данных, введенных пользователем 1. В Visual Studio откройте созданное ранее Web-приложение EventRegistration Web. 2. Чтобы отобразить введенные пользователем данные для регистрации события, добавьте метку labelResult на Web-страницу Default. aspx. 3. Дважды щелкните на кнопке Submit (Отправить), чтобы добавить обработчик события Click к этой кнопке. В файл Default. aspx. cs добавьте следующий код для обработчика события: public partial class _Default { protected void buttonSubmit_Click(object sender, EventArgs e) { string selectedEvent = dropDownListEvents.SelectedValue; string firstName = textFirstName.Text; string lastName = textLastName.Text; string email = text Email. Text; labelResult.Text = String.Format("{0} {1} selected the event {2}",firstName, lastName, selectedEvent); } } 4. Запустите Web-страницу в Visual Studio. После того как вы введете данные и щелкнете на кнопке Submit, в новой метке на этой же странице будут отображены данные, которые ввел пользователь. Описание полученных результатов При двойном щелчке на кнопке Submit происходит добавление атрибута OnClick в элемент <asp: Button> в файле Default. aspx: <asp:Button ID="buttonSubmit" runat="server" Text="Submit" OnClick="buttonSubmit_Click" /> Для серверного элемента управления Web OnClick определяет событие Click на стороне сервера, которое будет вызвано при щелчке на кнопке. Внутри реализации метода buttonSubmitClick () значения элементов управления можно прочитать благодаря свойствам. dropDownListEvents — это переменная,
626 Часть III. Программирование для Web которая ссылается на элемент управления DropDownList. В файле ASPX идентификатору (ID) присвоено значение dropDownListEvents, поэтому переменная создается автоматически. Свойство SelectedValue возвращает текущий вариант выбора. Для элементов управления TextBox свойство Text возвращает строки, которые были введены пользователем: string selectedEvent = dropDownListEvents.SelectedValue; string firstName = textFirstName.Text; string lastName = textLastName.Text; string email = textEmail.Text; Метка labelResult снова имеет свойство Text, которому присваивается результат ввода: labelResult.Text = String.Format("{0} {1} selected the event {2}", firstName, lastName, selectedEvent); Вместо того чтобы отображать результаты на той же странице, ASP.NET позволяет без труда отобразить результаты на другой странице, как это показано в следующем практическом занятии. Отображение результатов на другой странице 1. Создайте новую форму WebForm и присвойте ей имя Results Page. aspx. 2. Добавьте метку в форму ResultsPage и присвойте ей имя labelResult. 3. Добавьте код метода Page_Load в класс ResultsPage, как показано ниже: public partial class _ResultsPage : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { try { DropDownList dropDownListEvents = (DropDownList) PreviousPage.FindControl ("dropDownListEvents") ; string selectedEvent = dropDownListEvents.SelectedValue; string firstName = ((TextBox)PreviousPage.FindControl("textFirstName")) .Text; string lastName = ((TextBox)PreviousPage.FindControl("textLastName")).Text; string email = ((TextBox)PreviousPage.FindControl("textEmail")).Text; labelResult.Text = String.Format("{0} {1} selected the event {2}", firstName, lastName, selectedEvent); ) catch { labelResult.Text = "The originating page must contain " + "textFirstName, textLastName, textEmail controls"; ) } } 4. Присвойте свойству PostbackUrl кнопки Submit страницы Default.aspx значение ResultsPage.aspx.
Глава 19. Основы программирования для Web 627 5. Обработчик события Click кнопки Submit можно удалить, поскольку он не требуется. 6. Откройте страницу Default. aspx, введите данные и щелкните на кнопке Submit. Вы перейдете на страницу ResultsPage.aspx, на которой будут отображены данные, введенные пользователем. Описание полученных результатов В ASP.NET элемент управления Button реализует свойство PostbackUrl, чтобы определить страницу, которая была запрошена на Web-сервере. Это свойство создает код JavaScript на стороне клиента для запроса определенной страницы посредством обработчика onclick кнопки Submit на стороне клиента: <input type="submit" name="buttonSubmit" value="Submit" onclick="javascript:webForm_DoPostBackWithOptions ( new WebForm_PostBackOptions(&quot;buttonSubmit&quot;, &quot;&quot;, false, &quot; &quot;, &quot;ResultPage.aspx&quot;, false, false))" id="buttonSubmit" /> Браузер посылает все данные из формы, содержащейся на первой странице, на новую страницу. Однако внутри новой запрошенной страницы необходимо получить данные из элементов управления, которые были определены на предыдущей странице. Чтобы получить доступ к элементам управления на предыдущей странице, класс Page определяет свойство PreviousPage. Это свойство возвращает объект Page, в котором доступ к элементам управления на этой странице может быть осуществлен посредством метода FindControl (). Метод FindControl () возвращает объект Control, поэтому вам придется привести возвращаемое значение к типу того элемента управления, поиск которого производится: DropDownList dropDownListEvents = (DropDownList)PreviousPage.FindControl("dropDownListEvents"); Вместо того чтобы использовать метод FindControl () для доступа к значениям на предыдущей странице, доступ к предыдущей странице может быть строго типизирован, что позволит свести к минимуму ошибки во время проектирования. Чтобы пойти этим путем, в следующем упражнении мы постараемся определить специальную структуру, возвращаемую посредством свойства класса def aultaspx. Практическое занятие СОЗДЭНИе СТРОГО ТИПИЗИрОВаННОГО свойства PreviousPage 1. Создайте папку AppCode в Web-приложении, выбрав в окне Solution Explorer папку Web, а затем пункт меню Website^Add Folder^App^ode Folder (Web-caHT1^ Добавить папку^Папка App_Code). 2. В окне Solution Explorer выберите папку AppCode и добавьте новый файл с кодом С#, присвоив ему имя RegistrationInformation.cs, выбрав пункт меню Website^Add New Item^Class (Web-сайт^Добавить новый элемент^Класс). 3. Реализуйте структуру Registrationlnf ormation в файле Registrationlnf ormation. cs, как показано ниже: public struct Registrationlnformation { public string FirstName { get; set; } public string LastName { get; set; }
628 Часть III. Программирование для Web public string Email { get; set; } public string SelectedEvent { get; set; } } 4. Добавьте общедоступное свойство Registrationlnformation в класс Default в файле Default. aspx. cs: public Registrationlnformation Registrationlnformation { get { return new Registrationlnformation () { FirstName = textFirstName.Text, LastName = textLastName.Text, Email = textEmail.Text, SelectedEvent = dropDownListEvents.SelectedValue }; } } 5. Добавьте директиву Previous PageType в файл Result Page. aspx после директивы Page: < %@ Page Language="C#" AutoEventWireup="true" CodeFile="ResultPage.aspx.cs" Inherits="ResultPage_aspx" % > < %@ PreviousPageType VirtualPath="-/Default.aspx" % > 6. Теперь можно упростить код в методе PageLoad () класса Results Page: protected void Page_Load(object sender, EventArgs e) { try { Registrationlnformation ri = Previous Page. Registrationlnf ormation ; labelResult.Text = String.Format("{0} {1} selected the event {2}", ri.FirstName, ri.LastName, ri.SelectedEvent); } catch { labelResult.Text = "The originating page must contain " + "textFirstName, textLastName, textEmail controls"; } } Проверка данных, вводимых пользователем Когда пользователь вводит данные, их необходимо проверить, удостоверившись, что они являются допустимыми. Такую проверку можно выполнять как на стороне клиента, так и на стороне сервера. Проверка данных на стороне клиента может быть выполнена с помощью кода JavaScript. Однако в этом случае их необходимо проверять также и на сервере, поскольку ни одному из клиентов нельзя доверять полностью. Можно запретить выполнение кода JavaScript в браузере, хотя хакеры смогут воспользоваться другими функциями JavaScript, которые принимают неправильно введенные данные. Данные на сервере необходимо проверять обязательно. Проверка данных на стороне клиента позволяет повысить производительность, поскольку в этом случае отпадает необходимость повторно обращаться на сервер.
Глава 19. Основы программирования для Web 629 Используя ASP.NET, вам не нужно самому писать функции проверки данных, поскольку ASP.NET предлагает множество элементов управления проверкой, как на стороне клиента, так и на стороне сервера. В следующем примере показан элемент управления проверкой RequiredField Validator, который связан с текстовым окном textFirstName. Все элементы управления проверкой имеют общие свойства ErrorMessage и ControlTotalValidate. Если данные, введенные пользователем, не являются действительными, свойство ErrorMessage определяет, какое сообщение об ошибке будет отображено. По умолчанию сообщение об ошибке отображается в том месте, где находится элемент управления проверкой. Свойство ControlToValidate определяет элемент управления, в котором будет производиться проверка вводимых данных. <asp:TextBox ID="textFirstname" runat="server"></asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidatorl" runat="server" ErrorMessage="Enter your first name" ControlToValidate="textFirstName"> </asp:RequiredFieldValidator> В табл. 19.3 перечислены и описаны элементы управления проверкой. Таблица 19.3. Элементы управления проверкой Элемент управления Описание RequiredFieldValidator RangeValidator RegularExpressionValidator CompareValidator CustomValidator ValidationSummary Определяет, что для проверяемого элемента управления требуются входные данные. Если элемент управления, подлежащий проверке, имеет исходное значение, которое пользователь намерен изменить, вы можете задать это исходное значение в свойстве initiaivalue элемента управления проверкой Определяет минимальное и максимальное значение, которое может ввести пользователь. Специфическими свойствами этого Элемента управления ЯВЛЯЮТСЯ MinimumValue И MaximumValue С помощью свойства ValidationExpression можно задать регулярное выражение на основе синтаксиса языка Perl 5, с помощью которого можно проверять данные, введенные пользователем Сравнивает множество значений (например, пароли). Это средство проверки не только поддерживает функцию сравнения двух значений, но и предлагает дополнительные опции, которые можно использовать посредством свойства Operator. СВОЙСТВО Operator имеет тип ValidationCompareOperator, который определяет Значения перечисления, такие как Equal, NotEqual, GreaterThan и DataTypeCheck. Благодаря DataTypeCheck можно проверить введенное значение, чтобы выяснить, имеет ли оно определенный тип данных (например, является ли корректной дата) Если остальные элементы управления проверкой не удовлетворяют требованиям проверки, можно воспользоваться этим элементом управления. С его помощью можно определить функцию проверки данных как на стороне сервера, так и на стороне клиента Вместо сообщений об ошибках записывает итоговую информацию о странице непосредственно в элементы управления ввода В уже созданном нами приложении пользователь может ввести имя, фамилию и адрес электронной почты. В следующем практическом занятии мы расширим возможности нашего приложения за счет применения элементов управления проверкой.
630 Часть III. Программирование для Web практическое занятие Проверка имени, фамилии и адреса электронной почты 1. Откройте ранее созданный проект EventRegistrationWeb с помощью Visual Studio. 2. Откройте файл default. aspx. 3. Добавьте новый столбец в таблицу, выделив в окне Design View редактора правый столбец и выбрав пункт меню Tabled Insert^ Column to the Right (Таблица^ Вставить1^Столбец справа). 4. Имя, фамилия и адрес электронной почты — это обязательная информация для ввода. Проверка выполняется для того, чтобы выяснить, правильно ли записан адрес электронной почты. Добавьте три элемента управления RequiredFieldValidator и один элемент управления RegularExpressionValidator, как показано на рис. 19.9. Eveot | SQL Server 2008 and XML jj First name: | Last name: Г Emau | Submit J [labdRest*] RequiredFiekiValidator Reqm edFicldVakdaior Requit edlieldVabdatot Regufcn Exp ession Validator Рис. 19.9. Добавление элементов управления проверкой 5. Сконфигурируйте элементы управления проверкой так, как показано в табл. 19.4. Таблица 19.4. Значения свойств элементов управления проверкой Элемент управления проверкой Свойство Значение RequiredFieldValidator1 RequiredFieldValidator2 RequiredFieldValidator3 RegularExpressionValidator1 ErrorMessage ControlToValidate ErrorMessage ControlToValidate ErrorMessage ControlToValidate Display ErrorMessage ControlToValidate ValidationExpression Display First name is required. textFirstName Last name is required. textLastName Email is required. textEmail Dynamic Enter a valid email. textEmail \w+([-+.']\w+)*@\w+([- w+)*\.\w+([-.]\w+)* Dynamic 6. Вводить регулярное выражение вручную нет необходимости. Вместо этого можете щелкнуть на кнопке с многоточием напротив свойства ValidationExpression в окне Properties (Свойства), чтобы открыть редактор Regular Expression Editor (Редактор регулярных выражений), показанный на рис. 19.10. Этот редактор
Глава 19. Основы программирования для Web 631 предлагает некоторые предварительно определенные регулярные выражения, включая регулярное выражение для проверки адреса электронной почты. Rtgutar Expression Editor Standard expressions: I (Custom) : French phone number ! French postal code German phone number : German postal code » a МИНИН!' Validation expression Рис. 19.10. Редактор регулярных выражений 7. Если обратная отправка выполняется на страницу, которая отличается от страницы, включающей элементы управления проверкой (это определяется с помощью свойства PostBackUrl, которое было задано ранее), то проверку действительности результата на новой странице нужно проверить с помощью свойства IsValid. Добавьте следующий код в метод PageLoad () класса ResultsPage: protected void Page_Load(object sender, EventArgs e) try { if (!PreviousPage.IsValid) { labelResult.Text return; "Error in previous page"; } //.. 8. Теперь можно запустить приложение. Когда данные не вводятся, или вводятся неправильно, элементы управления проверкой отображают сообщения об ошибках, как показано на рис. 19.11. £ Untitled Page ■ Window* Internet Explorer QO-s> *|*t| X ||i,ve-S«m;n P - W 4* :pumMdPaet Л --"V 0 - «ft - :*•**' Event First na Lastna Introduction to ASP NET *• Frst name is required Last name is rcquied. Eater a valid email «4 Local* Off •*;ioo% Puc. 19.11. Отображение сообщений об ошибках элементами управления проверкой
632 Часть III. Программирование для Web Описание полученных результатов Элементы управления проверкой создают код JavaScript на стороне клиента для проверки вводимых данных на стороне клиента и код на стороне сервера для проверки введенных данных на сервере. Код JavaScript можно отключить, присвоив свойству EnabledientScript значение false. Вместо того чтобы изменять свойство в каждом элементе управления проверкой, код JavaScript можно отключить, присвоив свойству ClientTarget класс Page. В зависимости от типа клиента, элементы управления ASP.NET могут возвращать JavaScript-код клиенту. Это поведение зависит от значения свойства ClientTarget. По умолчанию этому свойству присваивается значение "automatic", в соответствии с которым, в зависимости от функций Web-браузера, код сценариев либо возвращается, либо нет. Если свойство ClientTarget имеет значение "downlevel", код сценариев не возвращается клиентам, а если оно имеет значение "uplevel", то код сценариев будет возвращаться всегда. Установить значение свойства ClientTarget можно внутри метода Page_Load () класса Page: protected void Page_Load(object sender, EventArgs e) { ClientTarget = "downlevel"; Управление состоянием Протокол HTTP не имеет состояний. Соединение, которое клиент устанавливает с сервером, может быть закрыто после любого запроса. И все же обычно бывает необходимо запоминать некоторую информацию клиента от страницы к странице. Это можно сделать несколькими способами. Главное отличие между этими разными способами заключается в том, где будет храниться информация о состоянии: на сервере или на клиенте. В табл. 19.5 описаны способы управления состоянием и указано время, на протяжении которого состояние может считаться действительным. Таблица 19.5. Способы управления состоянием Тип состояния Ресурс: клиент или сервер Время действия Состояние представления (ViewState) Cookie-набор Сеанс (Session) Приложение (Application) Кэш (Cache) Клиент Клиент Сервер Сервер Сервер Только в рамках одной страницы Временные cookie-наборы удаляются при закрытии браузера; постоянные cookie-наборы хранятся на диске клиентской системы Состояние сеанса связано с сеансом браузера. Сеанс становится недействительным по истечении некоторого периода времени (по умолчанию он составляет 20 минут) Состояние приложения разделяется между всеми клиентами. Это состояние остается действительным до момента перезагрузки сервера Подобно состоянию приложения, состояние кэша тоже разделяется среди клиентов. Однако если кэш необходимо сделать недействительным, существует более эффективный способ управления
Глава 19. Основы программирования для Web 633 Теперь давайте ознакомимся с этими способами более подробно. Управление состоянием на стороне клиента В этом разделе мы поговорим об управлении состоянием на стороне клиента, рассмотрев два способа: ViewState и cookie-наборы. ViewState Как уже говорилось, одним из способов хранения информации о состоянии на стороне клиента является ViewState. Этот способ используется автоматически серверными элементами управления Web для обеспечения работы событий. ViewState содержит то же состояние, что и элемент управления на момент отправки информации клиенту. Когда браузер посылает форму обратно на сервер, ViewState будет содержать исходные значения, а значения посылаемых элементов управления будут новыми. Если между ними будет существовать различие, будут вызваны соответствующие обработчики событий. Недостатком использования ViewState является то, что данные всегда передаются от сервера клиенту и наоборот, в результате чего увеличивается объем передаваемых данных по сети. Чтобы сократить объем сетевого трафика, ViewState можно отключить. Чтобы сделать это для всех элементов управления на странице, нужно присвоить свойству EnableViewState значение false посредством директивы Page: <%@ Page Language="C#" AutoEventWireUp="true" CodeFile="Default.aspx.cs" Inherits="_Default" EnableViewState="false" %> ViewState можно также сконфигурировать для отдельного элемента управления, присвоив соответствующее значение его свойству EnableViewState. Независимо от того, что будет содержаться в конфигурации страницы, если свойство EnableViewState элемента управления будет определено, то будет использоваться значение элемента управления. Значение конфигурации страницы применяется только для этих элементов управления, если состояние ViewState не сконфигурировано. Внутри ViewState можно хранить некоторые специальные данные. Это можно сделать за счет индексатора свойства ViewState класса Page. Вы можете определить имя, которое будет использоваться для доступа к значению ViewState посредством аргумента index: ViewState["mydata"] = "my data"; Прочитать предварительно сохраненное значение ViewState можно следующим образом: string mydata = (string)ViewState["mydata"]; В HTML-коде, который посылается клиенту, вы можете видеть значение ViewState для всей страницы внутри скрытого поля: <input type="hidden" name=" VIEWSTATE" value="/wEPDwUKLTU4NzY5NTcwNw8WAh4HbXlzdGF0ZQUFbX12YWwWAgIDD2QWAg IFDw8WAh4EVGV4dAUFbX12YWxkZGTCdCywUOcAW97aKpcjtltzJ7ByUA==" /> Использование скрытых полей дает преимущество в том, что каждый браузер может использовать это средство, а пользователь не может его отключить. Информация ViewState сохраняется только внутри страницы. Если состояние должно быть действительным для различных страниц, то для хранения состояния на стороне клиента следует использовать cookie-наборы.
634 Часть III. Программирование для Web Cookie-наборы Cookie-набор определяется в заголовке HTTP. Класс HttpResponse используется для отправки cookie-набора клиенту. Response — это свойство класса Page, которое возвращает объект типа HttpResponse. Класс HttpResponse определяет свойство Cookies, которое возвращает HttpCookieCollection. Посредством HttpCookieCollection можно возвращать клиенту множество cookie-наборов. Следующий пример кода показывает, как можно отправить cookie-набор клиенту. Сначала создается экземпляр объекта HttpCookie. В конструкторе этого класса задается имя cookie-набора — в данном случае это mycookie. Класс HttpCookie имеет свойство, позволяющее добавлять множество значений cookie-наборов. Если необходимо вернуть только одно значение cookie-набора, вы можете использовать свойство Value. Однако если вы планируете отправлять множество значений cookie-наборов, лучше добавить значения в один cookie-набор, чем использовать множество cookie-наборов: string myval = "myval"; HttpCookie cookie = new HttpCookie("mycookie"); cookie.Values.Add("mystate", myval); Response.Cookies.Add(cookie); Cookie-наборы могут быть временными и действительными внутри сеанса браузера или же храниться на диске клиента. Чтобы сделать cookie-набор постоянным, свойству Expires необходимо присвоить объект HttpCookie. Посредством свойства Expires определяется срок окончания действия cookie-набора; здесь было выбрано три месяца с момента текущей даты. Несмотря на то что можно указать конкретную дату, нет гарантии того, что cookie- набор будет храниться вплоть до этой даты. Пользователь может удалить cookie-набор, да и сам браузер тоже может удалять cookie-наборы, если на локальном компьютере их хранится слишком много. Браузер имеет ограничение в 20 cookie-наборов для одного сервера и 300 cookie-наборов для всех серверов. Когда этот лимит будет исчерпан, неиспользуемые в течение некоторого времени cookie-наборы будут удалены. HttpCookie cookie = new HttpCookie("mycookie"); cookie.Values.Add("mystate", "myval"); cookie.Expires = DateTime.Now.AddMonthsC); Response.Cookies.Add(cookie); Когда клиент запрашивает страницу на сервере, и cookie-набор для данного сервера доступен на стороне клиента, cookie-набор отправляется на сервер как часть HTTP- запроса. Чтение cookie-наборов на странице ASP.NET может быть выполнено посредством доступа к коллекции cookies в объекте HttpRequest. Подобно HTTP-отклику, класс Page имеет свойство Request, которое возвращает объект типа HttpRequest. Свойство Cookies возвращает коллекцию HttpCookieCollection, которую теперь можно применять для чтения cookie-наборов, посланных клиентом. Доступ к cookie-набору может быть осуществлен по его имени с помощью индексатора, а для получения значения из cookie-набора может использоваться свойство Values объекта HttpCookie: HttpCookie cookie = Request.Cookies["mycookie"]; string myval = cookie.Values["mystate"]; ASP.NET облегчает работу с cookie-наборами, однако вы должны знать об ограничениях при их использовании. Напомним, что браузер принимает только 20 cookie- наборов от одного сервера и 300 cookie-наборов от всех серверов. Существует также ограничение на размер cookie-файла: он не должен превышать 4 Кбайт. Эти ограни-
Глава 19. Основы программирования для Web 635 чения позволяют гарантировать, что диск клиента не будет заполнен одними лишь cookie-наборами. Управление состоянием на стороне сервера Вместо того чтобы запоминать состояние с помощью клиента, его можно запоминать также и с помощью сервера. Характерным недостатком использования состояния на стороне клиента является возрастающий объем передаваемых по сети данных. Характерным недостатком использования состояния на стороне сервера является то, что сервер вынужден перераспределять ресурсы для своих клиентов. Давайте внимательно рассмотрим способы управления состоянием на стороне сервера. Состояние сеанса Состояние сеанса (session) связано с состоянием браузера. Сеанс начинается, когда клиент впервые открывает страницу ASP.NET на сервере, и заканчивается, когда клиент не обращается к серверу в течение 20 минут. Вы можете определить ваш собственный код, который должен выполняться вначале сеанса или при его завершении внутри Global Application Class (Глобальный класс приложения). Чтобы создать Global Application Class, выберите пункт меню Website^Add New Item1^Global Application Class (Web-сайт^Добавить новый элемент^Глобальный класс приложения). С помощью этого класса создается файл global.aspx. Внутри этого файла будут определены некоторые обработчики событий: <%@ Application Language="C#" %> <script runat="server"> void Application_Start(Object sender, EventArgs e) { // Код, выполняющийся во время запуска приложения. } void Application_End(Object sender, EventArgs e) { // Код, выполняющийся во время завершения работы приложения. } void Application_Error(Object sender, EventArgs e) { // Код, выполняющийся в случае возникновения необработанных ошибок. } void Session_Start(Object sender, EventArgs e) { // Код, выполняющийся при запуске нового сеанса. } void Session_End(Object sender, EventArgs e) { // Код, выполняющийся при завершении сеанса. // Примечание. Событие Session_End возникает только тогда, // когда режим sessionstate имеет значение // InProc в файле Web.config. Если режим сеанса имеет // значение StateServer или SQLServer, событие не возникает. } </script> Состояние сеанса может храниться внутри объекта HttpSessionState. Доступ к объекту состояния сеанса, который связан с текущим содержимым HTTP, может быть осуществлен посредством свойства Session класса Page. В обработчике событий SessionStart () можно инициализировать переменные сеанса; в примере, показанном ниже, состоянию сеанса mydata присваивается значение 0: void Session_Start(Object sender, EventArgs e) { // Code that runs on application startup Session["mydata"] = 0; }
636 Часть III. Программирование для Web Опять-таки, чтение информации о состоянии сеанса может быть выполнено посредством свойства Session и имени состояния сеанса: void Buttonl_Click(object sender, EventArgs e) { int val = (mt) Session ["mydata"] ; Labell.Text = val.ToString (); val += 4; Session["mydata"] = val; } Чтобы установить связь между клиентом и его переменными сеанса, по умолчанию ASP.NET использует временный cookie-набор, содержащий идентификатор сеанса. ASP.NET также поддерживает сеансы без cookie-наборов, в которых URL-идентификаторы используются для отображения HTTP-запросов в виде того же сеанса. Состояние приложения Если данные необходимо разделять между разными клиентами, то в этом случае можно использовать состояние приложения (application). Состояние приложения может применяться примерно так же, как и состояние сеанса. В этом случае используется класс HttpApplicationState, доступ к которому может осуществляться через свойство Application класса Page. В примере, показанном ниже, инициализируется переменная приложения userCount во время запуска Web-приложения. Когда происходит первый запуск страницы ASP.NET Web-сайта, вызывается метод обработчика события ApplicationStart () в файле global. a sax. Эта переменная используется для подсчета каждого пользователя, осуществляющего доступ к Web-сайту: void Application_Start(Object sender, EventArgs e) { // Code that runs on application startup Application["userCount"] = 0; } В обработчике события SessionStart () происходит приращение значения переменной приложения userCount. Прежде чем изменять переменную приложения, необходимо заблокировать объект приложения посредством метода Lock(); в противном случае могут возникнуть проблемы с работой потоков, поскольку множество клиентов могут одновременно обращаться к переменной приложения. После того как будет изменено значение переменной приложения, необходимо вызвать метод Unlock (). Следует иметь в виду, что время между блокированием и снятием блокировки ограничено очень коротким промежутком — вам не следует производить чтение файлов или данных из базы данных в течение этого времени. В противном случае другие клиенты должны будут ожидать, пока не будет завершен доступ к данным. void Session_Start(Object sender, EventArgs e) { // Код, который выполняется во время начала нового сеанса. Application.Lock(); Application["userCount"] = (int)Application["userCount"] + 1; Application.UnLock(); Чтение данных из состояния приложения не представляет никакой сложности и производится точно так же, как и в случае с состоянием сеанса: Labell.Text = Application["userCount"].ToString ();
Глава 19. Основы программирования для Web 637 Не следует хранить слишком большой объем данных в состоянии приложения, поскольку для состояния приложения необходимы ресурсы сервера до тех пор, пока сервер не будет остановлен или перезапущен. Состояние кэша Кэш (cache) — это состояние на стороне сервера, которое похоже на состояние приложения в том смысле, что оно разделяется между всеми клиентами. Состояние кэша отличается от состояния приложения тем, что кэш является более гибким: для определения недействительности состояния можно воспользоваться несколькими способами. Вместо того чтобы производить чтение файла с каждым запросом или читать данные из базы, данные можно хранить в кэше. Для работы с кэшем необходимы пространства имен System.Web.Caching и класс Cache. Пример добавления объекта в кэш показан в следующем примере: Cache.Add("mycache", myobj, null, DateTime.MaxValue, TimeSpan. FromMinutes A0) , CacheltemPnority.Normal, null); Класс Page имеет свойство Cache, которое возвращает объект Cache. С помощью метода Add () класса Cache можно поместить любой объект в кэш. Первый параметр метода Add () определяет имя элемента кэша. Второй параметр определяет объект, который необходимо поместить в кэш. Если в файл вносятся какие-либо изменения, осуществляется проверка объекта кэша. В приведенном нами примере такой зависимости нет, поскольку этот параметр имеет значение null. С помощью четвертого и пятого параметров можно задать промежуток времени, в течение которого элемент кэша будет считаться действительным. Четвертый параметр определяет абсолютное время, при котором элемент кэша будет считаться недействительным, в то время как пятый параметр требует "скользящее" время: элемент кэша будет считаться недействительным, если доступ к нему не производился в течение указанного периода времени. В нашем примере используется скользящий временной промежуток: если доступ к элементу кэша не производился в течение 10 минут, он становится недействительным. Шестой параметр определяет приоритет кэша. CacheltemPriority — это перечисление для настройки приоритета кэша. Если рабочий процесс ASP.NET потребляет большой объем памяти, исполняющая среда ASP.NET будет удалять элементы кэша в соответствии с их приоритетом. Первыми будут удалены элементы, имеющие низкий приоритет. С помощью последнего параметра можно определить метод, который будет вызываться при удалении элемента кэша. Примером ситуации, в которой будет использоваться этот метод, является случай, когда кэш зависит от работы файла. Если в файле происходят изменения, удаляется элемент кэша и вызывается обработчик события. С помощью обработчика события можно перезагрузить кэш, прочитав файл еще раз. Чтение элементов кэша может осуществляться посредством индексатора, с которым вы уже встречались при рассмотрении состояния сеанса и приложения. Прежде чем использовать объект, возвращенный свойством Cache, всегда следует проверять, является ли результат равным null, что встречается в случае, если кэш является недействительным. Если значение, возвращенное индексатором Cache, не равно null, возвращенный объект можно привести к типу, который использовался для хранения элемента кэша: object о = Cache["mycache"]; if (о == null) { // Перегрузка кэша. }
638 Часть III. Программирование для Web else { // Использование кэша. MyClass myObj = (MyClass)o; //... } Аутентификация и авторизация Чтобы защитить Web-сайт, механизм аутентификации используется с той целью, чтобы проверить достоверность регистрационных данных пользователя. Ну а механизм авторизации позволяет проверить, разрешено ли пользователю, прошедшему аутентификацию, использовать данный ресурс. В ASP.NET поддерживается аутентификация Windows и аутентификация с помощью форм. Применительно к Web-приложениям наиболее часто используемой является аутентификация с помощью форм — именно ее мы и рассмотрим здесь. Следует отметить, что для данной формы аутентификации в арсенале ASP.NET имеется несколько новых мощных особенностей. В форме аутентификации Windows используются учетные записи Windows и IIS. Для аутентификации пользователей ASP.NET предлагает множество классов. На рис. 19.12 показана структура новой архитектуры. Благодаря ASP.NET стало возможным использование множества новых элементов управления безопасностью, таких как Login и PasswordRecovery. Эти элементы управления используют интерфейс Membership API, благодаря которому можно создавать и удалять пользователей, проверять регистрационную информацию или получать информацию о пользователях, зарегистрированных в данный момент времени. Этот интерфейс для своей работы использует т.н. поставщик членства (membership provider). В версии ASP.NET 2.0 для доступа пользователей к базе данных Access, SQL Server или Active Directory имеются разные поставщики. Можно также создать специальный поставщик, который будет осуществлять доступ к XML-файлу или любому другому специальному месту хранения. Элементы управления безопасностью Ц Интерфейс Membership API Поставщик членства Рис. 19.12. Новая архитектура Конфигурация аутентификации В этой главе будет показан механизм аутентификации с помощью форм с поставщиком Membership. В следующем практическом занятии мы постараемся настроить защиту для Web-приложения и назначить разные списки доступа к разным папкам.
Глава 19. Основы программирования для Web 639 Практическое за Настройка защиты 1. Откройте в Visual Studio ранее созданное Web-приложение EventRegistrationWeb. 2. Создайте новую папку Intro, выбрав каталог Web в окне Solution Explorer, а затем выбрав пункт меню Website^Add Folder^Regular Folder (Web-сайт^Добавить папку1^ Обычная папка). Мы настроим эту папку так, чтобы к ней имели доступ все пользователи, а основная папка будет доступна только для тех пользователей, кто прошел аутентификацию. 3. Запустите средство ASP.NET Web Application Administration (Администрирование Web-приложения ASP.NET), выбрав пункт меню Website^ASP.NET Configuration (Web-сайт^Конфигурация ASP.NET) в окне Visual Studio 2008. 4. Откройте вкладку Security (Безопасность), показанную на рис. 19.13. "G О * М*//1ааам»ЬИМУмрл \it 4Й 0 КЗР N«t weo NK*c*ion Adrrwurta&ofl Web Site Administration Tool »|*tjx!!ii«sieirt " P ГГ You can ut« the Web Sit* Administration Toot to manage all the secunty settings for your appecabon. You can set up users and passwords (authentication), create roles (groups of users), and create permissions (rules for contro*ng access to parts of your appecabon). By default, user «formation is stored m a Mrcrosoft SQL Server Express database m the Data folder of your Web site. If you want to store user information n a different database, use the Provider tab to select a different provider. Use tte весили Sttup WbceTd to configure security step by step. Ckck the bnks m the table to manage the settings for your application The current authentication type is Windows user management from withn this tool is therefore Roles are not enabled &nabj£.iojfiS. ueAtt.aj.crj»s._'.vtSS MjiOdflS. ICC ASS JVrC5 BssssCl ■tfbm&GiLiDO ЕШ «iLOUlBl MOW Рис. 19.13. Вкладка Security окна ASP. NET Web Application Administration 5. Щелкните на ссылке Security Setup Wizard (Мастер настройки защиты). В окне Welcome Screen (Добро пожаловать!) щелкните на кнопке Next (Далее). В появившемся окне выберите метод доступа From the Internet (Из Internet), как показано на рис. 19.14. 6. Щелкните на кнопке Next. В появившемся окне вы сможете увидеть выбранного вами поставщика (рис. 19.15). По умолчанию используется поставщик ASP.NET Access, что подразумевает хранение учетных записей пользователей в базе данных Access. Эту настройку невозможно изменить в режиме Wizard, однако чуть позже это можно будет сделать. 7. Щелкните на кнопке Next два раза, чтобы перейти в окно, где можно добавить новых пользователей. Создайте новую учетную запись, как показано на рис. 19.16.
640 Часть III. Программирование для Web 8. После того как вы создадите новую учетную запись, щелкните на кнопке Next, чтобы перейти в окно настройки параметров, определяющих права доступа пользователей к Web-сайту или определенным каталогам (рис. 19.17). Добавьте правило, запрещающее доступ для анонимных пользователей. Затем выберите каталог Intro (Введение) и добавьте правило, разрешающее доступ для анонимных пользователей. После этого щелкните на кнопке Next, а затем на кнопке Finish (Готово). Рис. 19.14. Выбор метода доступа -.»..»:' 6 asp ми ть sm Advanced provider settings To change the data store for your application, exit the Security Wizard, and cbck on the Provider Configuration tab. You can use the Provider Configuration tab to configure how web site management data is stored к N.*> <4loc«m ♦.UK Рис. 19.15. Информация о поставщике
Глава 19. Основы программирования для Web 641 6 ASP Ntl Web Site ^l ♦♦> j x <!b»«s«j/rh Рмс. 19.16. Создание новой учетной записи Л ASP.Net Web Siu Administration Tool Windows Internet Explorer Q<^y * !(t М1рг//11П*1*111Ш/1Ц,11>1111>|1и1|1»^^а—»Аш1и1<Л^ш<1 : W -|*r|xiG^I 401 *SP Ntt Лео Site Administration Tool ft "^ &"i£*»* tng users or rwes me rue apples to ana saga aibw or uenv. If you define multiple rules, they are applied m the order shown m the table. The first rule that matches appbes. You wil be abte to add, remove, modify, and reorder your rules later. Select a directory for this rule: я _j EventRegistrabonWeb _Ll App_Code £] App.Data ^j Intro Rule applies to: Permission: Role • Alow | User Deny Al Users • Anonymous Users Search for users Rules that appear dimmed are inherited from the parent and cannot be changed at this level. I * B*ck, J [We*» *k Local mtranet | Protected Mode: Off р"»«" ' Puc. 19.17. Применение правил
642 Часть III. Программирование для Web На рис. 19.18 показана вкладка Security после завершения работы мастера Security Setup Wizard. 4 ASP Net Web A, QUO • JU M»i/*M*>ii**Miy*pj« w «r £ASf»N«WtOA<>p«a§onAdm a "^ о »#t • EHUD Г9ЩШ • loo* - Web Site Administration Tool You can им the Web Sit* Admnistrabon Toot to manage afl the secunty setting» for your appecaoon. You can set up users and passwords (authentication), create rotes (groups of users), and create permissions (rules for controfcng access to parts of your application). By default, user information is stored wi a MKrosoft SQL Server Express database tn the Data folder of your Wet site. If you want to store user «formation m a different database, use the Provider tab to select a different provider. Ш Им security Setup VYuard tg confrgufe 5«urity step by step. Cbck the hnks *\ the table to manage the settings for your appbeabon Extsbng users: 1 Roles are not enabled Enable rotes CKAtft .*c^ess.iute Manege геем» mhs Setect authentrsitwn type Рис. 19.18. Результаты настройки Описание полученных результатов После того как вы завершите процесс настройки защиты, будет создана новая база данных Access. После обновления файлов в окне Solution Explorer вы увидите в каталоге AppData новую базу данных SQL Server Express ASPNETDB.mdf. Эта база данных содержит таблицы, которые используются поставщиком SQL Membership. Теперь, вместе с Web-приложением, вы увидите также конфигурационный файл web.config. Этот файл содержит конфигурационные данные для аутентификации с помощью форм, поскольку мы выбрали аутентификацию, выполняемую в Internet, a раздел <authorization> запрещает доступ анонимным пользователям. Если бы был изменен поставщик Membership, то в конфигурационном файле был бы отображен новый поставщик. Поскольку поставщик SQL используется по умолчанию, и он уже определен в конфигурационном файле компьютера, показывать его здесь нет необходимости. <?xml version=.0" encoding="utf-8"?> <configuration> <system.web> <authorization> <deny users="?" /> </authorization> Authentication mode="Forms" /> </system.web> </configuration> Внутри подпапки Intro можно увидеть еще один файл web.config. Раздел аутентификации в этом файле отсутствует, поскольку ее конфигурация взята из родительского каталога. Тем не менее, раздел аутентификации отличается.
Глава 19. Основы программирования для Web 643 Ниже показано, что анонимным пользователям доступ разрешается посредством строки <allow users="?" />: <?xml version=.0" encoding="utf-8"?> <configuration> <system.web> <authorization> <allow users="?" /> </authorization> </system.web> </configuration Использование элементов управления безопасностью В ASP.NET включено множество элементов управления безопасностью. Вместо того чтобы писать специальную форму, в которой пользователь мог бы ввести свое имя и пароль, можно воспользоваться готовым элементом управления Login. Элементы управления безопасностью и их функции описаны в табл. 19.6. Таблица 19.6. Элементы управления безопасностью Элемент управления Описание безопасностью иписание Login Составной элемент управления, включающий элементы управления для ввода имени пользователя и пароля Loginstatus Включает гиперссылки для входа и выхода, в зависимости от того, зарегистрировался пользователь или нет LoginName Отображает имя пользователя Loginview Может отображать разное содержимое, в зависимости от того, зарегистрировался пользователь или нет PasswordRecovery Составной элемент управления для восстановления забытых паролей. В зависимости от настроек защиты, пользователю будет предложено ответить на ранее установленный секретный вопрос, или же пароль будет отправлен по электронной почте ChangePassword Составной элемент управления, позволяющий зарегистрированным пользователям менять свои пароли CreateUserWizard Мастер, позволяющий создать нового пользователя и записать пользовательскую информацию в поставщик Membership В следующем упражнении мы добавим страницу регистрации в Web-приложение. Создание страницы регистрации Если вы пытались запустить Web-сайт после того, как для него были выбраны настройки, запрещающие доступ для анонимных пользователей, вы должны были получить сообщение об ошибке по причине отсутствия страницы login.aspx. Если для аутентификации с помощью форм не будет сконфигурирована специальная страница регистрации, то по умолчанию будет применяться страница login.aspx. Теперь создадим страницу login. aspx.
644 Часть III. Программирование для Web 1. Добавьте новую форму Web Forms и присвойте ей имя login. aspx. 2. Создайте на форме элемент управления Login. В окне Design View вы увидите элемент управления, показанный на рис. 19.19. 3. Вот и все, что нужно сделать для создания страницы регистрации. Теперь, когда вы запустите сайт default. aspx, вы будете перенаправлены на страницу login.aspx, на которой сможете ввести имя и пароль для созданного ранее пользователя. Login User Name I Password I Г" Remember me next tine Puc. 19.19. Элемент управления Login на форме Описание полученных результатов После того как вы добавите элемент управления Login, в окне Source View вы увидите следующий код: <asp:Login ID="LoginlM runat="server"> </asp:Login> Свойства этого элемента управления позволяют сконфигурировать текст для заголовка, меток имени пользователя и пароля, а также для кнопки регистрации. Вы можете сделать флажок Remember Me Next Time (Запомнить меня) видимым, установив свойство DisplayRememberMe. Если вы хотите еще более изменить внешний вид и поведение элемента управления Login, вы можете преобразовать элемент управления в шаблон. Это можно сделать в окне Design View, щелкнув на смарт-теге и выбрав пункт Convert to Template (Преобразовать в шаблон). Затем, когда вы щелкнете на кнопке Edit Templates (Редактировать шаблоны), вы получите представление, пример которого показан на рис. 19.20, в котором сможете добавить и изменить любой шаблон. Login User Name.l Password 1 Г Remember me next time | Literal "FaiueTcxt" • • 1 Login J Puc. 19.20. Представление для добавления и изменения шаблонов Чтобы проверить имя пользователя и пароль после щелчка на кнопке Login In (Вход), элемент управления вызывает метод Membership.ValidateUser (), поэтому делать это самостоятельно не придется. Когда у пользователей нет учетной записи для входа на Web-сайт EventRegistration, им необходимо создать свои собственные регистрационные данные. Это можно сделать очень просто с помощью элемента управления CreateUserWizard, как продемонстрировано в следующем практическом занятии.
Глава 19. Основы программирования для Web 645 Практическое занял .. Использование мастера CreateUser 1. Добавьте новую Web-страницу RegisterUser .aspx в папку Intro, которую вы создали ранее. Эта папка сконфигурирована таким образом, что доступ к ней могут иметь анонимные пользователи. 2. Добавьте на эту страницу элемент управления CreateUserWizard. 3. Присвойте свойству ContinueDestinationPageUrl значение -/Default .aspx. 4. Добавьте элемент управления LinkButton на страницу Login.aspx. Присвойте этому элементу управления содержимое Register User, а свойству PostBackUrl этого элемента управления — Web-страницу Intro/RegisterUser. aspx. 5. Теперь можете запустить приложение. Щелкните на ссылке Register User (Зарегистрировать пользователя) на странице Login. aspx, и вы перейдете на страницу RegisterUser. aspx, где будет создана новая учетная запись на основе введенных данных. Описание полученных результатов CreateUserWizard— это элемент управления, подобный мастеру, состоящий из множества шагов мастера, которые определяются в элементе <WizardSteps>: <asp:CreateUserWizard ID="CreateUserWizardl" runat="server" ContinueDestinationPageUrl="~/Default.aspx"> <WizardSteps> <asp:CreateUserWizardStep runat="server" /> <asp:CompleteWizardStep runat="server" /> </WizardSteps> </asp:CreateUserWizard> Эти шаги мастера могут быть сконфигурированы в визуальном конструкторе. Смарт-тег элемента управления позволяет сконфигурировать каждый из этих шагов по отдельности. На рис. 19.21 показано конфигурирование на шаге Sign Up for Your New Account (Создание вашей новой учетной записи), а на рис. 19.22— завершение этого шага. Вы можете также добавить специальные шаги со специальными элементами управления, чтобы добавить специальные требования, такие как принятие пользователем условий соглашения перед созданием учетной записи. [asp:createuservrtzard*CreateUsenML-] Sign Up for Your New Account User Name | • Password 1 • Confirm Password I • E-mail Security Question I • Security Answer | • i d and Confirmation Password must match Create User J 1 Common CreateOserWuard Tasks i Auto Format... Step: ^9" Up tor four New» Ц 1 Add/Remove WizardSteps Convert to SUrtNavioationTemplate Convert to StepNaviaattonTemptate 1 Convert to FinishNavtoationTemplate 1 Convert to CustomNavigationTemplate 1 Customize Create User Step Customize Complete Step Administer Website I art Templates Puc. 19.21. Конфигурирование на шаге Sign Up for Your New Account
646 Часть III. Программирование для Web J a»p:aeateuierwizacd*Cfe«t«Ut«VVfe-| Complete Your account has been successful)' created. Continue Convert to StartNavioationTemplate Convert to StepNavtgationTemplate Convert to f mishNavigationTemplate Customize Create User Step Customize Complete Step Administer Website f tlrt Templates Puc. 19.22. Завершение шага Sign Up for Your New Account Чтение и запись в базу данных SQL Server Большинству Web-приложений необходим доступ к базе данных для чтения и записи информации. В этом разделе мы создадим новую базу данных для хранения информации о событиях и узнаем о том, как эта база данных используется из среды ASP.NET. Сначала мы создадим новую базу данных SQL — этому будет посвящено следующее практическое занятие. Это можно сделать непосредственно из Visual Studio 2008. 1. Откройте ранее созданное Web-приложение EventRegistrationWeb. 2. Откройте окно Server Explorer (Проводник сервера). Если вы еще не видите его в Visual Studio, можете открыть его окно, выбрав пункт меню View^Other Windows1^ Server Explorer (Вид^Другие окна"=>Браузер сервера). 3. В окне Server Explorer выберите пункт Data Connections (Соединения с данными), щелкните правой кнопкой мыши, чтобы открыть контекстное меню и выберите пункт Create New SQL Server Database (Создать новую базу данных SQL Server). На экране появится диалоговое окно Create New SQL Server Database, показано на рис. 19.23. J С reate New SQL Server Database til Ш .!] j Enter information to connect to a SQL Server, then j specify the name of a database to create. i Server name; ', (local) *• [ Log on to the server • Use Windows Authentication Use SQL Server Authentication 1 • > password 1 New database namr Beg VCSharpE vents 1 OK II *** 1 Cancel ] Puc. 19.23. Диалоговое окно Create New SQL Server Database
Глава 19. Основы программирования для Web 647 4. Для имени сервера выберите вариант (local), а для имени базы данных — BegVCSharpEvents. 5. После того как база данных будет создана, выберите новую базу данных в окне Server Explorer. 6. Выберите запись Tables под базой данных, и в окне Visual Studio выберите пункт меню Data^Add New^Table (Данные^Добавить новые"=>Таблица). 7. Введите следующие имена столбцов и типы данных (рис. 19.24). dbo.T*btel: T*_VCSh*rpEvents)* Column Name 9 Id Title Date Location В Data Type mt n\farcharE0) datetlme nvarchar(SO) Allow Nulls В и в с Рис. 19.24. Создание столбцов таблицы Events 8. Настройте столбец ID как столбец первичного ключа с приращением 1 и начальным значением 1. Настройте все столбцы таким образом, чтобы они не принимали значения null. 9. Сохраните таблицу под именем Events (События). 10. Добавьте несколько событий в таблицу с разными заголовками, датами и местонахождениями. Чтобы отобразить и отредактировать данные, в панели Toolbox имеется отдельный раздел Data (Данные), представляющий элементы управления данными. Эти элементы можно распределить по двум группам: представление данных и источник данных. Элемент управления источника данных связан с источником данных, таким как XML- файл, база данных SQL или класс .NET; представления данных подключаются к источнику данных для представления данных. В табл. 19.7 представлены все элементы управления данными: Таблица 19.7. Элементы управления данными Элемент управления 0писание данными описание GridView DataList DetailsView FormView Repeater ListView Отображает данные со строками и столбцами Отображает один столбец для отображения всех элементов Может использоваться вместе с элементом управления Gndview, если у вас имеется взаимоотношение типа "главная/детали" с вашими данными Отображает одну строку источника данных Основан на шаблоне. Вы можете определить, какие HTML-элементы должны быть сгенерированы на основе данных, взятых из источника данных Новый элемент управления в .NET 3.5. Этот элемент управления основан на шаблоне, наподобие элемента управления Repeater Элементы управления — источники данных и их функции перечислены в табл. 19.8.
648 Часть III. Программирование для Web Таблица 19.8. Элементы управления — источники данных Элемент управления данными Описание SqlDataSource AccessDataSource LinqDataSource Ob]ectDataSource XmlDataSource SiteMapDataSource Доступ к SQL Server или любому другому поставщику ADO.NET (например, Oracle, ODBC и 0LEDB). Внутренне, он использует класс DataSet или DataReader Позволяет использовать базу данных Access Новый в .NET 3.5. Позволяет использовать поставщиков LINQ в качестве источника данных Позволяет использовать классы .NET в качестве источника данных Позволяет получать доступ к XML-файлам. С помощью этого источника данных можно отображать иерархические структуры Использует XML-файлы, чтобы определить структуру сайта для создания ссылок и связей в Web-сайте. Это средство будет рассматриваться в главе 20 В следующем упражнении мы будем использовать элемент управления GridView для отображения и редактирования данных из ранее созданной базы данных. П, фактическое занятие ИсПОЛЬЗОВЭНИе Элемента управления GridView для отображения данных 1. В окне Solution Explorer создайте новую обычную папку Admin. 2. Создайте новую Web-страницу EventsManagement. aspx в папке Admin. 3. Добавьте элемент управления GridView на Web-страницу. 4. В комбинированном списке Choose Data Source (Выбрать источник данных) смарт-тега элемента управления выберите пункт <New data source...> (Новый источник данных). На экране появится диалоговое окно мастера Data Source Configuration Wizard (Мастер конфигурирования источника данных), показанное на рис. 19.25. СЬоом a D*t» Source Тур* fe J & * 4 4 Seted t cut» wwee typ« from the do. above. Oestr.ptfy* tert fof the ttttcttd data »ou«e wnN appea* Spetrry »n 10 foi the dau «ource Puc. 19.25. Мастер конфигурирования источника данных
Глава 19. Основы программирования для Web 649 5. Выберите вариант Database (База данных) и введите имя EventsDataSource для этого нового источника данных. 6. Щелкните на кнопке ОК, чтобы настроить источник данных. На экране появится диалоговое окно Configure Data Source (Настройка источника данных). Щелкните на кнопке New Connection (Новое соединение), чтобы создать новое соединение. 7. В диалоговом окне Add Connection (Добавить соединение), которое показано на рис. 19.26, введите (local) в качестве имени сервера и выберите ранее созданную базу данных BegVCSharpEvents. Щелкните на кнопке Test Connection (Проверить соединение), чтобы проверить, что соединение должным образом настроено. Если все в порядке, щелкните на кнопке ОК. В следующем диалоговом окне (рис. 19.27) можно будет сохранить строку соединения. [ Add Connection I* : nJ' i Enter information to connect to the selected data source or click 'Change' to choose a different data source and/or provider. J Data source: 1 Microsoft SQL Server (SqrClient) [~g—-Щи ] П Server name: U (local) - LUHfeJ Log on to the server •■ Use Windows Authentication *) Use SQt Server Authentication 'Save my password Connect to a database • Select or enter a database name: BegVCSharpEvents ^ О Attach a database file B.OWSf Г I A*mntM.~] Ttit ConnectK>n 1 O* 1 | CfKO Рис. 19.26. Диалоговое окно Add Connection 8. Отметьте флажок, чтобы сохранить связь и введите имя строки связи EventsConnectionString. Щелкните на кнопке Next. 9. В следующем диалоговом окне выберите таблицу Events, чтобы прочитать из нее данные, как показано на рис. 19.28. Выберите столбцы ID, Title, Date и Location, чтобы определить команду SQL, показанную на рисунке. Щелкните на кнопке Next. 10. На последнем экране диалогового окна Configure Data Source можно проверить запрос. После этого нужно щелкнуть на кнопке Finish (Готово). 11. Теперь в окне визуального конструктора можно увидеть элемент управления GridView с вымышленными данными, и элемент управления SqlDataSource с именем EventsDataSource, как показано на рис. 19.29.
650 Часть III. Программирование для Web It-SJl l'fc Save the Connection String to the Application Configuration File Storing connection strings m the Application configuration tile simplifies maintenance and deployment To save the connection string in application configuration file, enter a name in the text boi and then click Next. If you choose not to do this, the connection string is saved in the page as a property of the data source control. Do you want to save the connection in the |V) Yes, save this connection as: EventsConnectionStnng appflcabon configuration Ше? 11 »*> 1 Рис. 19.27. Сохранение строки соединения г ______ _ 1 Configure the Select Statement How would you flax to retrieve data from your database? ~N Specify a custom _Ql statement or stored procedure • Specify columns from a table or view Name: Events Cglumns- B' у Id 001 Title jDttt SE.ECT statement SELECT (IdL [TrtleL [DateL (location) FROM [Events] | «freawu. [1 Це-> J 11| а П E Return only unique rows i___j?^Zi [ одре***-- i Ad_ance4- | • finish Cancel Puc. 19.28. Построение команды SQL для чтения данных из таблицы Events
Глава 19. Основы программирования для Web 651 Id 0 1 2 3 4 Title abc abc abc abc abc Date Location 02.10.2007 (ХИХИХ) abc 02.10.2007 00Ю0Ю0 abc 02.10.2007 00Ю0Ю0 abc 02.10.2007 (ХИХИХ) abc 02.10 2007 (ХИХИХ) abc SqlOataSource - EventsDataSource Puc. 19.29. Элемент управления Gri dVi e w 12. Чтобы придать более привлекательный внешний вид элементу управления GridView, выберите пункт AutoFormat в смарт-теге и выберите схему Lilacs in Mist (Сирень в тумане), показанную на рис. 19.30. [«' Д I Select a scheme: Remove Formatting 0 abc 1 abc 2 abc 3 abc 02.10 2007 00Ю0Ю0 02.10.2007 0ОО0Ю0 02 10.2007 00Ю0Ю0 02 10 2007 00ЮОЮ0 abc abc ;_< Puc. 19.30. Выбор схемы Lilacs in Mist 13. Запустите страницу с помощью Visual Studio, и вы увидите события в симпатичной таблице, как показано на рис. 19.31. — С Untitled Page Windows internet Explorer Q\^l ' 't- М^ЛосаЛоНЯа* "|*е| x|!liveSeo»tn P - w Ф jgfUnattdPaoe a -^ a * # *&*** 1 2 3 4 5 6 7 8 Gwen Stefan Chris de Burgb Gene» Brace Spnngsteei Van Halm Pad McCartney Spice Gab Rod Stewart 17.10 2007 0ОЮОЮ0 Vienna 13.11.2007 00Ю0Ю0 Glasgow 12.10 2007 00Ю0Ю0 Los Angeles i 19 112007 00:00:00 Boston 22 12.2007 0ОЮОЮ0 Oakland 25 10 2007 0ОЮОЮ0 London 04 01 200' 000000 London П0 200- 00.00-00 Berfa 4. Local totraoet | Protected Мое* Off Moo% Puc. 19.31. Содержимое таблицы событий
652 Часть III. Программирование для Web Описание полученных результатов После того как вы добавите элемент управления GridView, вы сможете увидеть его конфигурацию в исходном коде. Атрибут DataSourcelD определяет связь с элементом управления данными, который расположен ниже элемента управления сетки. В элементе <Columns> показаны все столбцы для отображения данных. HeaderText определяет текст заголовка, a DataField — имя поля в источнике данных. Источник данных определен в элементе <asp : SqlDataSource>, в котором SelectCommand определяет, как производится чтение данных из базы, a ConnectionString определяет, как осуществляется подключение к базе данных. Так как вы выбрали вариант сохранения строки соединения в конфигурационном файле, для создания связи с динамически сгенерированным классом из конфигурационного файла используется <%$: <asp:GridView AutoGenerateColumns="False" BackColor="White" BorderColor="White" BorderStyle="Ridge" BorderWidth=px" CellPadding=" CellSpacing="l" DataKeyNames="Id" DataSourceID="EventsDataSource" GridLines="None" ID="GridViewl" runat="server"> <footerstyle BackColor="#C6C3C6" forecolor="Black" /> <rowStyle BackColor="#DEDFDE" forecolor="Black" /> <Columns> <asp:boundfield DataField="Id" HeaderText="Id" InsertVisible="False" ReadOnly="True" SortExpression="Id"x/asp:boundfield> <asp:boundfield DataField="Title" HeaderText="Title" SortExpression="Title"x/asp:boundfield> <asp:boundfield DataField="Date" HeaderText="Date" SortExpression="Date"x/asp:boundfield> <asp:boundfield DataField="Location" HeaderText="Location" SortExpression="Location"></asp:boundfield> </Columns> <pagerstyle backcolor="#C6C3C6" forecolor="Black" horizontalalign="Right" /> <selectedrowstyle backcolor="#9471DE" font-bold="True" forecolor="White" /> <headerstyle backcolor="#4A3C8C" font-bold="True" forecolor="#E7E7FF" /> <editrowstyle font-bold="False" font-italic="False" /> </asp:GridView> <asp:SqlDataSource ConnectionString="<%$ ConnectionStrings:EventsConnectionString %>" ID="EventsDataSource" runat="server" SelectCommand= "SELECT [Id], [Title], [Date], [Location] FROM [Events]"> </asp:SqlDataSource> В конфигурационном файле web. conf ig вы можете найти строку соединения с базой данных: <connectionStrings> <add name="EventsConnectionString" connectionString="Data Source=(local); Integrated Security=True;Initial Catalog=BegVCSharpEvents; providerName="System.Data.SqlClient" /> </connectionStrings> Теперь элемент GridView должен быть настроен по-другому. В следующем упражнении столбец с обозначением идентификатора больше не отображается для пользователя, а в столбце даты и времени отображается только дата.
Глава 19. Основы программирования для Web 653 Практическое зам Настройка элемента управления GridView 1. Выделите смарт-тег элемента управления GridView и выберите меню Edit Columns (Редактировать столбцы). На экране появится диалоговое окно Fields (Поля), показанное на рис. 19.32. Выберите поле ID и присвойте свойству Visible значение False. Вы можете расставить столбцы в этом диалоговом окне по своему усмотрению, а также изменить цвета и задать текст заголовка. г? /3 Boundf teld 3w 3 тик 3 Drtc 3 Location Я Ch*tkBo*fteld Щ Hyp*rlmkF,fId 1 7.1 Bounofi*id properties. ™ Зтик 30«te 3 location "~: Auto-generate tieMt AppryfotmatlnJdit ConvertfatptyStrm Htwttncodf H1mifncodefofm*i IntertVisrbte NuUOitplayTevt ReatlOnry ShowHeader SortExpreinon Trwe True ■: CataF.eid Id DataFomat«nnfl Whether the field is mibie or not : CwrtrtttniftemiTrtaiTi X Puc. 19.32. Диалоговое окно Fields 2. Для редактирования GridView команду обновления необходимо определить посредством источника данных. Выберите элемент управления SqlDataSource с именем EventsDataSource, и в смарт-теге выберите пункт Configure Data Source (Настроить источник данных). В диалоговом окне Configure Data Source щелкните несколько раз на кнопке Next, пока не увидите ранее созданную команду SELECT. Щелкните на кнопке Advanced (Дополнительно), а затем отметьте флажок Generate INSERT, UPDATE and DELETE statements (Сгенерировать операторы INSERT, UPDATE и DELETE), как показано на рис. 19.33. Щелкните на кнопке Next и Finish. Advanced SQL Generation Options Additional INSERT. UPDATE and DELETE statements can be generated to update the data source. r/ Generate INSERT. UPDATE, and DELETE Generates INSERT. UPDATE, and DELETE statements based on your SELECT statement You must have all primary key fields selected for this option to be enabled. U« Modifies UPDATE and DELETE statements to detect whether the database has changed since the record was loaded into the DataSet. This helps prevent concurrency conflicts. Puc. 19.33. Отметка флажка Generate INSERT, UPDATE and DELETE statements
654 Часть III. Программирование для Web 3. Выберите снова смарт-тег GridView. Теперь нас интересует элемент Enable Editing (Разрешить редактирование) в меню смарт-тега (рис. 19.34). После того как вы отметите флажок, разрешающий редактирование, в элемент управления GridView будет добавлен новый столбец. Вы можете также изменить столбцы посредством меню смарт-тега, чтобы поместить новую кнопку Edit (Редактирование). 4. Запустите приложение и измените существующие записи событий. Remove Column f?j Enable Paging H Enable Sorting Ш Enable Editing И Enable Deleting И Enable Selection Edit Templates Рис. 1934. Элемент Enable Editing в меню смарт-тега Описание полученных результатов В этом примере вручную не было записано ни одной строки кода; вся обработка производилась посредством элементов управления Web из ASP.NET. Незаметно для нас эти элементы используют множество возможностей. Например, элемент управления SqlDataSource заполняет DataSet подсказкой SqlDataAdapter, используя данные из базы. Данные, используемые для заполнения DataSet, определены в строке соединения и команде SELECT. Простым изменением свойства SqlDataSource можно использовать SqlDataReader вместо DataSet. Также после присваивания значения true свойству EnableCaching объект Cache (рассмотренный ранее в этой главе) используется автоматически. Резюме В этой главе была рассмотрена архитектура ASP.NET, были показаны приемы работы с элементами управления на стороне сервера, и описаны некоторые базовые средства ASP.NET. Среда ASP.NET предлагает несколько элементов управления, не требующих объемного кода, как это было показано на примере элементов управления регистрации и данных. После изучения базовых функций ASP.NET в отношении серверных элементов управления и механизма обработки событий мы рассмотрели вопрос проверки вводимых данных, методы управления состоянием, аутентификацию и авторизацию, а также отображение данных из базы данных.
Глава 19. Основы программирования для Web 655 В этой главе рассмотрены следующие вопросы. □ Создание Web-страницы с использованием ASP.NET. □ Использование обработчиков событий для серверных элементов управления Web. □ Проверка вводимых пользователем данных с помощью элементов управления проверкой. □ Управление состоянием различными способами. □ Конфигурирование аутентификации и авторизации для Web-приложений. □ Использование элементов управления регистрации в ASP.NET. □ Доступ и отображение значений из базы данных с помощью элементов управления SqlDataSource и GridView. Предлагаемые далее упражнения помогут набраться опыта в разработке Web-приложений на основе созданного нами в этой главе приложения. Упражнения 1. Добавьте имя пользователя вверху страниц ASP.NET, которые были созданы в этой главе. Для выполнения этой задачи можно воспользоваться элементом управления LoginName. 2. Добавьте страницу для процедуры восстановления пароля. Вы можете добавить ссылку на Web-страницу login.aspx, чтобы пользователи могли восстанавливать пароль, если регистрация не будет произведена. 3. Измените источник данных страницы Default .aspx, чтобы для отображения событий она использовала базу данных Events.
Расширенное программирование для Web В главе 19 мы рассматривали основные средства ASP.NET, говорили о серверных элементах управления, используемых для визуализации HTML-кода на стороне клиента, рассматривали способы проверки данных, вводимых пользователями, и вопросы, связанные с управлением состоянием. Мы говорили также о функциях защиты, а также чтения и записи информации в базу данных. В настоящей главе мы займемся рассмотрением элементов пользовательского интерфейса и вопросов настройки Web- страниц посредством профилей и Web-частей. Мастер-страницы служат для определения каркаса для множества страниц. Вы можете использовать систему навигации сайта, чтобы определить структуру Web-сайта, создавая меню для доступа ко всем страницам. Чтобы повторно использовать элементы управления на множестве страниц одного и того же Web-сайта, можно создать пользовательские элементы управления. В последнем разделе этой главы будут рассмотрены Web-части, которые можно использовать для создания Web-портала. В частности, в этой главе будут рассматриваться следующие темы. □ Как создаются и используются мастер-страницы. □ Как создаются и применяются пользовательские элементы управления. □ Как производится настройка системы навигации в Web-сайтах. □ Как используются профили. □ Как используются Web-части. □ Как пишется код на языке JavaScript.
Глава 20. Расширенное программирование для Web 657 Мастер-страницы Как правило, во многих Web-сайтах какая-нибудь часть их содержимого повторно используется на каждой странице — это могут быть элементы вроде логотипа компании и меню, доступные на всех страницах. Повторять обычные элементы пользовательского интерфейса на каждой странице не нужно; вместо этого можно создать мастер-страницу (master page), добавив на нее обычные элементы. Мастер-страницы выглядят как обычные страницы ASP.NET, но содержат заполнители, которые заменяются страницами содержимого. Мастер-страницы имеют расширение файла .master, и в первой строке файла присутствует директива Master, как показано ниже: <%@ Master Language="C#" AutoEventWireup="true" CodeFile="MasterPage.master.cs" Inherits="MasterPage" %> HTML-элементы <html>, <head>, <body> и <f orm> в рамках Web-сайта используются только мастер-страницами. Сами Web-страницы содержат только содержимое элемента <f orm>. Web-страницы могут внедрять свое собственное содержимое в элемент управления ContentPlaceHolder. Если Web-страница не определяет содержимое по умолчанию для элемента управления ContentPlaceHolder, это может быть сделано на Web-странице: <html xmlns="http://www.w3.org/1999/xhtml> <head runat="server"> <title>Untitled Page</title> <asp:contentplaceholder id="head" runat="server"> </asp:contentplaceholder> </head> <body> <form id="forml" runat="server"> <div> <asp:contentplaceholder id="ContentPlaceHolderIм runat="server"> </asp:contentplaceholder> </div> </form> </body> </html> Чтобы использовать мастер-страницу, необходимо указать атрибут MasterPageFile в директиве Page. Чтобы заменить содержимое мастер-страницы, применяйте элемент управления Content. Элемент управления Content связывает ContentPlaceHolder и ContentPlaceHolderlD: <%Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireUp="true" CodeFile="Default.aspx.cs" Inherits="default" Title="Untitled Page" %> <asp:Content ID="Contentl" ContentPlaceHolderID="head" Runat="Server"x/asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"x/asp: Content> Вместо того чтобы определять мастер-страницу в директиве Page, вы можете назначить мастер-страницу по умолчанию для всех Web-страниц в элементе <pages> конфигурационного файла web. conf ig:
658 Часть III. Программирование для Web <configuration> <system.web> <pages masterPageFile="~/MasterPage.master"> <!— ... —> </pages> </system.web> </configuration> Если файл мастер-страницы будет сконфигурирован внутри web.config, то в этом файле должна присутствовать конфигурация элемента Content для страниц ASP.NET, как было показано выше; в противном случае атрибут masterPageFile применяться не будет. Если вы используете атрибут MasterPageFile директивы Page и запись в файле web. conf ig, то при настройке директивы Page будет заменена настройка из файла web.condfig. Таким образом, вы можете определить файл мастер-страницы, используемый по умолчанию (в файле web. conf ig), перезаписывая имеющуюся настройку по умолчанию для определенных Web-страниц. Изменить мастер-страницу можно и программным способом. Это позволит применять разные мастер-страницы для разных устройств или разных типов Web-браузеров. Последним местом, в котором может быть изменена мастер-страница, является метод обработчика Page_PreInit. В следующем примере кода свойство MasterPageFile класса Page получает значение IE.master, если Web-браузер посылает строку MSIE с названием Web-браузера (это выполняет Web-браузер Microsoft Internet Explorer), или Default .master для всех других Web-браузеров: public partial class ChangeMaster : System.Web.UI.Page { void Page_Load(object sender, EventArgs e) { } void Page_PreInit(object sender, EventArgs e) { if (Reguest.UserAgent.Contains ("MSIE")) { this.MasterPageFile = "-/IE.master"; } else { this.MasterPageFile = "-/Default.master"; } } } Теперь давайте попробуем создать собственную мастер-страницу. Ее шаблон будет иметь заголовок и тело, а ее основная часть будет заменяться отдельными страницами. Пр?КП1Ч*с>юе."!?!"? Создание мастер-страницы 1. Откройте Web-сайт EventRegistrationWeb, который был создан в предыдущей главе. 2. Добавьте элемент Master Page (Мастер-страница), как показано на рис. 20.1, и присвойте ему имя EventRegistration.master. 3. Удалите из мастер-страницы заполнитель ContentPlaceHolder, который был создан автоматически.
Глава 20. Расширенное программирование для Web 659 1 A^NllllHl.ClllliLUlliCHHWuliUUliiWNW I | Templates : Vit«i*«Studrain<UM*dtrmpMn MwatM 3mmi«m#» II: £ aoonh шщ о*» моек ALumi am* мним 1 «JAMX Chtnl ItKVT Я AM* Mostei Mp 1 j A Clan D»o»»« JJCryst* Report |j fj biwk Manditt J HTMl P»o* ■ : j|UNQto&QlClMl*t Д Report | ; £ Resource Hie J^tteMop : (| SQl Str»»r Ommii 4| Style Sheet |Nm№ ^»MlS<htfn« 1 j Seerch Onhne Templates 1 A Matter Paoe toi Wet АрргкаЬОГИ 1 *«»» *(MntRtoKtrat>onWtbl<Mit«f 1 iMtoueee £•*<» , . ...») * "•«« toe« * *j Setart «otic ^ ' UtMtfl ^Sil si i web uie< Control [ 4) ajai owm сотго> 1 ■ AJAX Wtb 'one. J 1] 14 I ;£0«»5«t I OJJScnptM* 1 «jRepon wua/o 1 ^SimPN* 1 •j Tftl fat 1 |JWeBServ.<f I >«1ТМе 1 ! 1 separate Ы* 1 0«9* [ Pwc. 20.7. Добавление элемента Master Page Вставьте на страницу таблицу, выбрав пункт меню Tabled Insert Table (Таблица1^ Вставить таблицу). В появившемся диалоговом окне Insert Table (Вставить таблицу) выберите три строки и два столбца, как показано на рис. 20.2. Элемент меню Table (Таблица) будет доступен только в том случае, если в окне Design View (Представления дизайна) будет открыт редактор. ( 1 V zzzz * б» Sue II Row,. |3 -5- Сскяп: \i gj Layout I Aftgrmnt: Detail £J №*ж*у«*Ми [ ..*ч .. .i '* In Mxeh I CeJpaditog: |l $ ?; Spedfy hettf* Snwfi.eM | SUr |0 1 C*r: i^OJapae tat* bard* [ Cobr: • 1 \* me tMdvound с**#е 1 i Ikdmbrrl рг°м'м* II !?S««<kfeAforntwUblH 1 ".:::;;;:;.;. ■ I OK 1 .. <*»»... 1 Рис. 20.2. Диалоговое окно Insert Table 5. Выделите два столбца в первой строке и объедините их, выбрав пункт меню Table^Modify^Merge Cells (Таблица1^ Изменить^Объединить ячейки). Сделайте то же самое для столбцов в последней строке. 6. Добавьте текст Registration Demo Web в верхней части мастер-страницы и знак авторского права в нижней части страницы. Чтобы сделать размер текста больше, его можно поместить в дескрипторы <Н1>. Также выровняйте текст по центру.
660 Часть III. Программирование для Web 7. Во второй строке добавьте элемент ContentPlaceHolder в каждый столбец. Присвойте имя ContentPlaceHolderMenu элементу ContentPlaceHolder в левой части и имя ContentPlaceHolderMain элементу ContentPlaceHolder в правой части. 8. Добавьте текст Default content of the Registration Demo Web Master в элемент ContentPlaceHolder в правой части. 9. Придайте размеры таблице на странице так, как показано на рис. 20.3. Registration Demo Web Default Content of the Registration Demo Web Copynete (O 2008 WW* Prtss Рис. 20.3. Установка размеров страницы Описание полученных результатов Как уже упоминалось ранее, мастер-страница содержит HTML-код с дескрипторами <F0RM>, содержащими заполнители, содержимое которых будет заменено страницами, использующими мастер-страницу. HTML-таблица определяет компоновку страницы: <%@ Master Language="C#" AutoEventWireup="true" CodeFile="EventRegistration.master.cs" Inherits="EventRegistration" %> <!D0CTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtmlll/DTD/xhtmlll.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Untitled Page</title> <asp:ContentPlaceHolder id="head" runat="server"> </asp:ContentPlaceHolder> <style type="text/css"> .stylel { width: 100%; height: 353px; style2 text-align: center; style3 font-size: smaller; style4 height: 58px; }
Глава 20. Расширенное программирование для Web 661 ,style5 { height: 24 9рх; } </style> </head> <body> <form id="forml" runat="server"> <table class="stylel"> <tr> <td colspan=" class="style4"> <hl class="style2">Registration Demo Web</hl> </td> </tr> <tr> <td class="style5"> <asp:ContentPlaceHolder ID="ContentPlaceHolderMenu" runat="server"> </asp:ContentPlaceHolder> </td> <td> <asp:ContentPlaceHolder ID="ContentPlaceHolderMain" runat="server"> <p>Default content of the Registration Demo Web</p> </asp:ContentPlaceHolder> </td> </tr> <tr> <td class="style3" colspan=" style="text-align: center"> Copyright (C) 2008 Wrox Press</td> </tr> </table> </form> </body> </html> После того как создадите мастер-страницу, вы сможете использовать ее из Web- страницы, как показано в следующем упражнении. Практическое занят*в|~~~ ИСПОЛЬЗОВЭНИе МЭСТер-СТраНИЦЫ 1. Добавьте новый элемент Web Form в Web-приложение и присвойте ему имя EventList .aspx. Отметьте флажок Select Master Page (Выбрать мастер-страницу), как показано на рис. 20.4. 2. Щелкните на кнопке Add (Добавить), чтобы открыть диалоговое окно Select Master Page (Выбрать мастер-страницу). Выберите ранее созданную мастер-страницу EventRegistration.master (рис. 20.5) и щелкните на кнопке ОК. 3. В окне Source View (Представление исходного кода) файла EventList .aspx будет показано только три элемента управления Content после директивы Page, которая ссылается на элементы управления ContentPlaceHolder из мастер-страницы. Измените свойства ID элементов управления Content на ContentHead, ContentMenu и ContentMain: <%@ Page Language="C#" MasterPageFile="~EventRegistration.master" AutoEventWireup="true" CodeFile="EventList.aspx.cs" Inherits="EventList" Title="Untitled Page" %>
662 Часть III. Программирование для Web <asp:Content ID=l,ContentHeadM ContentPlaceHolderID="head" Runat="Server"><asp:Content> <asp:Content ID="ContentMenu" ContentPlaceHolderID="ContentPlaceHolderMenu" Runat="Server"xasp: Content> <asp:Content ID="ContentMain" ContentPlaceHolderID="ContentPlaceHolderMain" Runat="Server"><asp:Content> 4. Откройте окно Design View в Visual Studio. В этом окне вы увидите содержимое мастер-страницы, которое нельзя изменить из страницы, с заголовком и сведениями об авторских правах (рис. 20.6). 5. Вставьте таблицу с двумя строками и двумя столбцами в элемент управления ContentMail. Добавьте элемент управления Calendar в левый столбец первой строки и элемент управления ListBox в правый столбец первой строки. Содержимое меню будет добавлено позже. Когда вы откроете окно Source View, вы сможете увидеть таблицу внутри элемента управления Content. 6. Просмотрите страницу в Web-браузере. Страница включает содержимое из Web- страницы, а также данные из мастер-страницы. { AddMowlltm ( begcfrurptfvon 1 lewptottv V-iu* Stu*o miltHtit temoitte» 1 l^wiSLttai 1 -p ADO NTT entity D»U Mod* 1 I ttl&JAX Clxnt llbrtry 1 JjJaja» ,rubied WCFServtce 1 : fjk ОИ1 Cheor»* I j i % Generic rtonolet 1 jJUtNQtoSQlCUHei _JP*»Obl(tFUt !J iQl Server Dtitbeie 1 ! M vVCF Service 11 ; jj M Ш i MyTrmpUt« 1 _j Seercn OnUne Теmpfetei J; AfoimforWeB ApplKJtloni 1! i«r t. <• -. • aa* UhmOBH ^3 Matter P»e« Oj a;a> снега oertewor 2] ajaj Master Paoe vlBfOwtff File JJCrrrtalReeort ^итм1Р4в» J Report ,Ъ Site Map AJ Style Sheet _fr weo ConftOAtntton File j|j XMl Schema """< ^pa«corJeir "" #Jete«t matte нБП тр1 I * 1 I Web Шсг Control 1 4}AJAX CHent Control 1 JJajax WeoFor» 1 «Jem» 1 4j OettSet I «jj;s<fpt Ftie 1 ,j Pepol .'..;«.J 1 ^SiinFue I 3 Te»t FUe 1 |J wet Servite 1 >«SUFHe 1 separate' Je I >*8' 1 1 Mi, J i..o^J 1 Рис. 20.4. Отметка флажка Select Master Page Selec t a Mister Pag* Project folders J ♦ JJ Admin t -ji App.Cod* % «.j App.D»ta . _j Intro СЖ. Рис. 20.5. Выбор мастер-страницы Even tRegis tra tion. master
Глава 20. Расширенное программирование для Web 663 1 iMiiiiTiwi |. рявЫШ/вшйшА Д1 Г Ifa о«ч»п : п $рж asount Registration Demo Web И1 С iti; ИИ (С) аМ Wmhtii e*tf«ft«g4itr«i ль ■..--* - Рис. 20.6. Мастер-страница Even tRegi s tra tion. master в окне Design View Система навигации по сайту Для организации перехода к множеству страниц Web-сайта можно создать XML- файл с описанием структуры Web-сайта и применить некоторые элементы управления пользовательского интерфейса для отображения опций навигации. В табл. 20.1 перечислены наиболее важные элементы управления навигацией. Таблица 20.1. Важные элементы управления навигацией Элемент управления Описание SiteMapDataSource Menu SiteMapPath TreeView Элемент управления SiteMapDataSource представляет собой элемент управления — источник данных, который ссылается на любого поставщика данных карты сайта. Этот элемент управления можно найти в панели инструментов Visual Studio Toolbox в разделе Data (Данные) Этот элемент управления отображает ссылки на страницы, как это определено в источнике данных карты сайта. Его можно отображать как горизонтально, так и вертикально. Кроме того, он имеет множество опций, позволяющих настраивать стиль элемента управления Элемент управления SiteMapPath использует минимальное пространство для отображения текущей позиции страницы внутри иерархии Web- сайта. Вы можете отобразить гиперссылки на текст или изображение Элемент управления TreeView отображает иерархический вид Web-сайта Создание системы навигации 1. Откройте Web-сайт EventRegistrationWeb. 2. Добавьте новый элемент Site Map (Карта сайта) в Web-сайт, щелкнув правой кнопкой мыши на проекте в окне Solution Explorer (Проводник решений), и выбрав пункт меню Add New Item (Добавить новый элемент). Присвойте ему имя Web.sitemap.
664 Часть III. Программирование для Web 3. Измените содержимое файла так, как показано ниже: <?xml version=.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0"> <siteMapNode url="Default.aspx" title="Home"> <siteMapNode url="Register.aspx" title="Register" /> <siteMapNode url="EventList.aspx" title="Events" /> </siteMapNode> </siteMap> 4. Откройте файл EventRegistration.master. 5. Отыщите элемент управления SiteMapDataSource на вкладке Data Tab (Данные) панели инструментов и добавьте его на страницу. 6. Добавьте элемент управления Menu, который находится на вкладке Navigation (Навигация) под заголовком Registration Demo Web. В качестве источника данных укажите SiteMapDataSourcel. 7. Добавьте элемент управления SiteMapPath ниже элемента управления Menu. 8. С помощью Web-браузера откройте файл EventList. aspx. Обратите внимание на меню и путь, который отображает местонахождение текущего файла в Web-сайте. 9. При необходимости вы можете добавить другие страницы, на которые имеются ссылки в файле Web. sitemap, ссылаясь на ту же мастер-страницу, чтобы показать определенные меню. Описание полученных результатов Структура Web-сайта определена Web-страницами, перечисленными в файле Web. sitemap. Этот XML-файл содержит элементы XML <siteMapNode> внутри корневого элемента <siteMap>. Элемент <siteMapNode> определяет Web-страницу. Имя файла страницы задано посредством атрибута url, а атрибут title определяет имя так, как оно должно отображаться в меню. Иерархия страниц определяется с помощью элементов <siteMapNode> в качестве дочерних элементов страницы, на которой должна быть ссылка на дочерний элемент. Элемент управления SiteMapDataSource представляет собой элемент управления источника данных с такими же особенностями, которыми обладают элементы управления данных в предыдущей главе. Этот элемент управления может использовать разных поставщиков. По умолчанию класс XmlSiteMapProvider применяется для получения данных. По умолчанию этот класс использует страницу Web. sitemap, поэтому вам не нужно конфигурировать это имя файла. Если вы переименуете XML-файл, то свойство siteMapFile этого поставщика должно будет содержать новое имя файла. Посредством элемента управления Menu вы можете редактировать элементы меню, которые появляются в исходном коде ASPX; или же вы можете добавить элементы меню программно. Самый легкий способ добавления элементов меню заключается в использовании источника данных карты сайта посредством конфигурирования источника данных. Пользовательские элементы управления Если на нескольких Web-страницах присутствует множество связанных между собой элементов управления, вы можете создать пользовательские элементы управления. Пользовательский элемент управления имеет расширение файла .ascx и содержит части формы, которые можно включать в Web-страницы. Вместо того чтобы создавать пользовательский интерфейс и писать код несколько раз для каждой страницы, на ко-
Глава 20. Расширенное программирование для Web 665 торой необходимо поместить один и тот же элемент управления, пользовательский элемент управления реализуется только один раз, а используется несколько раз. Пользовательский элемент управления может быть добавлен на Web-страницу статическим или динамическим образом. В случае динамического добавления пользовательские элементы управления можно изменять в течение времени выполнения. Пользовательский элемент управления определяется посредством директивы Control в первой строке файла исходного кода. Атрибуты CodeFile и Inherits используются точно так же, как и директива Page: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="DemoUserControl .ascx.cs" Inherits="DemoUserControl11 %> За директивой Control следует обычный HTML-код, являющийся частью формы. Дескрипторы <HTML> и <F0RM> не используются внутри пользовательского элемента управления, поскольку HTML-код пользовательского элемента управления внедряется в HTML-форму. Пользовательский элемент управления может быть добавлен на страницу статическим образом, как показано в следующем примере ASPX-кода. На него есть ссылка в директиве Register. Атрибут Src ссылается на ASCX-файл пользовательского элемента управления, который в этом примере имеет имя DemoUserControl. ascx. Атрибуты TagPref ix и TagName определяют имена элемента управления, который используется внутри страницы. TagPref ix имеет значение ucl, a TagName — DemoUserControl. Вот почему ссылка на сам элемент управления внутри страницы обеспечивается посредством <ucl: DemoUserControlX Вы можете сравнить префикс ucl с префиксом asp, который используется в серверных элементах управления ASP.NET. <%@ Page Language=MC#" AutoEventWireup="true" CodeFile="DemoPage.aspx.cs" Inherits="DemoPage" %> <%@ Register tagprefix="ucl" tagname="DemoUserControl11 src="DemoUserControl.ascx" %> <htral xmlns="http://www.w3.org/1999/xhtml"> <ucl:DemoUserControl ID="DemoUserControll" runat="server" /> Вместо того чтобы объявлять пользовательский элемент управления статическим образом, его можно загружать динамически. Чтобы реализовать динамический пользовательский элемент управления, необходимо поместить на страницу элемент управления PlaceHolder. Пользовательские элементы управления можно добавлять в коллекции Controls заполнителя. Показанный обработчик события PageLoad демонстрирует загрузку пользовательского элемента управления DemoUserControl. ascx методом LoadControl класса Page. Возвращенный пользовательский элемент управления добавляется в коллекцию Controls элемента управления PlaceHolder под именем PlaceHolder 1. Используя такой прием, можно загружать разные пользовательские элементы управления с учетом пользовательских настроек, беря за основу строку URL или отправленные данные: void Page_Load(object sender, EventArgs e) { Control cl = LoadControl("DemoUserControl.ascx"); PlaceHolderl.Controls.Add(cl); } Создавать и использовать пользовательские элементы управления можно без особого труда. Вы можете очень быстро создать такой элемент, выполнив следующее практическое занятие. В нем мы создадим новый специальный пользовательский элемент управления, используемый для отображения календаря и называний стран для событий из раскрывающегося списка.
ббб Часть III. Программирование для Web Практическое зам Создание пользовательского элемента управления 1. Откройте ранее созданный Web-сайт EventRegistrationWeb. 2. Добавьте новый пользовательский элемент управления, выбрав пункт меню Website^Add New Item (Web-cam"^Добавить новый элемент); выберите шаблон Web User Control (Пользовательский элемент управления) и назовите его ListEvents.ascx. 3. Добавьте в пользовательский элемент управления таблицу с двумя строками и одним столбцом. 4. С помощью конструктора добавьте элемент управления Calendar и раскрывающийся список в пользовательский элемент управления, как показано на рис. 20.7. Присвойте календарю имя EventCalendar, а списку— DropDownListCountries. 5. Присвойте свойству AutoPostback раскрывающегося списка значение true. 6. Откройте файл исходного кода пользовательского элемента управления ListEvents, ascx. cs. Добавьте общедоступный метод Configure (), показанный ниже, чтобы инициализировать элементы пользовательского элемента управления. Общедоступные методы и свойства можно вызывать из Web-страниц через элементы управления. public void Configure(DateTime date, params string[] countries) { DropDownListCountries.Items.Clear(); EventCalendar.SelectedDate = date; Listltem[] items = new ListItem[countries.Length] ; for (int i = 0; i < countries.Length; i++) { items[i] = new ListItem(countries [i]); } DropDownListCountries.Items.AddRange(items); Sun Mob Tee Wed I bu Fri Sat 30 31 6 7 13 14 20 21 27 2* 3 4 | Unbound 1 1 15 22 29 < ^ 9 16 23 <o 6 -g 3 10 17 24 31 7 4 11 18 29 l 8 5 12 19 16 2 9 a D«iigo 3 SpW £ Some» * «tr> <W> [ Puc. 20.7. Добавление элементов управления
Глава 20. Расширенное программирование для Web 667 Теперь пользовательский элемент управления можно использовать внутри Web- страниц. В следующем упражнении мы создадим Web-страницу, отображающую пользовательский элемент управления в виде ее содержимого. Практическое занятие] работа С ПОЛЬЗОВателЬСКИМ элементом управления 1. Создайте новую Web-страницу по имени UseMyControl. aspx. 2. Откройте Web-страницу в окне Design View. Перетащите пользовательский элемент управления из окна Solution Explorer на поверхность проектирования. Содержимое пользовательского элемента управления без промедления будет показано в окне проектирования. Если щелкнуть на коде ASPX, вы сможете увидеть добавленную директиву Register и пользовательский элемент управления. 3. Откройте исходный код и добавьте код инициализации для пользовательского элемента управления, вызвав созданный ранее общедоступный метод: protected void Page_Load(object sender, EventArgs e) { if (IPage.IsPostBack) { ListEventsl.Configure(DateTime.Today, "Italy", "France", "Germany", "Spain"); 4. Запустите Web-страницу. В календаре будет выделена текущая дата, а в раскрывающемся списке будет указан список доступных названий стран. Профили Многие коммерческие Web-сайты позволяют пользователям добавлять продукты в корзину покупателя, однако если вы отвлечетесь на некоторое время, не отправив заказ, сеанс будет завершен. Какой урок можно извлечь из этого? Лучше всего не хранить заказы только в переменных сеанса, поскольку информация о состоянии сеанса будет утеряна по достижении таймаута сеанса. Для постоянства информации о состоянии можно использовать профили. Профили хранятся в базе данных, поэтому они сохраняются также в тех случаях, когда сеансы заканчиваются. Профили обладают следующими характеристиками. □ Профили хранятся постоянно. Управлением хранимых данных занимается поставщик профиля. В версии ASP.NET 3.5 поддерживаются поставщики профилей для SQL Server и Access. Более того, можно самостоятельно разрабатывать специальные поставщики профилей. □ Профили строго типизированы. Переменные сеанса и приложения используют индексатор, а для доступа к данным необходимо выполнять приведение типов. Когда вы используете профили, свойства с именем профиля генерируются автоматически. Имена и типы свойств указываются в конфигурационном файле. □ Профили могут использоваться как для анонимных, так и для зарегистрированных пользователей. Идентификационная информация анонимного пользователя может храниться в cookie-наборе. По мере того как пользователь будет регистрироваться на Web-сайте, состояние от анонимного пользователя может быть преобразовано в состояние зарегистрированного пользователя.
668 Часть III. Программирование для Web В следующем упражнении мы создадим информацию о профиле пользователя и воспользуемся ею. "?™1!^^ Создание информации профиля 1. Откройте ранее созданное Web-приложение EventRegistrationWeb. 2. Откройте файл web.config, который был создан в предыдущей главе. Если вы не видите этот файл в окне Solution Explorer, создайте такой файл, выбрав пункт меню Add New Item^Web Configuration File (Добавить новый элементе Конфигурационный файл). 3. Добавьте раздел <prof ile>, содержащий свойства Country, Visits и LastVisit, в файл web.config, как показано ниже. Свойство Country имеет тип String (по умолчанию), свойство Visits имеет тип Int32, а свойство LastVisit имеет тип DateTime: <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web> <!— ... —> <profile> <properties> <add name="Country" /> <add name="Visits" type="System.Int32" defaultValue=" /> <add name="LastVisit" type="System.DateTime" /> </properties> </profile> <!-- ... —> </system.web> </configuration> 4. Создайте новую Web-страницу ProfileDemo.aspx и добавьте три метки для отображения текущих значений, а также раскрывающийся список для выбора названия страны. Присвойте меткам имена Label LastVisit, LabelVisitCount и LabelSelectedCountry. Присвойте раскрывающемуся списку имя DropDownListCountries, добавьте в него названия нескольких стран и присвойте свойству AutoPostBack значение true. 5. Добавьте обработчик события OnCountrySelection для события SelectedlndexChange раскрывающегося списка, как показано ниже: protected void OnCountrySelection(object sender, EventArgs e) { Profile.Country = this.DropDownListCountries.Selectedltem.Value; Profile.Save(); } 6. Добавьте следующий код в обработчик события PageLoad (), чтобы отображать текущие значения из профиля. Profile — это динамически создаваемое свойство класса Page, свойства которого определены в конфигурационном файле. Все эти свойства являются строго типизированными — имена и типы свойств совпадают с именами и типами, определенными в конфигурационном файле. void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { DropDownListCountries.SelectedValue = Profile.Country; }
Глава 20. Расширенное программирование для Web 669 LabelLastVisit.Text = Proflie.LastVisit.ToLongTimeString (); LabelVisitCount.Text = Proflie.Visits.ToString (); LabelSelectedCountry.Text = Profile.Country; Proflie.Visits++; Profile.LastVisit = DateTime.Now; Profile.Save(); } 7. Теперь вы можете запустить и просмотреть Web-страницу. Выберите название страны из списка. Закройте Web-браузер и запустите его снова. Вы убедитесь в том, что информация из профиля будет сохраняться в промежутках между сеансами. Описание полученных результатов Свойство Profile, которое вы можете использовать внутри кода Web-страницы, создается динамически. Данное свойство не упоминается при описании класса Page в документации MSDN, потому что оно не существует как часть класса Page. Вместо этого, благодаря конфигурации свойства в конфигурационном файле web.config, свойство Profile создается автоматически. Тип свойства Profile — это динамически создаваемый класс, являющийся потомком класса Prof ileBase, и содержит свойства, определенные в конфигурационном файле. Группы профилей Профили могут отслеживать большой объем информации, которую можно организовывать в виде групп. Группа свойств определяется посредством элемента <group>, как показано ниже: <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web> <profile> <properties> <add name="Country" /> <add name="Visits" type="System.Int32" defaultValue=M0" /> <add name="LastVisit" type="System.DateTime" /> <group name="EventSelection"> <add name="Country" /> <add name="City" /> <add name="StartDate" type="System.DateTime" /> <add name="EndDate" type="System.DateTime" /> </group> </properties> </profile> <!— ... --> </system.web> </configuration> Чтобы получить доступ к значениям профиля, определенным внутри групп, создается динамическое свойство, которое можно применять для доступа к значениям свойств: string country = Proflie.EventSelection.Country; string city = Profile.EventSelection.City; DateTime startDate = Profile.EventSelection.StartDate; DateTime endDate = Profile.EventSelection.EndDate;
670 Часть III. Программирование для Web Профили и компоненты Внутри кода компонентов доступ к информации профиля невозможно получить с помощью свойства Profile. Вместо этого, доступ к профилю производится посредством свойства HttpContext. Кроме того, в этом случае невозможно строго определить типы. В следующем примере показан вариант доступа к строке профиля Country, определенной ранее. Current — это статическое свойство класса HttpContext, которое возвращает объект HttpContext, связанный с сеансом клиента. Свойство Profile возвращает объект типа ProfileBase, в котором к каждому значению профиля можно получить доступ посредством индексатора. Индексатор определяется для возвращаемых объектов типа Object, поэтому перед присваиванием значения переменной необходимо выполнить приведение к типу: string country = (string)HttpContext.Current.Profile["Country"] ; Профили и специальные типы данных Благодаря профилям, вы можете хранить не только базовые типы данных, такие как int, short, string и DateTime, но и специальные типы данных. Тип данных необходим для поддержки единого механизма сериализации. В следующем списке поясняются механизмы сериализации, поддерживаемые профилями. □ XML-сериализация. Этот механизм используется по умолчанию. Он позволяет сохранять все общедоступные свойства и поля. Для этого механизма необходимо применять конструктор по умолчанию. XML-сериализация рассматривается в главе 23. □ Двоичная сериализация. Для этого вида сериализации класс нужно пометить атрибутом [Serializable]. Сериализуются все приватные поля. Сериализация во время выполнения рассматривается в главе 22. □ Строковая сериализация. При этом виде для сериализации данных можно использовать преобразователь типов. Если для выполнения сериализации нет преобразователя типов, вызывается метод ToString (), который преобразовывает объект в строку. Используемый в нашем случае механизм сериализации определен посредством атрибута serializeAs. Если этот атрибут не задан, используется XML-сериализация. Возможными вариантами для этого атрибута являются Binary, Xml и String. <profile> <properties> <add naxne="Demo" type="MyClass" serializeAs=MBinary" /> </properties> </profile> Профили и анонимные пользователи По умолчанию профили можно применять только для аутентифицированных пользователей. Чтобы использовать профили и для анонимных пользователей, их необходимо указать в конфигурационном файле. В этом случае каждое свойство профиля, которое предполагается быть доступным для анонимных пользователей, должно быть установлено индивидуально: <configuration> <system.web> <anonymousIdentification enabled="true" />
Глава 20. Расширенное программирование для Web 671 <profile> <properties> <add names"Country" allowAnonymous="true" /> </properties> </profile> Когда доступ к профилю осуществляется для анонимного пользователя, в базе данных поставщика профиля создается запись для анонимного пользователя. Анонимный пользователь получает идентификатор (ID), подобный аутентифицированному пользователю. По умолчанию идентификатор анонимного пользователя хранится внутри cookie-набора, хотя вместо cookie-наборов можно использовать идентификаторы анонимного пользователя внутри URL-строк (т.н. изменение URL (URL-mungling)). Часто бывает так, что информацию профиля об анонимном пользователе необходимо передать для зарегистрированных пользователей. Например, пользователь может добавить продукты в корзину товаров, не пройдя регистрацию; а чтобы отправить заказ, пользователь должен зарегистрироваться в системе. В подобных случаях вся информация о профиле, которая хранилась для анонимного пользователя, должна быть передана зарегистрированному пользователю. Для работы с анонимными профилями в файле global. a sax можно задействовать события, перечисленные в табл. 20.2. Таблица 20.2. События для анонимных профилей Имя обработчика Описание Anonymousidentif ication_Create Когда анонимный пользователь заходит на Web- сайт впервые, создается анонимный профиль. В этот момент вызывается обработчик события AnonymousIdentification_Create Anonymousidentif icationRemove Когда зарегистрированный пользователь заходит на Web-страницу, а запрос зарегистрированного пользователя содержит идентификатор профиля анонимного пользователя, профиль анонимного пользователя удаляется, и вызывается обработчик события AnonymousIdentification_Remove Prof ile_MigrateAnonymous После события Anonymousidentif ication_ Remove вызывается обработчик события Prof ile_ MigrateAnonymous, благодаря которому анонимный профиль можно передавать в профиль для зарегистрированного пользователя Реализация обработчика ProfileMigrateAnonymous в файле global, asax выглядит так, как показано в следующем примере. Все значения профиля, доступные для анонимных пользователей, могут быть переданы в конфигурацию зарегистрированного пользователя. В примере кода происходит перенос значения профиля Country, доступного для анонимных пользователей, в конфигурационный файл. Сначала проверяется Profile. Country, чтобы выяснить, содержит ли он уже значение. Profile. Country будет иметь значение, если пользователь уже был зарегистрирован в системе в тот момент, когда создавалось значение профиля. Оно не будет содержать значение, если до этого момента пользователь был неизвестен в системе. Здесь метод GetProfile (), который передает идентификатор пользователя, помогает получить доступ к информации профиля от других пользователей. Несмотря на то что за одним клиентским компьютером сидит один и тот же человек, у заре-
672 Часть III. Программирование для Web гистрированного пользователя и анонимного пользователя всегда имеются разные идентификаторы. Когда анонимный пользователь регистрируется в системе и становится аутентифицированным пользователем, идентификатор анонимного пользователя передается в аргументе Prof ileMigrateEventArgs метода Profile_ MigrateAnonymous (). Идентификатор анонимного пользователя (е. Anonymous Id) передается методу GetProfile () для доступа к профилю анонимного пользователя. Эта информация профиля передается в информацию профиля аутентифицированно- го пользователя путем присваивания всех значений профиля (как Country в нашем примере). void Profile_MigrateAnonymous(object sender, ProfileMigrateEventArgs e) { if (Profile.Country == null) { Profile.Country = Profile.GetProfile(e.AnonymousId).Country; } } Web-части Действительно мощным средством, которое позволяет настраивать Web-приложения под каждого пользователя, являются Web-части (Web Parts). Благодаря Web-частям можно с легкостью создавать Web-приложения в стиле порталов. Примером такого приложения для портала из разряда социальных сетей является Facebook.com (рис. 20.8), который позволяет пользователям организовывать информацию так, как им этого захочется. Web-части можно организовывать для поддержания контакта с другими пользователями, получения информации о поездках, обсуждения тем во многих группах и т.д. Благодаря возможности персонально настраивать Web-портал, пользователь может определить, какие Web-части должны быть доступными. Рис. 20.8. Реальный пример портала
Глава 20. Расширенное программирование для Web 673 Инфраструктура Web-частей состоит из зон (zone) и частей (part). Части, которые могут находиться внутри зоны, зависят от типа зоны; например, часть LayoutEditorPart может быть частью зоны EditorZone. Главной зоной является зона Web Parts. Web-страница может иметь несколько зон Web Parts для определения компоновки страницы. Внутри этой зоны может содержаться множество Web-частей, пользовательских или серверных элементов управления. Зона Catalog позволяет пользователю или администратору выбрать Web-часть из каталога для отображения внутри Web-страницы. Для этой зоны могут использоваться части PageCatalogPart, ImportCatalogPart и DeclarativeCatalogPart. Благодаря зоне Editor пользователь или администратор могут редактировать поведение и внешний вид отдельных Web-частей. Внешний вид Web-части можно настраивать с помощью следующих элементов: AppearanceEditorPart, BehaviourEditorPart и LayoutEditorPart. Зона Connections используется для обеспечения связи между множеством Web- частей. Например, если пользователь выбирает название месяца года в одной Web- части, то другая Web-часть, отображенная на той же странице, будет знать об этом, и соответствующим образом изменит свое поведение. Диспетчер Web-частей Для любого сайта, на котором используются Web-части, необходим специальный элемент управления— администратор Web-частей WebPartManager. Он отвечает за управление Web-частями. Благодаря этому элементу управления можно создавать, удалять или перемещать элементы управления Web-частей. Единственным требованием в отношении WebPartManager является то, что он должен присутствовать на Web-странице, как показано в следующем коде ASPX: <asp:WebPartManager ID="WebPartManagerl" runat="server"></asp:WebPartManager> Этот элемент управления не имеет пользовательского интерфейса во время выполнения, и нет необходимости конфигурировать какие-либо свойства. Его свойства просто позволяют конфигурировать некоторые предупреждающие сообщения, отображаемые перед закрытием или удалением Web-частей. Применительно к WebPartManager интерес представляют те методы, которые выборочно можно применять для изменения режима отображения, как будет показано далее. Зона Web-частей Чтобы иметь возможность использовать каркас Web-частей, для этого как минимум должна существовать зона WebPartZone. Зона WebPartZone содержит элементы управления Web-частей и определяет компоновку и внешний вид этих элементов управления внутри зоны. Как уже было отмечено ранее, зона WebPartZone может содержать пользовательские элементы управления, Web-части и серверные элементы управления. Внутри страницы ASPX зона WebPartZone включает шаблон ZoneTemplate, содержащий элементы управления для отображения. Пример, показанный ниже, содержит ранее созданный пользовательский элемент управления ListEvents: <asp:WebPartZone ID="EventsZone11 runat="server"> <ZoneTemplate> <ucl:ListEvents ID="ListEventsl" runat="server" /> </ZoneTemplate> </asp:WebPartZone> Благодаря зоне WebPartZone можно определять компоновку, цвет и меню. В приведенном ниже примере продемонстрирована зона WebPartZone с заранее определен-
674 Часть III. Программирование для Web ной профессиональной компоновкой для установки цветов и стилей рамки и меню Web-частей: <asp:WebPartZone BorderColor="#CCCCCC" Font-Names="Verdana" ID="EventZone" Padding=" runat="server"> <PartChromeStyle BackColor="#F7F6F3" BorderColor="#E2DED6" Font-Names="Verdana" ForeColor=MWhite" /> <MenuLabelHoverStyle ForeColor="#E2DED6" /> <EmptyZoneTextStyle Font-Size=.8em" /> <MenuLabelStyle ForeColor="White" /> <MenuVerbHoverStyle BackColor="#F7F6F3" BorderColor="#CCCCCC" BorderStyle="Solid" BorderWidth="lpx" ForeColor="#333333" /> <HeaderStyle Font-Size=.7em" ForeColor="#CCCCCC" HorizontalAlign="Center" /> <MenuVerbStyle BorderColor="#5D7B9D" BorderStyle="Solid" BorderWidth="lpx" ForeColor="White" /> <PartStyle Font-Size=.8em" ForeColor="#333333" /> <TitleBarVerbStyle Font-Size=.6em" Font-Underline="False" ForeColor="White" /> <MenuPopupStyle BackColor="#5D7B9D" BorderColor="#CCCCCC" BorderWidth="lpx" Font-Names="Verdana" Font-Size=.6em" /> <PartTitleStyle BackColor="#5D7B9D" Font-Bold="True" Font-Size=.8em" ForeColor="White" /> <ZoneTemplate> <ucl:ListEvents ID="ListEventsl" runat="server" /> </ZoneTemplate> </asp:WebPartZone> В следующем практическом занятии мы создадим Web-страницу WebPartDemo. aspx, выполняя действия, необходимые для использования каркаса Web-частей. Мы добавим два пользовательских элемента управления в две разные зоны. Практическое занятие СоЗДЭНИе Web-ПрИЛОЖвНИЯ с помощью Web-частей 1. Откройте ранее созданное Web-приложение EventRegistrationWeb. 2. Добавьте новую страницу под именем WebPartsDemo.aspx. 3. Добавьте элемент управления WebPartManager на эту страницу (его можно найти на вкладке WebParts (Web-части) панели инструментов). 4. Добавьте таблицу с двумя столбцами и двумя строками, чтобы организовать компоновку Web-частей. 5. Внутри каждого столбца во второй строке добавьте элемент управления Web Part Zone. Позже эти элементы управления будут добавлены в первую строку. Присвойте идентификатору (ID) зоны WebPartZone значение ZoneLef t, а идентификатору второй зоны — значение ZoneRight. 6. С помощью смарт-тега первого элемента управления WebPartZone в окне Design View выберите формат Professional Auto Format (Профессиональный автоматический формат). Чтобы легче было различать элементы управления WebPartZone, для второго элемента выберите вариант Colorful Auto Format (Полноцветный автоматический формат).
Глава 20. Расширенное программирование для Web 675 7. Присвойте свойству HeaderText первого элемента управления WebPartZone значение Events, а такому же свойству второй зоны — значение Weather. 8. Добавьте пользовательский элемент управления ListEvents .ascx, созданный ранее, в левый элемент управления WebPartZone. 9. Создайте новый пользовательский элемент управления Weather .ascx, чтобы показать информацию о погоде на основе выбранного страны. На данный момент пользовательскому элементу управления необходима лишь одна метка по имени LabelWeather. Вы можете использовать случайный алгоритм, как показано ниже, если необходимо отобразить такие состояния погоды, как Sun (солнечно), Cloudy (облачно) или Rain (дождь). Добавьте этот пользовательский элемент управления в правый элемент управления WebPartZone. public partial class Weather : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { Random r = new Random () ; int n = r.Next A, 3) ; switch (n) { case 1: LabelWeather.Text = "Sun"; break; case 2: LabelWeather.Text = "Rain"; break; case 3: LabelWeather.Text = "Cloudy"; break; } } } 10. Запустите Web-страницу. На рис. 20.9 показаны пользовательские элементы управления. Обратите внимание на рамку вокруг элементов управления с заголовком Untitled (Без заголовка) и небольшой треугольник, направленный вниз (кнопка Verb) в правой части рамки. Эта рамка известна как хром (chrome). Щелкните на кнопке Verb, чтобы посмотреть элементы меню Minimize (Свернуть) и Close (Закрыть). Попытайтесь свернуть элемент управления и восстановить его снова, но не нажимайте кнопку Close (Закрыть), поскольку без сконфигурированного каталога невозможно повторно открывать уже закрытые элементы управления. 11. Теперь давайте поработаем над возможностью закрытия элементов управления. Откройте Web-страницу еще раз в окне проектирования и выберите пункт меню Web Part (Web-часть). Откройте свойство CloseVerb в окне Property Editor (Редактор свойств). Присвойте параметру Enabled этого свойства значение false. Этим вы заблокируете доступ к выбору элемента меню Close. Чтобы удалить элемент меню, присвойте параметру Visible такое же значение false. 12. Откройте еще раз Web-страницу в окне Web-браузера. Обратите внимание на то, что теперь вы не можете выбрать элемент меню Close.
676 Часть III. Программирование для Web f tfUnmrtPag* Windows MM Exptom I • yVj- JC М»/Лн<>иН»П/1I—ц I hi <м iuMMPaoi i January 2008 £ Sun Mon Tue Wed Thu Fri Sat a 2i i 2 a i s fi 2 S S 1Q 11 12 12 U 15 16 1Z IB 12 2Q 21 22 U 24 25 2fi 2Z 2fi 22 M 21 1 2 a i s • z • i Austria » *Щ Local Mr "|h ъ• anetIF X i 1 iive Swrrf. О S " #-H>»*» Sun rotated Mod*: On T^mSBI p * 1 >Щ*ш« "I 1 Чш» -■ || Pwc. 2(9.9. Пользовательские элементы управления Зона Editor Зона Editor используется для изменения внешнего вида, поведения и компоновки Web-частей. Для каждой из этих задач используются разные части редактора. Части редактора являются активными, когда элемент управления Web Parts установлен в режим Edit (Редактирование). Часть редактора Layout (Компоновка), показанная на рис. 20.10, позволяет задавать состояние хрома, зону, в которой будет размещена Web-часть, и порядок Web-части внутри этой зоны. Хром может иметь состояния Normal (Обычный) и Minimized (Свернут). В состоянии Mimimized Web-часть отображается только в виде маленького значка, чтобы не занимать пространство внутри Web-страницы. Пользователь может восстановить Web-часть до нормального вида, просто щелкнув на значке. В комбинированном окне Zone (Зона) перечислены имена зон, в которых могут находиться элементы управления Web-частей. Layout Chrome State Nofmal » Zone Events Zone Index 0 Puc. 20.10. Часть редактора Layout Часть редактора Appearance (Внешний вид) позволяет задавать заголовок, тип хрома и его размер. На рис. 20.11 показаны все возможные параметры настройки части редактора Appearance. Заголовок нужен для того, чтобы присвоить Web-части новый заголовок. Тип хрома определяет видимость заголовка и рамку (границы). Также можно задать высоту и ширину элемента управления.
Глава 20. Расширенное программирование для Web 677 Appearance Tide: Chrome Type: Default Direction Not Set Height pixels Width: pixels Hidden " - Puc. 20.11. Часть редактора Appearance Часть редактора Behavior (Поведение), показанная на рис. 20.12, позволяет задать ссылки на заголовок и изображения, ссылки на каталог и изображения, а также указать, разрешено ли закрывать, редактировать, скрывать и сворачивать элементы управления Web-части. Режим Export (Экспорт) определяет, можно ли экспортировать информацию элемента управления Web-части на локальный диск пользователя. В данном случае экспортировать на локальный диск можно только параметры настройки элемента управления Web-части. Эту конфигурацию можно будет использовать впоследствии для импортирования элемента управления Web-части на любой Web-сайт, имеющий такие же элементы управления Web-части. г Behavior — Description 1 ШеШс 1 Tide Icon Image Link. Catalog Icon Image Link: 1 HdpLmk: 1 Help Mode | Modal 3 Import Error Message I Export Mode | Do not allow Attfhohzation Fiker d Г Г Alow Close Г* Alow Connect I Г Alow Edit Г Alow Hide Г Allow Minimize Г Alow Zone Change Puc. 20.12. Часть редактора Behavior
678 Часть III. Программирование для Web 1!?!!!!^^^ Добавление зоны Editor Зона Editor позволяет изменять внешний вид, поведение и компоновку Web-частей. 1. Откройте ранее созданную Web-страницу WebPartDemo. aspx. 2. Добавьте элемент управления Edit or Zone в первую строку таблицы внутри страницы. 3. Добавьте в зону Editor части AppearanceEditorPart и LayoutEditorPart. 4. Добавьте элемент управления DropDownList в верхнюю часть страницы. Этот элемент управления будет использоваться для смены режима Browse (Обзор) на Edit (Редактирование). Присвойте этому элементу управления имя DropDownList DisplayModes. 5. Присвойте свойству AutoPostBack элемента управления DropDownListDisplay Modes значение true. 6. Добавьте следующий код в класс WebPartsDemo в файле WebPartsDemo. aspx. cs. Здесь все режимы отображения, поддерживаемые действительной конфигурацией WebPartManager, показаны в раскрывающемся списке: protected void Page_Init(object sender, EventArgs e) { foreach (WebPartDisplayMode mode in WebPartManager1.SupportedDisplayModes) { if (mode.IsEnabled(WebPartManagerl)) { DropDownListDisplayModes.Items.Add(new Listltem(mode.Name)); } } } 7. Добавьте метод OnChangeDisplayMode в событие SelectedlndexChanged раскрывающегося списка для DropDownListDisplayModes. 8. Добавьте реализацию метода OnChangeDisplayMode (), чтобы сменить режим отображения WebPartManager на режим, выбранный в раскрывающемся списке DropDownListDisplayModes: protected void OnChangeDisplayMode(object sender, EventArgs e) { string selectedMode = DropDownListDisplayModes.SelectedValue; WebPartDisplayMode mode = WebPartManagerl.SupportedDisplayModes[selectedMode]; if (mode != null) { WebPartManagerl.DisplayMode = mode; } } 9. Откройте Web-страницу в браузере. Поскольку теперь существует зона Editor, в раскрывающемся списке есть вариант Edit (Редактирование). Выберите в раскрывающемся списке Edit the Page (Редактировать страницу). Обратите внимание на меню Edit в хроме Web Parts при щелчке на кнопке Verb. 10. Выберите меню Edit. На экране появится зона Editor. Измените заголовок и тип хрома Web-части. Попытайтесь переместить Web-части в другие зоны и измените порядок зоны.
Глава 20. Расширенное программирование для Web 679 11. Вместо того чтобы использовать части Editor зоны Editor для замены Web-частей, вы можете перетащить Web-часть из одной зоны в другую, если в качестве режима будет выбран Edit. Зона Catalog Зона Catalog— это самый настоящий каталог, в котором вы можете выбрать требуемые Web-части. Эта зона похожа на другие зоны, осуществляющие управление Web-частями. Web-части, управляемые зоной Catalog, являются частями Catalog. ASP.NET различает три вида каталогов: каталог страницы, декларативный каталог и каталог импорта. На рис. 20.13 показана часть PageCatalogPart. В каталоге страницы показаны все Web4acTH, доступные на Web-странице. Все Web-части, которые были закрыты пользователем, перечислены в части PageCatalogPart. Таким образом, пользователь может заново открыть закрытые элементы управления. При использовании части DeclarativeCatalogPart, показанной на рис. 20.14, элементы управления, которые должны быть доступны для выбора, нужно определить внутри элемента <WebPartsTemplate>. С помощью визуального конструктора это можно сделать, щелкнув на смарт-теге элемента управления, и выбрав пункт Edit Templates (Редактировать шаблоны). <asp:CatalogZone ID="CatalogZonel" runat="server"> <ZoneTemplate> <asp:DeclarativeCatalogPart runat="server" ID="DeclarativeCatalogPartl"> <WebPartsTemplate> <ucl:ListEvents ID="ListEvents2" runat="server" /> <asp:Calendar ID="Calendarl" runat="server" /> </WebPartsTemplate> </asp:DeclarativeCatalogPart> </ZoneTemplate> </asp:CatalogZone> Часть ImportCatalogPart может применяться для импортирования Web- частей, которые хранились в клиентской системе. Как показано на рис. 20.15, ImportCatalogPart включает кнопки Browse (Обзор) и Upload (Выгрузить). С помощью кнопки Browse можно выбрать и загрузить локально хранящуюся Web-часть в персональные настройки пользователя на Web-сервере. Добавление зоны Catalog делает возможным выбор Web-частей, которые необходимо включить в Web-страницу из каталога. В следующем практическом занятии мы создадим зону Catalog, включающую множество каталогов. Catalog Zone Page Catalog Weather Add to: Weather Close " [Arfdj [ Clo»e 1 Catalog Zone Declarative Catalog Untitled Untitled Close Add to Events - [Add j| Close j Puc. 20.13. Часть PageCatalogPart Puc. 20.14. Часть DeclarativeCatalogPart
680 Часть III. Программирование для Web Catalog Zone Close I Imported Web Part Catalog Type a fie name ( WebPart) or click "Browse" to locate a Web Part I Blow—-. ] Once you have selected a Web Part fie to import. dick the Upload button 1Г5*»П I Add to: Events ~ [dam] Рис. 20.15. Часть ImportCatalogPart практическое занятие Добавление каталога Zone 1. Откройте ранее созданную Web-страницу WebPartDemo. aspx. 2. Добавьте элемент управления CatalogZone во второй столбец первой строки таблицы внутри страницы. 3. С помощью элемента управления CatalogZone выберите формат Professional Auto Format. 4. Добавьте часть PageCatalogPart и DeclarativeCatalogPart в зону Catalog. Перетащите эти элементы управления непосредственно поверх CatalogZone, поскольку эти элементы не могут существовать непосредственно на странице. 5. Для DeclarativeCatalogPart требуется некоторая настройка. В окне визуального конструктора щелкните на смарт-теге элемента управления DeclarativeCatalogPart и выберите пункт меню Edit Templates (Редактировать шаблоны). Теперь вы можете добавить в этот элемент управления несколько других элементов управления (пользовательские или серверные элементы управления). После того как добавление будет завершено, выберите пункт меню End Template Editing (Завершить редактирование шаблонов). 6. Откройте Web-страницу в браузере. Закройте один или несколько элементов управления Web-частей, щелкнув на меню Close (Закрыть). Элементы управления Web-частей исчезнут с экрана. Естественно, вы не можете закрыть элементы управления зоны, в которой Enabled свойства CloseVerb равно false. 7. Присвойте раскрывающемуся списку DropDownList значение Show the Catalog (Показать каталог). Поскольку у нас существует зона CatalogZone, меню Catalog добавляется в раскрывающийся список. Будет открыта зона Catalog. 8. Выберите опцию Page Catalog (Каталог страницы) в списке каталогов. В каталоге страницы все Web-части, которые были закрыты, появятся в списке. Выберите Web-часть из списка и добавьте ее в зону. 9. Выберите опцию Declarative Catalog (Декларативный каталог) в списке каталогов. При декларативном каталоге все настроенные Web-части появятся в списке. Выберите Web-части из списка, добавьте их в зону и закройте зону Catalog. Зона Connections Используя зону Connections, вы можете посылать данные из одного элемента управления Web-частей в другой такой элемент управления на той же странице.
Глава 20. Расширенное программирование для Web 681 Например, если пользователь выбирает город, чтобы узнать информацию о состоянии погоды из Web-части Weather, то выбранный город может быть отправлен в Web- часть News, которая отображает новости для этого же города. За организацию и управление соединениями между Web-частями отвечает элемент WebPartManager. Для организации связи между Web-частями необходимо определить, кто будет поставщиком, а кто — потребителем. Поставщик предлагает некоторую порцию данных; потребитель использует эти данные. На рис. 20.16 показано взаимодействие WebPartManager с поставщиком и потребителем. WebPartManager вызывает методы от поставщиков для предоставления интерфейсов, доступ к которым может осуществляться для получения данных. Поставщики идентифицируются посредством атрибута [ConnectionProvider]. Диспетчер WebPartManager передает эти интерфейсы потребителям. Потребители идентифицируются посредством атрибута [ConnectionConsumer]. Теперь потребитель может вызывать методы поставщика для получения данных. Получение интерфейсов поставщика > WebPartManager Поставщик Получение данных от поставщиков Передача информации поставщика ^потребителям Потребитель Рис. 20.16. Взаимодействие WebPartManager с поставщиком и потребителем В следующем практическом занятии мы организуем связь между двумя Web-частями, передавая выбранное название страны из Web-части ListEvents в другую Web-часть. Практическое занятие Обеспечение соединения между Web-частями 1. Откройте ранее созданный Web-сайт EventRegistrationWeb. 2. Откройте специальную папку AppCode в окне Solution Explorer. Если эта папка не существует, создайте ее, выбрав пункт меню Website1^Add Folder^App^ode Folder (Web-canT1^ Добавить папку^Папка AppCode). 3. Создайте новый файл С# по имени ICountry.cs, чтобы определить интерфейс ICountry: public interface ICountry { string GetCountryO ; 4. Откройте исходный код (файл ListEvents.ascx.cs) ранее созданного пользовательского элемента управления ListEvents. Посредством класса ListEvents реализуйте интерфейс ICountry. Реализация метода GetCountryO необходима для того, чтобы вернуть выбранное название страны вызывающему объекту:
682 Часть III. Программирование для Web public partial class ListEvents : System.Web.UI.UserControl, ICountry { public string GetCountryO { return DropDownListCountries.Selectedltem.Value; } //... 5. Для поставщика необходим также метод, помеченный атрибутом [Connection Provider]. Этот метод возвращает интерфейс. Реализуйте метод GetCountry Interface (), чтобы вернуть ссылку на интерфейс ICountry. Атрибут [Connection Provider] определяет для поставщика дружественное имя (Country) и действительное имя (CountryProvider). Добавьте следующий код в файл ListEvents. ascx. cs: [ConnectionProvider ("Country", "CountryProvider")j public ICountry GetCountrylnterface () { return this; } 6. Создайте новый пользовательский элемент управления Country, ascx, который будет действовать в качестве потребителя. Для этого элемента управления нужна всего лишь метка для отображения названия страны. В качестве идентификатора метки присвойте значение LabelCountry. 7. Откройте код С# пользовательского элемента управления Country, чтобы реализовать метод потребителя, помеченный атрибутом [ConnectionConsumer]. Метод SetCountry () получает интерфейс ICountry, который он использует для приема данных от поставщика с помощью метода GetCountry (). Возвращенная строка присваивается свойству Text метки: [ConnectionConsumer("Country", "CountryConsumer")] public void SetCountry(ICountry provider) { string country = provider.GetCountry (); if (!string.IsNullOrEmpty(country)) { LabelCountry.Text = country; } } 8. Откройте Web-страницу WebPartDemo.aspx. Добавьте новый пользовательский элемент управления Country .ascx в зону Web-частей. 9. Выберите WebPartManager и щелкните на кнопке Ellipsis (Овал) со свойством StaticConnections в окне редактора свойств (Property Editor). Добавьте новое соединение с параметрами, перечисленными в табл. 20.3. Таблица 20.3. Значения свойств соединения Свойства соединения Значение ConsumerConnectionPointID CountryConsumer ConsumerlD Countryl ID CountryConnection ProviderConnectionPointID CountryProvider ProviderlD ListEventsl
Глава 20. Расширенное программирование для Web 683 Конфигурирование соединений посредством редактора Property Editor добавляет элемент <StaticConnections> в элемент управления WebPartManager, как показано ниже: <asp:WebPartManager ID="WebPartManagerl" Runat="server"> <StatlcConnections> <asp:WebPartxConnection ID="CountryConnection" ProviderID="ListEventsl" ProviderConnectionPointID="CountryProvider" ConsumerID="Countryl" ConsumerConnectionPointID="ListEventsl" /> </StaticConnections> </asp:WebPartManager> 10. Запустите Web-страницу. После настройки соединения, если вы выберете название страны с помощью элемента управления ListEvents, оно отобразится в элементе управления Country. 11. Вместо того чтобы использовать статические соединения с возможностью изменять Web-части, которые отображаются во время выполнения, необходимо сконфигурировать соединения во время выполнения. Это можно сделать посредством элемента управления Connections Zone. Добавьте элемент управления Connect ions Zone на Web-страницу и удалите статические соединения из WebPartManager. 12. Запустите Web-страницу еще раз. Выберите пункт Connect (Соединить) в раскрывающемся списке. 13. При определении режима ConnectDisplayMode для WebPartManager (это делается в случае изменения раскрывающегося списка) зона Connections Zone не становится видимой. Вместо этого, со всеми элементами управления поставщика и потребителя, кнопка Verb предлагает новую команду Connect (Соединить). Щелкните на этом элементе в элементе управления Country; при этом будет открыта зона Connections Zone. Создайте динамическое соединение с помощью элемента управления ListEvents (рис. 20.17). О • •FtTTTf^ а •*■> в • * - •'»••' ConDccfeocts Zooe Manage the coonecnom for Landed Manage the со—ecbtxu for (he current Web pan Rrondm Cfosr Web parts (hat the current Web part gm rtornunon bom Get County From Event Info ». r^^^^^T.. '. ID 21 1 2 2 1 i 6 2 A 8 10 И 12 11 M 1С 1Л 17 10 Ю %Unatt<—l|W 1-J Рис. 20.17. Динамическое соединение с помощью элемента управления Lis t Events
684 Часть III. Программирование для Web 14. Как и прежде, с помощью элемента управления ListEvents выберите название страны, позволяя выбранному названию страны появиться в элементе управления ListEvents. JavaScript Чтобы обеспечить взаимодействие с пользователем на стороне клиента, вы можете добавлять на Web-страницы код JavaScript. Для использования JavaScript не нужно устанавливать среду .NET на клиентской системе, а любой код JavaScript работает в большинстве Web-браузеров. Идея разработки JavaScript принадлежит компании Netscape. Язык JavaScript не имеет ничего общего с Java. Название JavaScript было выбрано по причине кооперации между компаниями Sun и Netscape. Сейчас можно видеть разные варианты JavaScript, поскольку другим производителям не разрешено использовать Java в имени продукта: ECMAScript является официальной версией языка написания сценариев, стандартизированного организацией Ecma International (www.ecma-international.org). Реализация этого стандарта от Microsoft известна под названием JScript. В версии Visual Studio 2008 обеспечивается поддержка JavaScript, которая включает средство IntelliSense и функции отладки. Синтаксис JavaScript имеет некоторые сходства с синтаксисом языка С#, однако "за кулисами" они разные. Для создания промежуточного языка (Intermediate Language — IL) не предусмотрено компилятора; вместо этого JavaScript интерпретируется во время выполнения. Прежде чем работать с упражнениями, давайте рассмотрим синтаксис языка JavaScript. Элемент Script В HTML-коде вы можете добавить код JavaScript внутри элемента <script>: <script language="javascript" type="text/javascript"> <! — function foo() { } —> </script> Вы можете также поместить функции JavaScript в отдельный файл . j s и ссылаться на этот отдельный файл из блока сценариев в атрибуте src: <script language="javascript" type="text/]avascript" src="SampleScripts.js" /> Разделение функций JavaScript помогает повторно использовать и кэшировать сценарии в клиентском Web-браузере. Если сценарии необходимо выполнять на разных страницах, то функции JavaScript загружаются только один раз. Объявление переменных Переменные объявляются с применением ключевого слова var. В версии С# 3.0 имеется ключевое слово var, однако принцип использования переменных в JavaScript существенно отличается. В языке JavaScript переменная может получать любой тип. В примере, показанном ниже, первая строка объявляет переменную х как число. В следующей строке на эту переменную имеется ссылка как на строку. И, наконец, переменная х объявляется в качестве объекта со свойствами FirstName и LastName. Свойства создаются динамически, по мере их присваивания:
Глава 20. Расширенное программирование для Web 685 var x = 3; х = "text"; х = new Object (); x.FirstName = "Tom"; x.LastName = "Turbo"; Вы можете также объявить и инициализировать объект из одной строки: var р = { "FirstName" : "Tom", "LastName" : "Turbo" }; Доступ к значениям свойств может быть осуществлен с помощью синтаксиса свойства, известного из языка С#, но с квадратными скобками: var f = p.FirstName; var 1 = p["LastName"] ; Определение функций Функции JavaScript определяются посредством ключевого слова function. Аргументы определяются по именам: function foo(argl, arg2) { } Вы можете также определить переменную для ссылки на функцию через объект Function. С помощью конструктора объекта Function последний параметр определяет реализацию функции; параметры, стоящие до него, перечисляют аргументы функции: var add = new Function("x", "y", "return x + у"); Операторы Операторы итераций JavaScript очень похожи на аналогичные операторы в С#. В JavaScript можно использовать ключевые слова for, while и do. . .while. Вместо оператора foreach, который есть в С#, можно применять конструкцию for. . . in. С помощью конструкции for. . .in можно осуществлять итерацию не только в массивах, но и во всех свойствах объекта, как показано здесь на примере переменной р, которая имеет свойства FirstName, LastName и Country. Оператор for. . . in выполняет итерацию по всем свойствам и заполняет строку именами и значениями свойств: var p = {"FirstName" : "Christian", "LastName" : "Nagel", "Country" : "Austria"}; var s = ""; for (key in p) { s += key; s += ": "; s += p[key]; s += "\t"; } Условно выполняя некоторое действие, вы можете применять операторы if. . . else и switch. Объекты JavaScript предлагает некоторые заранее определенные объекты с множеством методов. В качестве примера можно привести объект String для работы со
686 Часть III. Программирование для Web строками. При работе со строками JavaScript определяет функции toUpperCase (), toLowerCase () и indexOf () для возврата первого случая нахождения подстроки, lastlndexOf () для возврата последнего случая нахождения подстроки, и slice () для возврата подстроки, определяя начальное и конечное положение. Объект String предлагает также методы, окружающие строку HTML-кодом. Например, функция bold () добавляет элементы <В> перед строкой и </В> после нее. Другими функциями являются anchor () для создания гиперссылки и italics () для создания строки, выделенной курсивом. Объект Math позволяет выполнять некоторые вычисления. К числу методов этого объекта относятся abs (), sin (), cos (), tan (), random () и round (). Объект Date представляет дату и время. Для доступа к разным частям времени и даты используются некоторые методы, такие как getYear (), getMonth (), getDay (), getHours() и getMinutes(). Давайте рассмотрим некоторые примеры, демонстрирующие варианты использования JavaScript в Web-страницах. Практическое занятие ИсПОЛЬЗОВЭНИе CustomValidator Элемент управления проверки, рассмотренный в главе 19, включает проверку на стороне клиента и сервера. С помощью элемента управления CustomValidator можно определить специальный метод JavaScript для проверки данных, вводимых пользователем. 1. Откройте ранее созданный Web-сайт EventRegistrationWeb. 2. Добавьте новую Web-страницу по имени ValidationDemo. aspx. 3. Добавьте элементы управления TextBox, Button и CustomValidator. Для элемента управления CustomValidator присвойте свойству ControlToValidate значение TextBoxl. 4. Для элемента управления CustomValidator присвойте событие ServerValidate методу OnServerOddNumber и реализуйте метод на языке С#: protected void OnServerOddNumber(object source, ServerValidateEventArgs args) { if (int.Parse(args.Value) % 2 != 0) { args.IsValid = true; } else { args.IsValid = false; } } 5. Добавьте JavaScript-метод oddNumberValidation () для выполнения той же проверки, которая делалась на предыдущем этапе, но на этот раз при помощи языка JavaScript: < script language="]avascript" type="text/javascript" > function oddNumberValidation(source, arguments) { if (arguments.Value % 2 != 0) { arguments.IsValid = true;
Глава 20. Расширенное программирование для Web 687 else { arguments.IsValid = false; } } < /script > 6. Для элемента управления CustomValidator присвойте свойству Client ValidationFunction значение oddNumberValidation. Затем запустите страницу и произведите отладку методов проверки на стороне клиента и на стороне сервера. Приспнеспе занятие Создание объектов 1. Откройте ранее созданный Web-сайт EventRegistrationWeb. 2. Добавьте новую Web-страницу под именем ObjectDemo. aspx. 3. Из категории HTML панели инструментов добавьте элемент Div и Input (Button) на страницу. Элемент Div по умолчанию должен иметь идентификатор divl. 4. Добавьте следующий блок сценария в исходный код HTML. Функция Person (firstName, lastName) ведет себя подобно конструктору, поскольку ключевое слово this используется внутри функции для создания свойств firstName и lastName. Прототип синтаксиса позволяет объявлять версию JavaScript класса. Person, prototype назначает членов класса Person. Конструктор Person .prototype . constructor назначает функцию Person в качестве конструктора; Person .prototype . toString назначает функцию toString в качестве члена. Функция GetPerson () создает новый объект Person посредством вызова конструктора с двумя аргументами и вызывает метод toString. Метод document. getElementByld () находит дескриптор div для записи результатов: <script type="text/]avascript"> function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } function toString () { return this.firstName + " " + this.lastName; } Person.prototype.constructor = Person; Person.prototype.toString = toString; function getPerson() { var p = new Person("Natalie", "Portman"); var label = document.getElementByld("divl"); label.innerHTML = p.toString(); } </script> 5. Добавьте свойство onClick в кнопку, чтобы функция getPerson () вызывалась в случае возникновения события click: <input id="Buttonl" type="button" value="button" onclick="getPerson ();" /> 6. Теперь вы можете запустить и протестировать Web-страницу.
688 Часть III. Программирование для Web Практическое занятие Открытие ОКНЭ Для открытия окна на стороне клиента необходимо использовать код JavaScript; открывать новые окна только на основе базового кода С# не получится. 1. Откройте ранее созданный Web-сайт EventRegistrationWeb. 2. Добавьте новую Web-страницу OpenWindow.aspx. Добавьте кнопку HTML на эту страницу. 3. Добавьте JavaScript-функцию openWindow (), чтобы открыть ранее созданную страницу: <script type="text/javascript"> function openWindow() { window.open("ObjectDemo.aspx"); } </script> 4. Добавьте свойство onClick для кнопки, чтобы вызвать функцию openWindow (): <input id="Buttonl" type="button" value=flbutton" onclick="openWindow () ; " /> 5. Теперь можно протестировать страницу и открыть новое окно непосредственно на стороне клиента. Настройки, принятые по умолчанию в большинстве Web-браузеров, не разрешают выполнять код сценариев для открытия новых окон без участия со стороны пользователя. (Многие приложения являются надоедливыми, открывая окно за окном после того, как пользователь закрывает какое-нибудь окно.) Как правило, открыть новое окно с помощью кода JavaScript при участии пользователя можно, но при этом все равно существует вероятность того, что такие окна будут блокироваться утилитами, перехватывающими всплывающие окна. Методу open окна можно передавать ряд параметров, как будет показано в следующем фрагменте кода. Первым параметром является URL-адрес документа, который необходимо открыть. Второй параметр определяет имя окна. Благодаря значению blank будет открыто новое окно, однако можно также указать и существующее целевое окно. Третьим параметром является список характеристик, разделенных запятыми, в котором можно определить размер окна и указать, будут ли видимыми какие-то опции (например, меню, заголовок и строка состояния). window.open("ObjectDemo.aspx", "_blank", "menubar=no, resizable=no, status=no, titlebar=no, width=300, height=300"); практическое занятие Итерация по элементам управления С помощью объекта window из предыдущего упражнения вы уже использовали объект, который не является частью JavaScript, а относится к DHTML. Благодаря DHTML вы можете иметь доступ ко всем элементам HTML на странице. Давайте на основе примера попытаемся получить информацию об элементах управления HTML на странице. 1. Откройте ранее созданный Web-сайт EventRegistrationWeb. 2. Добавьте новую Web-страницу ScriptingDemo . aspx. Добавьте на страницу таблицу с пятью строками и двумя столбцами. В первые четыре строки левого
Глава 20. Расширенное программирование для Web 689 столбца поместите элементы управления Label. В правый столбец добавьте три элемента управления TextBox и один DropDownList. Добавьте элемент управления HTML Input в последнюю строку правого столбца. 3. Ниже таблицы поместите элемент управления HTML TextArea и измените его размеры таким образом, чтобы он занял большую часть страницы и мог отобразить всю информацию об элементах. 4. Установите свойство onClick кнопки HTML, чтобы обеспечить вызов метода walktree (): <input id="bl" type="button" value="Click" onclick="walktree@, forml.childNodes);" /> 5. Реализуйте метод walktree () в блоке сценариев. Этот метод рекурсивно вызывает сам себя при итерации по дочерним элементам. Результат этой операции записывается в элемент TextArea 1. Вы можете получить доступ к каждому элементу метода getElementByld () объекта document. Когда метод вызывается впервые, все дочерние элементы Form передаются в переменную tree. Для каждого элемента в дереве свойство tagName записывается в TextArea. Если элемент имеет дочерние элементы, walktree вызывается снова, а дочерние узлы childNodes элемента передаются в переменную tree. <script language=ll3avascript" type="text/javascript"> function walktree(level, tree) { var textArea = document.getElementByld("TextAreal"); var numltems = tree.length; for (var i=0; i < numltems; i++) { var item = tree, item (i) ; textArea .value += level + " " + item. tagName + "\n"; if (item.hasChildNodes) { walktree(level + 1, item.childNodes); </script> 6. Запустите страницу, чтобы получить доступ ко всем HTML-элементам с помощью кода JavaScript. Резюме В этой главе рассматривались многие средства ASP.NET, включая мастер-страницы, профили и Web-части. Мастер-страницы позволяют определять общую компоновку для всех страниц. Профили используются для постоянного хранения пользовательских данных в базе данных. В отличие от переменных сеанса, если информация о состоянии теряется по истечении срока действия сеанса, профили по-прежнему сохраняются в базе данных. В связи с тем, что профили являются строго типизированными, можно без труда получить доступ к значению, используя свойства и средство IntelliSense в Visual Studio. Web-части являются мощной новой возможностью ASP.NET. Они позволяют создавать Web-приложения типа порталов, которые поддаются настройке индивидуальными пользователями всего лишь несколькими щелчками кнопкой мыши. Рассматривая
690 Часть III. Программирование для Web Web-части, вы ознакомились с разными типами зон, включая WebPartZone, Editor, Catalog и Connections. В этой главе были рассмотрены также и пользовательские элементы управления, поскольку они облегчают повторное использование компонентов на Web-страницах. Пользовательские элементы управления могут применяться внутри зон Web. Также в этой главе был рассмотрен язык JavaScript, поскольку он позволяет поддерживать взаимосвязь с пользователем на стороне клиента. На основе JavaScript была создана технология AJAX, речь о которой пойдет в главе 22. В этой главе рассмотрены следующие вопросы. □ Создание и использование мастер-страниц. □ Создание структуры сайта для осуществления навигации по разным страницам. □ Создание пользовательских элементов управления. □ Хранение пользовательских данных для настройки Web-приложения с помощью Web-частей. □ Использование JavaScript при работе с Web-страницами. В качестве упражнений вы создадите Web-приложение, использующее средства ASP.NET, продемонстрированные в этой и предыдущей главах. Следующая глава будет посвящена написанию Web-служб с помощью ASP.NET. Упражнения 1. Создайте новое Web-приложение типа портала. 2. Создайте пользовательский элемент управления для отображения резюме. 3. Создайте пользовательский элемент управления для отображения списка ссылок на избранные Web-сайты. 4. Определите в качестве механизма аутентификации для Web-сайта аутентификацию с помощью форм. 5. Создайте пользовательский элемент управления, в котором пользователь может регистрировать Web-сайт. 6. Создайте мастер-страницу, определяющую общую компоновку с верхним и нижним разделами. 7. Определите карту сайта для создания ссылок на все Web-страницы Web-сайта. 8. Создайте Web-страницу с Web-частями, чтобы пользователи могли определять компоновку страницы.
21 Web-службы Несомненно, вы уже встречались с термином Web-службы, хотя, возможно, и не имели представления о том, что они собой представляют, и каким образом вписываются в то, как действует Web сейчас, и как будет действовать в будущем. Достаточно сказать, что Web-службы обеспечивают фундамент для нового поколения Web-приложений. Каким бы ни было клиентское приложение — Windows-приложением или приложением ASP.NET Web Forms — и в какой бы операционной системе оно ни действовало — Windows, Pocket Windows или какой-то другой ОС — оно будет регулярно обмениваться данными через Internet посредством Web-службы. Web-службы — это программы серверной стороны, которые прослушивают сообщения, поступающие от клиентских приложений, и возвращают конкретную информацию. Эта информация может поступать от самой Web-службы, от других компонентов в данном домене или от других Web-служб. В то время как концепция Web-служб постоянно развивается, существует несколько различных типов Web-служб, которые выполняют различные функции: некоторые из них предоставляют информацию, специфичную для конкретной отрасли, такой как производство или здравоохранение. Порталы используют службы различных поставщиков для предоставления информации по конкретной теме. Существуют службы, характерные для отдельных приложений, и унифицированные службы, которые могут использоваться множеством различных приложений. Web-службы позволяют объединять, использовать совместно, обмениваться или подключать отдельные службы различных поставщиков и разработчиков для образования совершенно новых служб или нестандартных приложений, созданных "на лету" для удовлетворения требований клиента. В частности, в этой главе будут рассматриваться следующие темы. □ Предшественники Web-служб. □ Что собой представляет Web-служба. □ Протоколы, используемые для Web-служб. □ Создание Web-службы ASP.NET.
692 Часть III. Программирование для Web □ Тестирование Web-службы. □ Построение клиента, использующего Web-службы. □ Асинхронный вызов Web-службы. □ Отправка и прием сообщений. В этой главе мы не будем углубляться в детали внутренней работы Web-служб, но вы получите общее представление о том, для чего используются эти протоколы. После прочтения этой главы можно приступить к созданию и применению простых Web- служб с помощью Visual Studio. Предшественники Web-служб Соединение компьютеров для передачи данных стало важной концепцией уже в 1969 г., когда телефонными линиями связи были соединены четыре компьютера, образуя ARPANET (Advanced Research Projects Agency Network — сеть управления перспективных исследовательских программ). В 1976 г. был изобретен протокол TCP/IP. Для упрощения использования этого протокола Университет Беркли создал модель сокетов для программирования сетевых задач. При выполнении программирования с помощью интерфейса Sockets API клиент должен был инициировать подключение к серверу, после чего он мог отправлять и принимать данные. Для вызова некоторых операций на сервере с целью получения результатов требуются дополнительные протоколы для описания кодов запроса и ответа. Примерами таких протоколов приложений являются FTP (File Transfer Protocol — протокол передачи файлов), Telnet и HTTP (Hypertext Transfer Protocol — протокол передачи гипертекста). Протокол FTP используется для получения файлов с сервера и для помещения файлов на сервер. Он поддерживает коды запросов, такие как GET и put, которые пересылаются по линии связи от клиента к серверу. Сервер анализирует принимаемый поток данных и в соответствии с кодами запроса вызывает соответствующий метод. Работа протокола HTTP очень похожа на работу протокола FTP. Вызов удаленных процедур (RPC) При использовании интерфейса Sockets API и протокола TCP/IP, вызывающего специализированные методы на сервере, программист должен был создавать средства, с помощью которых сервер анализировал потоки данных для вызова соответствующего метода. Чтобы облегчить решение этой задачи, ряд компаний создал протокол RPC (remote procedure calls — вызов удаленных процедур), примером которого служит все еще популярный протокол DCE — RPC (Distributed Computing Environment — среда распределенных вычислений), разработанный фондом OSF (Open Software Foundation— Фонд открытого программного обеспечения). Впоследствии эта организация была переименована в Open Group (www.opengroup.org). Используя RPC, можно определять в формате IDL (Interface Definition Language — язык определения интерфейса) методы, которые сервер должен реализовать, и которые клиент может вызывать. Теперь для вызова методов больше не нужно определять нестандартный протокол и анализировать коды запросов. Эта задача выполняется специальной программой, называемой заглушкой (stub), сгенерированной компилятором интерфейса. RPC предназначен для вызова методов — т.е. программист должен заниматься процедурным программированием. Технология RPC появилась сравнительно поздно, когда большинство разработчиков уже начали использовать концепцию ООП. Для заполнения образовавшейся бреши был разработан ряд технологий, в том числе CORBA и DCOM.
Глава 21. Web-службы 693 CORBA В 1991 г. группа OMG (Object Management Group — группа по управлению объектами; www.omg.org) инициировала создание модели CORBA (Common Object Request Broker Architecture — общая архитектура брокера объектных запросов), чтобы перенести концепцию ООП в решение сетевых задач. Многие поставщики, такие как Digital Equipment, HP, IBM и другие, реализовали серверы CORBA. Однако поскольку OMG не определила эталонную реализацию, а только спецификацию модели, серверы этих поставщиков в действительности не могли взаимодействовать друг с другом. Сервер HP нуждался в клиенте HP, сервер IBM — в клиенте IBM и т.д. DCOM С выходом на рынок ОС Windows NT 4 компания Microsoft дополнила протокол DCE — RPC объектно-ориентированными функциональными возможностями. Протокол DCOM (Distributed COM — распределенная модель СОМ) позволяет вызывать компоненты СОМ через сеть и используется в приложениях СОМ+. По прошествии нескольких лет, в течение которых операционные системы Microsoft были обязательным условием возможности использования модели DCOM, компания Microsoft открыла протокол для применения в других средах, дополнив его компонентами Active Group. DCOM стал доступным для систем UNIX, VMS и IBM. Протокол DCOM интенсивно использовался в средах Microsoft, но инициатива по его переносу в другие системы оказалась не совсем успешной. Что же требуется администратору мэйнфрейма IBM для добавления технологии Microsoft в управляемую им систему? RMI Компания Sun Microsystems пошла другим путем, используя свои технологии Java. В чистом мире Java протокол RMI (Remote Method Invocation — вызов удаленного метода) можно использовать для удаленного вызова объектов. Компания Sun добавила несколько мостов для мира CORBA и СОМ, но основной ее целью было склонить основных пользователей к применению решения на базе только языка Java. SOAP Все рассмотренные технологии использовались для обмена данными между приложениями, но при наличии решения, включающего в себя компоненты CORBA, DCOM и RMI, эти разные технологии трудно заставить взаимодействовать друг с другом. Еще одна проблема, связанная с этими технологиями, таится в их архитектурах: поскольку они не предназначены для обслуживания тысяч клиентов, они не могут обеспечить масштабируемость, требуемую для решений для Internet. Поэтому в 1999 г. несколько компаний, включая Microsoft и UserLand Software (www.userland.com), создали протокол SOAP в качестве нового способа вызова объектов по Internet — способа, который работает на основе уже принятых стандартных протоколов. Для описания методов и параметров, необходимых для выполнения удаленных вызовов по сети, SOAP использует формат на основе языка XML. В системе СОМ сервер SOAP мог бы преобразовывать SOAP-сообщения в СОМ-вызовы, а в системе CORBA он преобразует их в CORBA-вызовы. Первоначально в определении SOAP протокол HTTP применялся для выполнения SOAP-вызовов через Internet. С появлением версии SOAP 1.2 Web-службы стали независимыми от протокола HTTP Можно использовать любой транспортный протокол, но до настоящего времени HTTP остается наиболее часто применяемым транспортным протоколом для Web-служб.
694 Часть III. Программирование для Web Эта глава посвящена Web-службам, которые могут быть созданы с помощью шаблона проекта Visual Studio 2008 и используют протоколы SOAP и HTTP. Спецификации SOAP можно найти на Web-сайте www. w3. org/TR/SOAP. Применение Web-служб Чтобы взглянуть на Web-службы под еще одним углом, можно сделать различие между обменом данными между пользователем и приложением и между приложениями. Начнем с рассмотрения примера обмена данными между пользователем и приложением: получение информации о погоде из Web. Несколько Web-сайтов, таких как weather. msn. com и www. weather. com, представляют читателю информацию о погоде в легко систематизируемом формате. Обычно пользователь читает эти страницы непосредственно. Если же нужно было создать мощное клиентское приложение для отображения информации о погоде (выполняющее обмен данными между приложениями), приложение должно было бы подключаться к Web-сайту посредством строки URL-адреса, содержащей название интересующего города. Результирующее HTML-сообщение, возвращенное Web-сайтом, нужно было бы проанализировать для получения информации о температуре и погодных условиях, после чего информацию, наконец, можно было бы отобразить в соответствующем формате клиентского приложения. Эта задача чрезвычайно трудоемка, учитывая, что нам требуется всего лишь получения ряд температурных показаний для конкретного города, а процесс извлечения данных из HTML-сообщения не прост, поскольку эти данные предназначены для отображения в Web-браузере и не предусматривают их использования каким-либо другим клиентским бизнес-приложением. Поэтому данные внедрены в текст и не доступны для простого извлечения. В результате для получения других данных (вроде сведений об осадках) с этой же Web-страницы клиентское приложение пришлось бы переписывать или адаптировать. В отличие от Web-браузера, приложение позволяет пользователям немедленно выбирать нужные данные и пропускать ненужные. Для решения проблемы обработки HTML-данных Web-служба предоставляет полезные средства возврата только запрошенных данных. Для этого достаточно вызвать метод на удаленном сервере и получить необходимую информацию, которая может непосредственно обрабатываться клиентским приложением. К счастью, в этом случае приходится иметь дело с предварительно сформатированным текстом, предназначенным для интерфейса пользователя, поскольку Web-служба представляет информацию в формате XML, а для обработки XML-данных уже существует набор инструментальных средств. Единственное, что требуется от клиентского приложения — вызов ряда методов XML-классов каркаса .NET Framework для получения информации. Более того, при создании клиентского приложения для Web-службы .NET на языке С# для этого даже не требуется написание кода — существуют средства, которые сгенерируют код С# автоматически. Подобное приложение отображения информации о погоде — один из примеров множества возможных применений Web-служб. Сценарий использования приложения бюро путешествий Как вы планируете проведение своего отпуска? Вместо того чтобы предоставить все планирование бюро путешествий, свои выходные можно распланировать через Internet. На Web-сайте авиакомпании можно просмотреть имеющиеся рейсы и заказать нужные билеты. Поисковый механизм Web можно использовать для поиска оте-
Глава 21. Web-службы 695 ля в нужном городе. Если повезет, вы найдете карту с указанием маршрута до отеля. Найдя домашнюю страницу отеля, нужно перейти на страницу формы заказа и забронировать номер. Затем можно поискать фирму по прокату автомобилей и т.д. На сегодня поиск подходящих Web-сайтов с помощью поисковых механизмов и последующий переход к этим сайтам сопряжен с огромным объемом работы. Вместо того чтобы проделывать все это, можно было бы создать приложение "Домашнее бюро путешествий", использующее Web-службы со сведениями о гостиницах, авиалиниях, фирмах проката автомобилей и т.п. Затем клиенту можно было бы предоставить простой интерфейс, учитывающий все аспекты отпуска, включая заблаговременный заказ билетов на конкретный музыкальный концерт. С помощью своего карманного ПК во время отпуска эти же Web-службы можно применять для получения карты расположения определенных мест отдыха и точной информации о культурных событиях, киносеансах и т.п. Сценарий использования приложения распространения книг Web-службы могут также быть полезными для двух компаний, поддерживающих партнерские отношения. Предположим, что распространитель книг хочет развернуть книжные магазины с информацией об имеющихся в наличии книгах. Эту задачу можно выполнить с помощью Web-службы. Можно создать приложение ASP.NET, использующее Web-службу, которое будет предоставлять эту услугу непосредственно пользователям. Второе клиентское приложение этой службы — Windows-приложение книжного магазина, которое вначале проверяет наличие указанных книг на локальном складе, а затем на складе дистрибьютора. Продавец может немедленно отвечать на вопросы о сроках доставки, не обращаясь к другим источникам складской информации в других приложениях. Типы клиентских приложений Клиентом Web-службы может быть мощное Windows-приложение, созданное с помощью Windows Forms, или приложение ASP.NET, использующее Web Forms. Использование Web-службы может осуществляться посредством ПК под управлением Windows, системы UNIX или карманного ПК. Каркас .NET Framework позволяет применять Web-службы в любом типе приложений — Windows, Web или консольных. Интеллектуальный клиент — термин, обозначающий мощное клиентское приложение, которое использует Web-службы. Интеллектуальный клиент может действовать, не будучи подключенным к Web-службе, поскольку он кэширует данные локально. Microsoft Outlook служит примером интеллектуального клиента, поскольку все почтовые данные хранятся на клиентской системе, а при подключении к серверу Exchange они синхронизируются. Архитектура приложения Как же в действительности выглядит приложение, использующее Web-службы? Независимо от того, идет речь о разработке приложений ASP .NET или Windows либо приложений для небольших устройств (описанных в представленных в этом разделе сценариях), Web-службы являются важной технологией. Сценарий возможного использования Web-служб показан на рис. 21.1. Устройства и браузеры подключаются к приложению ASP.NET, разработанному с помощью Web Forms, через Internet. Это приложение ASP.NET использует локальные и удаленные, доступные по сети, Web-службы: Web-службы портала, Web-службы, специфичные для приложения и унифицированные элементарные Web-службы.
696 Часть III. Программирование для Web Следующий перечень должен помочь в понимании смысла этих типов служб. □ Web-службы портала. Предоставляют службы различных компаний по одной тематике. Обеспечивают простую в использовании точку доступа к нескольким службам. □ Web-службы, специфичные для приложения. Созданы для использования отдельным приложением. □ Унифицированные элементарные Web-службы. Службы, которые легко могут использоваться внутри нескольких приложений. Представленные на рис. 21.1 Windows-приложения могут использовать Web-службы непосредственно, не обращаясь к приложению ASP.NET. Устройства Браузеры I — г Приложение ASP.NET \ i Локальные Web-службы ^Г^ Windows- приложения раница Interne V V 1 ^ J / г t — > J / J А. X \ * Службы портала , Web-службы, специфичные для приложения Унифицированные элементарные Web-службы Рис. 21.1. Сценарий возможного использования Web-служб Архитектура Web-служб Web-службы используют протокол SOAP, являющийся стандартом, который определен многими компаниями. Огромное преимущество Web-служб — их независимость от платформы. Однако Web-службы — полезная технология не только при необходимости обеспечения взаимодействия нескольких платформ. Они могут быть также очень полезны при разработке приложений .NET как для клиентской, так и для серверной стороны. Преимущество этой технологии в том, что клиент и сервер могут выступать на сцене независимо. Описание службы создается документом WSDL (Web Service Description Language — язык описания Web-служб), который может быть разработан независимо от новых версий Web-службы и, следовательно, клиент не будет нуждаться в изменении. Подробнее рассмотрим необходимые шаги. Методы, которые можно вызывать Документ WSDL содержит информацию о методах, поддерживаемых Web-службой, и способах их вызова, типах параметров, переданных службе и возвращенных службой. Документ WSDL, генерируемый исполняющей средой ASP.NET, приведен на рис. 21.2. Добавление строки ?wsdl в файл . asmx ведет к возврату документа WSDL.
Глава 21. Web-службы 697 6 http://tocalhost:4»ei VS*mc*1.asmx?WSDL ■ Windows Internet Explorer ir^ f^J . e httptf/toc*lhoA4WiVS«nrtc«l^HW?WSOl ** *** * WlivtSeortb P о ii 4r i0ntpj|ocalhost498l3rS«vic«Vumx?WSOL ft *Ъ Q • Ф ' •••Net- ^тв** <wgai!Vp^b> <s:schema elementFormDefault="quelined" targe tNamespace=*http://www.wrox.com/webservices • <s: element name-*ReverseStrlng" • <s: complex Type > cs: sequence> <^ .-element minOccurs=" maxOccurs='l* name ^'message' tvpe^'sisWng" f> </s: sequence > </s:complexType> </s:element> <s:element name =*ReverseStringResponse* > <s:complexType> <s: sequence> <s:element minOccurs=* maxOccurs="l" name=*ReverseStringResult" type-"s:strlng* /> </s: sequence > </s:complexType> </s: element > </s: schema > </wsdl: types > <wsdl: message name 'ReverseStringSoapIn* ■■» <wsdl:part name ='parameters* element =*tns:ReverseStrfng' /> </wsdl: message > <wsdl: message name "ReverseStringSoapOut" > <wsdlpart name-'parameters* element-*tns:ReverseStringResponse* /> </wsdl: message > <wsdl: port Type name =*Servtce 1 Soap" • <wsdl:operation njme "ReverseString" <wsdl:mput message ="tns:ReverseStringSoapIn* /> ** Local intranet | Protected Mode: On Рис. 21.2. Документ WSDL Обработку этой информации не обязательно выполнять непосредственно. Атрибут WebMethod (мы рассмотрим его позднее) будет приводить к динамической генерации документа WSDL. Добавление Web-ссылки в клиентское приложение средствами Visual Studio вызывает запрос документа WSDL. Этот документ WSDL, в свою очередь, применяется для создания прокси-клиента с теми же методами и аргументами. Использование этого прокси-клиента обеспечивает приложению то преимущество, что ему приходится только вызывать методы, как они реализованы на сервере, поскольку для выполнения вызова по сети прокси-клиент преобразует их в вызовы SOAP. Спецификация WSDL поддерживается консорциумом World Wide Web Consortium (W3C). С ней можно ознакомиться на Web-сайте W3C (www.w3 .org/TR/wsdl). Вызов метода Чтобы метод можно было вызвать применительно к Web-службе, вызов должен быть преобразован в сообщение SOAP, как это определено в документе WSDL. Сообщение SOAP — основной элемент обмена данными между клиентом и сервером. Части сообщения SOAP показаны на рис. 21.3. Сообщение SOAP включает в себя конверт SOAP, который, как легко догадаться, заключает всю информацию SOAP в единый блок. Сам конверт SOAP состоит из двух частей: заголовка SOAP и тела SOAP. Необязательный заголовок определяет то, как клиент и сервер должны обрабатывать тело сообщения. Обязательное тело SOAP содержит пересылаемые данные. Обычно информацией внутри тела является вызываемый метод и упорядоченные значения параметров. Сервер SOAP отсылает обратно возвращаемые значения в теле SOAP сообщения SOAP.
698 Часть III. Программирование для Web Сообщение SOAP Конверт SOAP Заголовок SOAP Тело SOAP Рис. 21.3. Части сообщения SOAP Следующий пример демонстрирует, как выглядит сообщение SOAP, отправленное клиентом серверу. Клиент вызывает метод ReverseString () Web-службы. Строка Hello World! передается этому методу в качестве аргумента. Как видите, вызов метода расположен внутри тела SOAP, внутри XML-элемента <soap: Body>. Само тело содержится внутри конверта <soap:Envelope>. Перед сообщением SOAP расположен заголовок HTTP, поскольку сообщение SOAP отправляется с помощью POST-запроса HTTP. Создание такого сообщения не обязательно, поскольку оно создается прокси-клиентом: POST /Servicel.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: 508 SOAPAction: "http://www.wrox.com/webservices/ReverseString" <?xml version="l.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <ReverseString xmlns="http://www.wrox.com/webservices"> <message>Hello World!</message> </ReverseString> </soap:Body> </soap:Envelope> Как видно из XML-элемента ReverseStringResult, сервер отвечает SOAP-сообще- нием IdlroW olleH: HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: 446 <?xml version="l.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <ReverseStringResponse xmlns="http://www.wrox.com/webservices"> <ReverseStringResult>!dlroW olleH</ReverseStringResult> </ReverseStringResponse> </soap:Body> </soap:Envelope>
Глава 21. Web-службы 699 Поддержку спецификации SOAP осуществляет рабочая группа протокола XML консорциума W3C (www.w3.org/TR/soap). SOAP и брандмауэры Системные администраторы часто задают вопрос о том, нарушает ли протокол SOAP границы безопасности брандмауэров — иными словами, не противоречит ли SOAP концепции брандмауэров? Фактически в данном случае приходится учитывать не больше аспектов безопасности, чем при использовании обычных Web-серверов. При обычных брандмауэрах Web-серверов для предоставления серверу возможности обмена данными с внешним миром системный администратор открывает НТТР- порт 80. Пользователи в Internet могут иметь прямой доступ к этим серверам, даже находясь позади брандмауэра. Пользователь может запросить HTML-файл с помощью HTTP-запроса, а сервер возвращает либо статическую страницу, либо страницу, созданную "на лету" посредством сценариев ASP или CGI. Web-службы — это всего лишь еще один тип серверного приложения, которое осуществляет обмен данными посредством HTTP, хотя при этом вместо приема простых HTTP-запросов GET или post оно принимает HTTP-запрос POST, содержащий внедренное сообщение SOAP, а вместо возврата HTML-ответа оно возвращает HTTP-ответ, содержащий ответное сообщение SOAP. До тех пор пока брандмауэр действует, обмен данными выполняется по HTTP и, следовательно, он разрешен через порт 80. Однако если эта Web-служба ведет себя не так, как должно, например, допуская утечку конфиденциальных данных или взламывая сервер, это создает проблему. Тем не менее, подобные проблемы присущи всем серверным приложениям, будь то традиционные Web-страницы, серверные бизнес-объекты или Web-службы. Если проблемы безопасности, порождаемые Web-службами, продолжают беспокоить системного администратора брандмауэра, он может воспользоваться фильтром приложений, чтобы запретить выполнение запросов SOAP в сочетании с HTTP-запросом. Базовый профиль WS-I Спецификация SOAP появилась не сразу. Именно поэтому иногда трудно добиться взаимодействия между Web-службами различных поставщиков. Для решения этой проблемы была создана Организация по обеспечению взаимодействия Web-служб (Web Services Interoperability Organization; http: //ws-i. org). Эта организация в своей спецификации WS-I Basic Profile (Базовый профиль взаимодействия Web-служб) определила требования к Web-службам. Со спецификацией WS-I Basic Profile можно ознакомиться по адресу www.ws-i.org/Profiles/BasicProfile-l.1.html. Web-службы и каркас .NET Framework Каркас .NET Framework позволяет легко создавать и применять Web-службы, используя три основных пространства имен, связанные с Web-службами: □ System.Web. Services — классы этого пространства имен служат для создания Web-служб; □ System.Web. Services . Description — позволяет описывать Web-службы посредством WSDL; □ System.Web. Services . Protocols — позволяет создавать запросы и ответы SOAP.
700 Часть III. Программирование для Web Создание Web-службы Чтобы реализовать Web-службу, можно создать класс Web-службы, производный от System.Web. Services .WebService. Класс WebService предоставляет доступ к объектам Application и Session ASP.NET. Использование этого класса не является обязательным, и создавать его производный класс нужно, только если требуется простой доступ к предоставляемым им свойствам. Свойства класса WebService описаны в табл. 21.1. Таблица 21.1. Свойства класса WebService Свойство класса WebService Описание Application Возвращает объект HttpAppiicationState для текущего запроса Context Возвращает объект HttpContext, содержащий информацию, специфичную для HTTP. Этот контекст позволяет выполнить чтение информации заголовка HTTP Server Возвращает объект HttpServerUtility. Этот класс содержит ряд вспомогательных методов для выполнения кодирования и декодирования URL-адреса Session Возвращает объект HttpSessionState для хранения определенного состояния клиента User Возвращает объект пользователя, реализующий интерфейс iPrincipal. С помощью этого интерфейса можно получить имя пользователя и тип аутентификации SoapVersion Возвращает версию SOAP, используемую данной Web-службой. Версия SOAP содержится в перечислении SoapProtocoiversion При создании Web-служб с помощью ASP.NET они не обязательно должны наследоваться от базового класса WebService. Этот базовый класс требуется только при необходимости наличия простого доступа к HTTP-контексту, содержащему свойства класса WebService. Атрибут WebService Подкласс класса WebService должен быть помечен атрибутом WebService. Класс WebServiceAttribute имеет свойства, перечисленные в табл. 21.2. Таблица 21.2. Свойства класса WebServiceAttribute Свойство Описание Description Описание службы, которое будет использовано в документе WSDL Name Извлекает или устанавливает имя Web-службы Namespace Извлекает или устанавливает пространство имен XML для Web-службы. Значение, используемое по умолчанию — http://tempuri.org, которое вполне подходит для тестирования, но при публикации службы пространство имен потребуется изменить Подробнее атрибуты освещены в главе 30. Атрибут WebMethod Все методы, доступные из Web-службы, должны быть помечены атрибутом WebMethod. Конечно, служба может содержать и другие методы, которые не помечены этим ат-
Глава 21. Web-службы 701 рибутом. Вызов таких методов возможен из WebMethod, но не из клиентского приложения. Класс атрибута WebMethodAttribute позволяет вызывать метод из удаленных клиентов и определять, должен ли ответ подвергаться буферизации, в течение какого времени кэш должен оставаться действительным, и должно ли состояние сеанса сохраняться с именованными параметрами. Свойства класса WebMethodAttribute перечислены в табл. 21.3. Таблица 21.3. Свойства класса WebMethodAttribute Свойство Описание BufferResponse CacheDuration Description EnableSession MessageName TransactlonOption Получает или устанавливает флаг, указывающий необходимость буферизации ответа. Значение этого свойства по умолчанию — true. При буферизации ответа только готовый пакет отправляется клиенту Устанавливает продолжительность временного интервала, в течение которого должно выполняться кэширование результата. При вторичном выполнении того же запроса только кэшированное значение будет возвращено, если запрос выполняется в течение периода, заданного этим свойством. Значение этого свойства по умолчанию — 0, что означает отключение кэширования результата Используется при генерировании справочных страниц службы для предполагаемых клиентов Булевское значение, указывающее, является ли состояние сеанса допустимым. Значение этого свойства по умолчанию — false, т.е. свойство Session класса Webservice не может использоваться для хранения состояния сеанса По умолчанию имя сообщения устанавливается совпадающим с именем метода Указывает поддержку транзакций для метода. Значение этого свойства по умолчанию — Disabled Атрибут WebServiceBinding Атрибут WebServiceBinding служит для пометки уровня соответствия Web-службы требованиям пригодности для взаимодействия между Web-службами. Свойства этого атрибута описаны в табл. 21.4. Таблица 21.4. Свойства класса WebServiceBinding Свойство Описание ConformanceClaims EmitConformanceClaims Name Location Устанавливается равным значению перечисления WsiClaims. wsiciaims может иметь два значения: BasicProfilell, когда Web- служба соответствует базовому профилю Basic Profile 1.1, или None при отсутствии какого-либо соответствия Булевское свойство, которое определяет, должны ли заявления о соответствии, указанные свойством ConformanceClaims, передаваться в обобщенную документацию WSDL Определяет имя привязки. По умолчанию это имя совпадает с именем Web-служб с добавлением к нему строки Soap Определяет расположение информации о привязке, например, http://www.wrox.com/DemoWebservice.asmx?wsdl Namespace Определяет пространство имен привязки
702 Часть III. Программирование для Web Клиент Чтобы вызывать метод, клиент должен создать HTTP-подключение к серверу Web- службы и отправить HTTP-запрос для передачи сообщения SOAP. Вызов метода должен быть преобразован в сообщение SOAP. Все эти действия выполняются прокси-клиен- том. Реализация прокси-клиента осуществляется в классе SoapHttpClientProtocol. Класс SoapHttpClientProtocol Класс System.Web.Services.Protocols.SoapHttpClientProtocol — базовый класс прокси-клиента. Метод Invoke () преобразует аргументы для построения сообщения SOAP, отправляемого Web-службе. Вызываемая Web-служба определяется свойством Url. Класс SoapHttpClientProtocol поддерживает также асинхронные вызовы с помощью методов Beginlnvoke () и Endlnvoke (). Альтернативные клиентские протоколы Вместо класса SoapHttpClientProtocol можно использовать и другие классы прокси-клиента. HttpGetClientProtocol и HttpPostClientProtocol просто выполняют простые HTTP-запросы GET или POST, не порождая накладных расходов, связанных с вызовом SOAP. Классы HttpGetClientProtocol u HttpPostClientProtocol можно использовать, если в данном решении каркас .NET использован в системе как клиента, так и сервера. При необходимости использования других технологий придется применять протокол SOAP. Сравните следующий HTTP-запрос POST с ранее приведенным вызовом SOAP: POST /WebServiceSample/Servicel.asmx/ReverseString HTTP/1.1 Host: localhost Content-Type: application/x-www-form-urlencoded Content-Length: длина HTTP-запрос GET еще короче. Недостаток запроса GET состоит в том, что длина отправленных параметров ограничена. Если их длина превышает 1 Кбайт, следует подумать об использовании запроса POST. GET /WebServiceSample/Servicel.asmx/ReverseString?message=string HTTP/1.1 Host: localhost Накладные расходы, связанные с классами HttpGetClientProtocol и HttpPost Client Protocol, меньше чем те, что связаны с методами SOAP. Однако недостаток этого подхода в том, что какая-либо поддержка со стороны Web-служб, действующих на других платформах, и поддержка отправки чего-либо кроме простых данных отсутствует. Создание простой Web-службы ASP.NET В следующем практическом занятии мы создадим простую Web-службу с помощью Visual Studio.
Глава 21. Web-службы 703 шшшшяшшшявшшшшашшшшшвшаашшшшшшшшшшашш практическое занятие Создание проекта Web-службы 1. Выбрав пункт меню File^New1^Project (Файл1^ Создать1^ Проект), создайте новые проект Web-службы, укажите шаблон ASP.NET Web Service Application (Приложение Web-службы ASP.NET), как показано на рис. 21.4, присвойте проекту имя WebServiceSample и щелкните на кнопке ОК. Рис. 21.4. Создание нового проекта WebServiceSample Описание полученных результатов Файлы, сгенерированные шаблоном проекта, перечислены ниже. □ Servicel. asmx. Этот файл содержит класс Web-службы. Все Web-службы ASP.NET обозначаются расширением .asmx. Исходный код хранится в файле Service. cs, поскольку связанные с ним функциональные возможности используются средой Visual Studio. □ Servicel. asmx. cs. Шаблон проекта генерирует в файле Servicel. asmx. cs класс Servicel, который является производным от System. Web. Services. WebService. Этот файл содержит также образец кода для кодирования метода Web-службы — он должен быть общедоступным и помеченным атрибутом WebMethod: using System.Web; using System.Web.Services; using System.Web.Services.Protocols; [WebService(Namespace = http://tempuri.org)] [WebServiceBinding(ConformsTo=WsiProfile.BasicProfilel_l)] [Toolboxltem(false)] public class Service : System.Web.Services.WebService { [WebMethod] public string HelloWorldO { return "Hello World"; } }
704 Часть III. Программирование для Web Добавления Web-метода Следующее, что потребуется сделать — это добавить метод в создаваемую Web- службу. В приведенном ниже практическом занятии мы добавим простой метод — ReverseString (), — который принимает строку и возвращает клиенту строку в обратном порядке. Практическое занятие ~бавление МвТОДЭ 1. Удалите метод HelloWorld () и всю его реализацию. Добавьте следующий код в файл Service 1. asmx. cs. Чтобы можно было использовать класс Array, необходимо импортировать пространство имен System. [WebMethod] public string ReverseString(string message) { char[] arr = message.ToCharArray(); Array.Reverse(arr); message = new string(arr); return message; } 2. Модифицируйте пример кода в файле Servicel. asmx. cs следующим образом: [WebService (Namespace="http: //www.wrox.com/webservices") ] [WebServiceBinding(ConformsTo=WsiProfile.BasicProfilel_l)] [Toolboxltem(false)] public class Service : System.Web.Services.WebService { 3. Скомпилируйте проект. Описание полученных результатов Исполняющая среда ASP.NET использует рефлексию для считывания ряда специфичных для Web-служб атрибутов, таких как [WebMethod], чтобы предоставить метод в качестве функциональной возможности Web-службы. Исполняющая среда ASP.NET предоставляет также WSDL для описания службы. Чтобы XML-элементы в сгенерированном описании Web-службы могли идентифицироваться уникальным образом, необходимо добавить пространство имен XML. В приведенном примере пространство имен http: //www. wrox. com/webservices было добавлено в класс Service с использованием атрибута [WebService]. Естественно, можно применять любую другую строку, которая уникальным образом идентифицирует XML-элементы, такую как URL-ссылку страницы своей организации или компании. Вовсе не обязательно, чтобы Web-ссылка существовала в действительности. Она предназначена только для уникальной идентификации. Использование пространства имен, построенного на основе Web-адреса своей организации, обеспечивает почти стопроцентную гарантию того, что ни одна другая компания не будет применять это же пространство имен. Если пространство имен XML не будет изменено, по умолчанию будет использоваться пространство имен http://tempuri.org. В учебных целях это пространство имен вполне подходит, но его не следует использовать для развертывания реальной Web-службы.
Глава 21. Web-службы 705 Тестирование Web-службы Теперь Web-службу можно протестировать. Открытие файла Servicel. asmx в браузере (его можно запустить из Visual Studio 2008, выбрав пункт меню Debug^Start Without Debugging (Отладка1^Запустить без отладки) ведет к отображению списка всех методов службы, как показано на рис. 21.5. В созданной нами службе имеется единственный метод ReverseString (). 1 Ш Servicel Weto Service ММ MMI | ш^л.ишиЛ$туш 1: и s* Л Ser*c*i лее service I Servicel I The following operations «re supported • - ' •• r%rstrinu чШш/ШшшшШШшшш rtceLaaM ~\*t\x\\i.*tSeo** P "' 1 a • л о • • • >~ - . ■«* • J For a formal definition, please revien» the «•«ГУМ е QOJjaiA'P-0- - ■■■" _____--___ ** local sitreoetl Protected Mode On Marx » 1 Put. 21.5. Открытие файла Servicel. asmx в браузере При выборе ссылки на метод ReverseString открывается диалоговое окно тестирования Web-службы. Оно содержит поля редактирования каждого параметра, который можно передать с методом. В данном случае параметр является единственным. На этой странице можно также получить информацию о том, как будут выглядеть вызовы SOAP со стороны клиента и ответы сервера (рис. 21.6). Пример демонстрирует запросы SOAP и HTTP-запросы POST. £ Ser>K.e1 Web Service i } *. м*/Лос q^mrz и V #S*MC*1WtDS ft -Л В • «* • «:>»*» •<>*■*• ReverseString lest To test the operation using the HTTP POST protocol, dick the Invoke' button. POST /Servicel.ваш HTTP/1.1 Hose: localhost Content-Type: text/xml; cha.rset-ur.r-? Content-Length: length SOAPAccion: "http://www.wrox.coat/webservices/ReverseString" <?xml version-.0" encoding"utf-e*?> csoap:Envelope xxU.ns:xsi-"http: //www.w3.org/2001/XMLSchesM-instence" amine:xsd-"hti <soap:Body> <Reversestring xsUns-"http://www.wrox.coevwebservices"> <avessage>string</Bvessage> </ReverseString> </soap:Body> </soap:Envelopes > «fc Local Mtraiwt | Protected Mode On Рис. 21.6. Диалоговое окно тестирования Web-службы
706 Часть III. Программирование для Web Щелчок на кнопке Invoke (Вызвать) после ввода текста Hello Web Services ! в текстовом поле приведет к получению с сервера результата, показанного на рис. 21.7. Г Ш HHpU^ocimott:4ttiySTV«f1.aMimaHw»CnyirliiB Windows МияМ Exptortr 1 U Ы J|MpilocaRKMt48ei3ewwc«lM(TMRMritSt. *fc • Л Q * «* * *— <?xml versions'i.o* encoding ="utf-8" ?-» cstnng «^h>s=Nit»p://www.»^ox.com/«^l>sorvkes*>l$«cha»SbeWo»e«</stnng> i 1 dom <% tool MraMt|PrrtKM Mate: Oa ^*^d| p - 1 - JT-l. I Moo* - | ■* Pwc. 27.7. Результат вызова Web-службы Результат типа string, как и ожидалось, представляет собой введенную строку, отображаемую в обратном порядке. Реализация Windows-клиента Тестирование выполняется успешно, но нам нужно создать Windows-клиента, который использует Web-службу. Клиент должен создавать сообщение SOAP, которое будет пересылаться по HTTP-каналу. Это сообщение не обязательно создавать самостоятельно. Visual Studio 2008 создает прокси-класс, который использует HTTP-канал Windows Communication Foundation (WCF), который "за кулисами" выполняет все необходимые действия. WCF более подробно рассматривается в главе 35. Практическое занят! Создание клиентского Windows-приложения Создайте новое С#-приложение Windows Forms в существующем решении WebServiceSample и присвойте ему имя SimpleClient. Добавьте на форму два текстовых поля и кнопку (рис. 21.8). Для вызова Web-службы будет использоваться обработчик Click этой кнопки. "УШ11 сг- Рис. 21.8. Форма клиента Web-службы 2. С помощью команды меню Project^Add Service Reference (ПроектеДобавить ссылку службы) добавьте ссылку службы. В открывшемся диалоговом окне Add Service Reference (Добавление ссылки на службу) щелкните на стрелке кнопки Discover (Исследовать) и выберите опцию Services in Solution (Службы в решении). Ранее созданная служба отображается в левой панели. В отображенном слева древовидном представлении выберите Service 1 Soap. Прежде чем щелкнуть на кнопке ОК, измените имя в поле Namespace (Пространство имен) на WebServicesSample, как показано на рис. 21.9. 3. До сих пор мы не написали ни единой строчки кода для клиента. Мы разработали небольшой интерфейс пользователя и воспользовались меню Add Service Reference, чтобы создать класс прокси-клиента. Теперь остается только связать эти два объекта. Добавьте к кнопке обработчик button lClick события Click и поместите в него следующие два оператора:
Глава 21. Web-службы 707 ТШ2 То see a Mt of available services on a tpecrfx semr. enter • sendee UR1 and click Go. To browse for available services, dtck uncover уме ГтНр./Лоса»поЛ49в13/5е«<ке1 am*» Senoces - #- Servuel asnu - jj Servicel J° SennctlSoap Qptntionj ♦ ReveneStrmg -iJtJMnii'Miiiil l servue(s) found at address http:/1ocarhost498l3-Servkel.a»nu WebServKesSamplel Puc. 27.9. Диалоговое окно Add Service Reference private void buttonl_Click(object sender, EventArgs e) { WebServicesSample.ServicelSoapClient client = new WebServicesSample.ServicelSoapClient() ; textBox2.Text = client.ReverseString(textBoxl.Text); Solution Explorer ■ Solution -VVebSemc eSa Ц Описание полученных результатов Теперь новая ссылка на службу WebServicesSample должна отображаться в окне Solution Explorer (рис. 21.10). Щелкните на кнопке Show All Files (Показать все файлы), чтобы увидеть документ WSDL и файл reference, cs, содержащий исходный код прокси-клиента. Кнопка Show All Files — вторая в панели инструментов Solution Explorer. При помещении указателя мыши над кнопками всплывающие подсказки отображают информацию о назначении кнопок. Информацию, отображаемую проводником решений Solution Explorer только при щелчке на кнопке Show All Files, удобнее просматривать в представлении классов Class View (новый класс, реализующий прокси-клиента). Этот класс преобразует вызовы методов в формат SOAP. Представление Class View (рис. 21.11) будет содержать новое пространство имен с названием, которое было определено посредством имени ссылки на службу. В данном случае было создано пространство имен WebServicesSample. Класс ServicelSoapClient является производным от ClientBase<ServicelSoap> и реализует метод Web-службы ReverseString(). Выполните двойной щелчок на классе ServicelSoapClient, чтобы открыть автоматически сгенерированный файл reference. cs. Рассмотрим этот сгенерированный код. Класс ServicelSoapClient является производным от класса ClientBase<ServicelSoap>. Этот базовый класс создает сообщение SOAP в методе Invoke (). - #; WebServicesSample Ч^ configuration svetnfo В t(] Reference.svemap ■♦J Reference cs •£] Servuel disco 3J Servicel wsdi d3 ._, Ью i Obj _i app config В Э Forml cs jrj Solution Explorer ЩвЮГУИгТ " Puc. 21.10. Новая ссылка на службу WebServicesSample
708 Часть III. Программирование для Web «Search^ ♦ {) SimpleClient ♦ (> SimpleClient f 8 <>Г~ & ^J R*ver»eStnngRequejt ffl "^ RevefseStnnflRequestBody ♦ ^ ReverseStnngResponse A ■% ReverjeStnngRejpomeBody ffl **> ServicelSoap bt/ "° ServuelScupChannel В "t| SerwelSoapClient в ca в*** тл>« ♦ -*f ClientBase<Sen/KelSoap> ■* ServicelSoap • Ji ' I JdgSokjtion f wplorer * '^г Properties ; I Рис. 21.11. Новое пространство имен WebServicesSample Атрибут WebServiceBindingAttribute устанавливает значения привязки к Web- службе: [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code") ] [System.Web.Services.WebServiceBindingAttribute(Name="ServiceSoap", Namespace="http://www.wrox.com/webservices")] public partial class Service : System.Web.Services.Protocols.SoapHttpClientProtocol { В конструкторе свойство Url устанавливается в соответствии с Web-службой. Оно будет использоваться из класса SoapHttpClientProtocol для запроса службы: public Service() { this.Url = SimpleClient.Properties.Settings.Default.SimpleClient_WebServicesSample_Service; } Наиболее важный метод — метод, предоставляемый Web-службой: ReverseString (). Этот метод обладает тем же параметром, который реализован на сервере. Реализация клиентской версии метода ReverseString () вызывает метод Invoke () базового класса SoapHttpClientProtocol. Метод Invoke () создает сообщение SOAP, используя имя метода ReverseString и параметр message. Этот метод определен в файле reference. cs: public string ReverseString(string message) { ReverseStringReguest inValue = new ReverseStringReguest(); inValue.Body = new ReverseStnngReguestBody () ; inValue.Body.message = message; ReverseStringResponse retVal = ((ServicelSoap)(this)).ReverseString(inValue); return retVal.Body.ReverseStringResult; Отправка запроса SOAP no HTTP-соединению определена в автоматически созданном файле конфигурации приложения, который определяет basicHttpBinding:
Глава 21. Web-службы 709 <?xml version="l.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <bmdings> <basicHttpBinding> <binding name="ServicelSoap" closeTimeout=0:01:00" openTimeout=0:0 1:00" receiveTimeout=0:10:00" sendTimeout=0:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize=5536" maxBufferPoolSize=24288" maxRecelvedMe s sageS i ze=5536" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true" > <readerQuotas maxDepth=2" maxStringContentLength="8192" maxArrayLength=6384" maxBytesPerRead=096" maxNameTableCharCount=6384" /> <security mode="None"> <transport clientCredentialType="None" proxyCredentialType="None" realm="" /> <message clientCredentialType="UserName" algorithmSuite="Default" /> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="http://localhost:4 9813/Servicel.asmx" binding="basicHttpBinding" bindingConfiguration="ServicelSoap" contract="WebServicesSample.ServicelSoap" name="ServicelSoap" /> </client> </system.serviceModel> </configuration> Обращение к службе выполняется в следующем операторе с помощью сгенерированного прокси-класса. Следующий оператор создает новый экземпляр прокси-класса. Как показано в реализации конструктора, свойство Url устанавливается в соответствии с Web-службой: WebServicesSample.ServicelSoapClient client = new WebServicesSample.ServicelSoapClient(); В результате вызова метода ReverseString () прокси-класса сообщение SOAP отправляется серверу, что ведет к вызову Web-службы: textBox2.Text = client.ReverseString(textBoxl.Text); Выполнение программы ведет к созданию вывода, подобного показанному на рис. 21.12. [ V Web Services Client j The cow jumped over the moon Reverse Message noom eht tevo depmuj woe ehT L-lmJeMll 1 1 Рис. 21.12. Клиент Web-службы в действии
710 Часть III. Программирование для Web Асинхронный вызов службы При отправке сообщения по сети всегда нужно учитывать сетевую задержку. При синхронном вызове Web-службы клиентское приложение блокируется до момента возвращения вызова. В локальной сети это может происходить достаточно быстро. Однако следует уделить внимание инфраструктуре сети производственной системы. Сообщения Web-службе можно отправлять асинхронно. Прокси-клиент создает не только синхронные методы, но и асинхронные, однако при использовании Windows- приложений ситуация носит особый характер. Поскольку каждый элемент управления Windows связан с единственным потоком, методы и свойства элементов управления Windows могут вызываться только из создающего потока. Прокси-класс .NET 3.5 предоставляет ряд специальных функциональных возможностей, как видно из сгенерированного кода прокси-объекта. Практическое занятие АСИНХРОННЫЙ ВЫЗОВ Службы На этот раз применим асинхронную реализацию прокси-класса. 1. Внесите изменение в код сгенерированного прокси-класса, выбрав ссылку службы WebServicesSample. Откройте контекстное меню и выберите команду Configure Service Reference (Конфигурировать ссылку службы). Откроется диалоговое окно Service Reference Settings (Настройки ссылки на службу), показанное на рис. 21.13. SlmptoCKtnt.W*bS*rvic*»SMn(>to Seortc* Refertnct Sittings Chent Address Access Iweifor generated classes ЛЫи V Generate asynchronous operations Data Type * | Arways generate message contracts Collection type: Syst tmxray Dictionary collection type Syittm.Co«ectiom.GenerK.Dtctton*iy ;V: Reuse types in referenced assemblies • Reuse types in all referenced assembles Reuse types m specified refetenced assemblies -JrnscorUb J System JSystemCore -JSystem.Data vJS>stem.Data.DataSetbrtensions -J System Deployment J',it«m Drawing И -J S,4«m Runtime Serialization -JSystemSenrKeModel «| J J TJ Рис. 21.13. Диалоговое окно Service Reference Settings 2. В диалоговом окне Service Reference Settings установите флажок Generate asynchronous operations (Генерировать асинхронные операции). 3. Чтобы вызывать Web-службу асинхронно, измените реализацию метода buttonlClick (код приведен ниже). После того как экземпляр прокси-объекта создан, добавьте к событию ReverseStringCompleted обработчик по имени clientReverseStringCompleted. Затем вызовите асинхронный метод прокси-объекта ReverseStringAsync и передайте ему свойство Text из textBoxl. Метод asnyc создает поток, который выполняет обращение к Web-службе:
Глава 21. Web-службы 711 private void buttonl_Click(object sender, EventArgs e) { // Асинхронная версия WebServicesSample.ServicelSoapClient client = new WebServicesSample.ServicelSoapClient (); client. Reverses tringComple ted += new EventHandler<WebServicesSample. ReverseStringCompletedEventArgs> ( client_ReverseStringCompleted) ; client.ReverseStringAsync(textBoxl.Text); 4. Теперь реализуйте метод обработчика clientReverseStringCompleted: private void client_ReverseStringCompleted(object sender, WebServicesSample.ReverseStringCompletedEventArgs e) { textBox2.Text = e.Result; } Этот метод будет вызываться по завершении вызова Web-службы. При данной реализации свойство Result параметра ReverseStringComletedEventArgs передается свойству Text объекта textBox2. 5. Теперь можно запустить клиентское приложение еще раз, чтобы протестировать асинхронный вызов службы. В реализацию Web-службы можно добавить также интервал приостановки, чтобы убедиться, что интерфейс пользователя клиентского приложения не блокируется на время вызова Web-службы. Описание полученных результатов Следующий фрагмент кода представляет асинхронную версию метода ReverseString (). Асинхронная реализация прокси-класса всегда содержит метод, который можно вызывать асинхронно, и событие, позволяющее определять, какой метод должен вызываться по завершении метода Web-службы. Рассмотрим эти особенности подробнее. Метод ReverseStringAsync () содержит только те параметры, которые отправляются серверу. Считывание данных, полученных от клиента, можно выполнять, присваивая обработчик события событию ReverseStringCompleted, имеющему тип EventHandler<ReverseStringCompletedEventArgs>. Он представляет собой делегат, в то время как второй параметр (ReverseStringCompletedEventArgs) создается из выходных параметров метода ReverseString (). Свойство Result класса ReverseStringCompletedEventArgs содержит возвращаемые данные Web-службы. Эта реализация работает благодаря делегату SendOrPostCallback. Он направляет вызов в нужный поток элемента управления Windows Forms: public event System.EventHandler<ReverseStringCompletedEventArgs> ReverseStringCompleted; public void ReverseStringAsync(string message) { this.ReverseStringAsync(message, null); } public void ReverseStringAsync(string message, object userState) { if ((this.onBeginReverseStringDelegate == null)) { this.onBeginReverseStringDelegate = new BeginOperationDelegate(this.OnBeginReverseString); } if ((this.onEndReverseStringDelegate == null)) { this.onEndReverseStringDelegate = new EndOperationDelegate(this.OnEndReverseString); }
712 Часть III. Программирование для Web if ((this.onReverseStringCompletedDelegate == null)) { this.onReverseStringCompletedDelegate = new System.Threading.SendOrPostCallback( this.OnReverseStringCompleted); } base.InvokeAsync(this.onBeginReverseStringDelegate, new object[] { message}, this.onEndReverseStringDelegate, this.onReverseStnngCompletedDelegate, userState); } public void OnReverseStringOperationCompleted(object arg) { if ((this.ReverseStringCompleted !=null)) { InvokeAsyncCompletedEventArgs e = ((InvokeAsyncCompletedEventArgs) (state)); this.ReverseStringCompleted(this, new ReverseStringCompletedEventArgs (e.Results, e.Error, e.Cancelled, e.UserState)); public partial class ReverseStringCompletedEvenArgs : System.ComponentModel.AsyncCompletedEventArgs { private object [] results; internal ReverseStringCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : base(exception, cancelled, userState) { this.results = results; } public string Result { get { this.RaiseExceptionlfNecessary(); return ((string)(ths.results[0])); Реализация клиента ASP.NET Теперь эту же службу можно использовать из клиентского приложения ASP.NET. Ссылка на службу может выполняться так же, как в приложении Windows Forms. шшштттштттящяшшшттт тштштттттищицuiiij,u ijццi.j\mm*mm Создание клиентского приложения ASP.NET 1. Откройте ранее созданную Web-службу WebServices Sample. 2. Добавьте в решение новый Web-сайт С# ASP.NET, назовите его ASPNETClient и добавьте в Web-форму два текстовых поля и кнопку, как показано на рис. 21.14. 3. Добавьте ссылку на службу http: //localhost/web- servicesample/service.asmx, как это было выполнено в Windows-приложении. В зависимости от кон- Рис. 21.14. Форма сайта кретной конфигурации может потребоваться указа- ASPNETClient ние номера порта.
Глава 21. Web-службы 713 4. После того как ссылка службы добавлена, снова генерируется класс прокси-кли- ента. Добавьте к кнопке обработчик щелчка и напишите для него следующие строки кода: private void Buttonl_Click(object sender, System.EventArgs e) { WebServicesSainple. ServiceISoapClient client = new WebServicesSaznple.ServicelSoapClientO ; TextBox2. Text = client. Reversestring (TextBoxl. Text) ; } 5. Выполните компоновку проекта. С помощью команды меню Debugs Start (Отладка^Запуск) можно запустить браузер и ввести тестовое сообщение в первое текстовое поле. Щелчок на кнопке приведет к вызову Web-службы и во втором текстовом поле отобразится сообщение в обратном порядке, как показано на рис. 21.15. При использовании решения с множеством проекта в качестве начального проекта нужно установить проект, который требуется запустить. I S Unttttod Pag* Window» Internet... 1 n | fai ЯЫШ 1 ©О ' \щ_ Ь**//*кяЬо*Ю H«t| X l!t«« 1 и <W #Un*tdPaga Btginning Visual C# 2008 Rtvtrsa M«»sag# 8002 #C lausiV gnmmgaB ъ * 1 [% Local Mraa* I Protected Mod»: Чщв% " 1 Рис. 21.15. Сайт ASPNETClient в действии Описание полученных результатов Работа прокси-класса с Windows-приложением ничем не отличается от работы с созданным ранее Web-приложением. Добавление ссылки службы создает прокси-класс, построенный на основе документа WSDL. Прокси-класс выполняет SOAP-запрос службы. Передача данных При использовании разработанной ранее простой Web-службы ей передается только простая строка. Теперь нам предстоит добавить метод для передачи информации о погоде, запрошенной из Web-службы. Это требует пересылки Web-службе и из нее более сложных данных. Практическое занятие Передача данных с помощью Web-службы 1. С помощью Visual Studio откройте ранее созданный проект Web-службы WebServicesSample. Для этой Web-службы определите типы, показанные в следующем коде. Классы GetWeatherRequest и GetWeatherResponse определяют документы, которые должны отправляться Web-службе и из нее. Внутри этих классов применяются перечисления TemperatureType и TemperatureCondition.
714 Часть III. Программирование для Web Web-службы ASP.NET используют сериализацию XML для преобразования объектов в XML-представление. Для изменения внешнего вида сгенерированного XML-формата можно использовать атрибуты из пространства имен System. Xml. Serialization. Подробнее сериализация описана в главе 25. public enum TemperatureType { Fahrenheit, Celsius } public enum TemparatureCondition { Rainy, Sunny, Cloudy, Thunderstorm } public class GetWeatherRequest { public string City; public TemperatureType TemperatureType; } public class GetWeatherResponse { public TemparatureCondition Condition; public int Temperature; } 2. Добавьте метод GetWeather () Web-службы: [WebMethod] public GetWeatherResponse GetWeather(GetWeatherRequest req) { GetWeatherResponse resp = new GetWeatherResponse (); Random r = new Random () ; int Celsius = r.Next (-20, 50); if (req.TemperatureType == TemperatureType.Celsius) resp.Temperature = celsius; else resp.Temperature = B12-32)/100 * celsius + 32; if (req.City == "Redmond") resp.Condition = TemparatureCondition.Rainy; else resp.Condition = (TemparatureCondition)r.Next@, 3); return resp; } Этот метод принимает данные, определенные аргументом GetWeatherRequest, и возвращает данные, определенные аргументом GetWeatherResponse. Внутри реализации осуществляется возврат произвольных погодных условий — за исключением штаб-квартиры Microsoft в Рэдмонде, где дождь льет всю неделю. Для генерирования произвольных погодных условий используется класс Random из пространства имен System. 3. После построения Web-службы создайте новый проект на основе шаблона Windows Forms и назовите приложение WeatherClient.
Глава 21. Web-службы 715 4. Измените главное диалоговое окно, как показано на рис. 21.16. Оно содержит два переключателя, которые позволяют выбирать температурную шкалу (по Цельсию или по Фаренгейту), и текстовое поле для ввода названия города. Щелчок на кнопке Get Weather (Получить информацию о погоде) вызывает Web- службу, а результат отображается в текстовых полях Weather Condition (Погодные условия) и Temperature (Температура). Weather Condhon Temperature Рис. 21.16. Главное диалоговое окно приложения Wea therClient Типы, имена и значения свойства Text элементов управления перечислены в табл. 21.5. Таблица 21.5. Значения свойства Text элементов управления Элемент управления Имя Значение свойства Text RadioButton RadioButton Button Label TextBox TextBox TextBox Label Label Label GroupBox radioButtonCelsius radioButtonFahrenheit buttonGetWeather labelWeatherCondition textCity textWeatherCondition textTemperature labelCity labelWeatherCondition labelTemperature groupBoxl Celsius Fahrenheit Get Weather City Weather Condition Temperature 5. Добавьте ссылку на Web-службу, подобно тому, как это было сделано в предшествующих проектах клиентских приложений. Присвойте ссылке имя WeatherService. 6. Импортируйте пространство имен WeatherClient .WeatherService. 7. Используя диалоговое окно Properties (Свойства) кнопки, добавьте к кнопке buttonGetWeather обработчик события Click по имени buttonGetWeather_ Click.
716 Часть III. Программирование для Web 8. Добавьте следующую реализацию метода buttonGetWeatherClick (): private void OnGetWeather(object sender, EventArgs e) { GetWeatherRequest req = new GetWeatherRequest() ; if (radioButtonCelsius.Checked) req.TemperatureType = TemperatureType.Celsius; else req. Tempera tureType = TemperatureType.Fahrenheit; req.City = textCity.Text; ServicelSoapClient client = new ServicelSoapClientQ ; GetWeatherResponse resp = client.GetWeather(req) ; textWeatherCondition.Text = resp.Condition.ToStringO ; textTemperature. Text = resp. Temper a ture.ToStringO ; } Эта реализация создает объект GetWeatherRequest, который определяет запрос, отправленный Web-службе. Вызов Web-службы выполняется вызовом метода GetWeather (). Этот метод возвращает объект GetWeatherResponse, содержащий значения, которые считываются для отображения в интерфейсе пользователя. 9. Запустите клиентское приложение. Введите название города и щелкните на кнопке Get Weather. Если повезет, в окне отобразится информация о реальной погоде (рис. 21.17). 1Ш шШт client Temperature Type • Cetaus О Fthranhet [ GtfWatfhv Weather CorxJbor» Temperature •. mi*\ Oty 1 \Aerme 1 Cloudy 15 Рис. 21.17. Приложение Wea therClient в действии Резюме В этой главе вы узнали, что собой представляют Web-службы, и кратко ознакомились с применяемыми вместе с ними протоколами. Для отыскания и запуска Web- служб можно использовать следующие возможности. □ Каталог. Поиск Web-служб можно выполнять посредством технологии UDDI (Universal Description, Discovery and Integration — универсальное описание, поиск и взаимодействие). □ Описание. WSDL описывает методы и аргументы. □ Вызов. Независимые от платформы вызовы методов выполняются с использованием протокола SOAP.
Глава 21. Web-службы 717 В этой главе было показано, насколько просто создать Web-службы с помощью Visual Studio, где класс WebService применяется для определения определенных методов посредством атрибута WebMethod. Создание клиента, который использует Web- службы, столь же просто, как и создание Web-служб — достаточно в проект клиентского приложения добавить Web-ссылку и использовать прокси-объект. Основная часть клиентского приложения — класс SoapHttpClientProtocol, который преобразует вызов метода в сообщение SOAP. Созданный прокси-клиент предоставляет как асинхронные, так и синхронные методы. При использовании асинхронных методов интерфейс клиента не блокируется до завершения работы метода Web-службы. Вы научились также создавать пользовательские классы, которые определяют данные, передаваемые при необходимости передачи более сложных данных. Следующая глава посвящена развертыванию Web-приложений и Web-служб. Упражнения Следующие упражнения помогут применить приобретенные в этой главе знания для создания новой Web-службы, отвечающей за бронирование мест в кинотеатре. 1. Создайте новую Web-службу CinemaReservation. 2. Для определения данных, отправляемых Web-службе и из нее, требуются классы ReserveSeatRequest и ReserveSeatResponse. Класс ReserveSeatRequest нуждается в члене Name (Имя) типа string для отправки имени и в двух членах типа int для отправки запроса на бронирования места, определенного объектами Row (Ряд) и Seat (Место). Класс ReserveSeatResponse определяет данные, отправляемые клиенту — т.е. имя, на которое выполняется бронирование, и номер в действительности забронированного места. 3. Создайте Web-метод ReserveSeat, который требует наличия параметра ReserveSeatRequest и возвращает параметр ReserveSeatResponse. В реализации Web-службы можно использовать объект Cache (см. главу 19) для запоминания уже забронированных мест. Если запрошенное место свободно, приложение должно возвратить это место и зарезервировать его в объекте Cache. Если место занято, приложение должно выбрать соседнее свободное место. Для хранения мест в объекте Cache применяйте двумерный массив, как описано в главе 5. 4. Создайте клиентское Windows-приложение, которое использует Web-службу для бронирования мест в кинотеатре.
Программирование с использованием Ajax Пользователи хотят, чтобы Web-приложения были такими же интерактивными, как и Windows-приложения. Ajax (Asynchronous JavaScript and XML — асинхронный JavaScript и XML) представляет собой ту самую технологию, которая позволяет легко создавать интерактивные Web-приложения. В частности, в этой главе будут рассматриваться следующие темы. □ Что собой представляет технология Ajax. □ Как использовать предлагаемый в ASP.NET элемент управления Update Panel. □ Как использовать предлагаемый в ASP.NET AJAX элемент управления Timer. □ Как использовать элемент управления UpdateProgress. □ Как создавать Web-службу и вызывать ее из клиентского сценария. □ Какие расширяющие элементы управления доступны в наборе инструментальных средств ASP.NET AJAX Control Toolkit. Что собой представляет технология Ajax Технология ASP.NET работает на основе операций обратной отправки данных. Когда пользователь щелкает на кнопке или делает выбор в окне списка с включенной функцией AutoPostBack, серверу отсылается соответствующий запрос, после чего с него обратно клиенту отправляется целая страница. Это лишает пользователя ощущения того, что он работает с "обычным" приложением, поскольку осуществляется перерисовка всей страницы. Пользовательский интерфейс при этом должен обязательно оставаться активным на стороне клиента, а со стороны сервера должны запрашиваться только дополнительные данные для отображения.
Глава 22. Программирование с использованием Ajax 719 В действительности лежащая в основе Ajax технология новой не является. Internet Explorer и Web-приложения позволяли делать подобное в течение многих лет. В состав Internet Explorer, начиная еще с версии 5, входил объект XmlHttpRequest, делавший возможной отправку запроса из JavaScript с сохранением страницы в активном состоянии. Именно он и применялся в Outlook Web Access 2000 для предоставления пользователям более высокой степени интерактивности. Проблема заключалась в том, что работать с ним было нелегко. Ajax упрощает использование объекта XmlHttpRequest, и потому сегодня он уже также предлагается и в других браузерах. Ajax основан на следующих технологиях. □ DHTML. На стороне клиента получение доступа и изменение HTML-элементов должно осуществляться программным путем, что стало возможным с появлением динамического HTML (Dynamic HTML). □ JavaScript. Почти все браузеры поддерживают JavaScript — язык сценариев, применяемый для получения доступа к DOM (Document Object Model — объектная модель документов) из DHTML. □ XmlHttpRequest. Этот объект применяется для отправки асинхронных вызовов из JavaScript-кода серверу с целью запрашивания дополнительных данных. □ JSON (JavaScript Object Notation — объектная нотация JavaScript). Вместо передачи XML-кода между клиентом и сервером обычно применяется формат JSON, который требует меньше данных и оптимизирован под JavaScript. Предлагаемая Microsoft реализация Ajax называется ASP.NET AJAX и состоит из клиентской библиотеки JavaScript и использующих ее серверных элементов ASP.NET. В отличие от версии ASP.NET 2.0, в которой библиотеку и элементы управления ASP.NET AJAX требовалось загружать отдельно, в версии ASP.NET 3.5 библиотека ASP.NET AJAX уже входит в основной состав, так что ею можно начинать пользоваться в любой момент. Microsoft предлагает еще кое-какие дополнительные наборы инструментальных средств, которые могут быть полезны при разработке приложения ASP.NET AJAX. □ ASP.NET AJAX Control Toolkit. Разработанный при участии сообщества разработчиков набор, в состав которого входит несколько элементов управления для расширения возможностей Web-страниц. Как им пользоваться, будет показано в последнем разделе настоящей главы. □ Microsoft ASP.NET 3.5 Extensions Preview. В этом наборе предлагаются предварительные версии функциональных возможностей, которые, вероятно, будут доступны в будущей версии ASP.NET. Загрузить эти наборы можно по адресу http: //ajax. asp. net. Серверные элементы управления, которые можно использовать в приложениях ASP.NET, перечислены в табл. 22.1. Вместо серверных элементов управления можно также и напрямую использовать функции JavaScript. Имена соответствующих классов и методов очень похожи на имена классов и методов в .NET Framework, что позволит сразу же чувствовать себя "как дома". Некоторые из расширений клиентских сценариев ASP.NET AJAX описаны в табл. 22.2.
720 Часть III. Программирование для Web Таблица 22.1. Серверные элементы управления, которые можно использовать в приложениях ASP.NET Элемент управления ASP.NET AJAX Описание ScriptManager UpdatePanel Timer UpdateProgress Загружает клиентские сценарии. Для каждой страницы ASRNET AJAX требуется один экземпляр этого элемента управления Обеспечивает возможность выполнения частичного визуального обновления страницы посредством поддерживаемой ASRNET AJAX операции обратной отправки По истечении указанного промежутка времени вызывает какую-то функцию Отображает информацию о ходе процесса во время выполнения запроса Таблица 22.2. Расширения клиентских сценариев ASP.NET AJAX Расширения клиентских сценариев Описание Расширения типа Array Расширения типа Date Расширения типа string Пространство имен Sys Пространство имен Sys .Net Библиотека клиентских сценариев добавляет функции в JavaScript-тип Array. Методы, вроде add (), addRange(),clear (),contains (),insert() и remove () очень похожи на методы .NET-типа Array Библиотека клиентских сценариев добавляет функции в JavaScript-тип Date. С помощью метода format можно передавать форматирующие строки, подобные тем, что применяются вместе с .NET-струк- турой DateTime Библиотека клиентских сценариев добавляет функции в JavaScript-тип string. В число доступных здесь методов входят startswith (), endsWith (), trim () и trimEndO В пространстве имен Sys доступен класс StringBuilder, который ПОХОЖ на класс stringBuilder, предлагаемый в .NET Еще в нем доступен класс Application, который предоставляет возможность работать с такими глобальными клиентскими событиями, как mit, load и unload В состав пространства имен Sys .Net входят классы, которые можно использовать в клиентском сценарии для вызова Web-служб Теперь давайте посмотрим, как использовать возможности ASRNET AJAX в приложениях ASRNET, начиная с возможности частичного визуального обновления Web- страницы.
Глава 22. Программирование с использованием Ajax 721 Элемент управления UpdatePanel В следующем практическом занятии демонстрируется создание простой Web-страницы, подразумевающей использование такого элемента управления ASP.NET, как UpdatePanel. практическое занятие Использование элемента управления UpdatePanel 1. Создайте новый Web-сайт, выбрав в меню File (Файл) пункта New Web Site (Создать новый Web-сайта) и щелкнув на значке ASP.NET Web Site (Web-сайт ASP.NET). Присвойте новому сайту имя AJAXWebsite. 2. Добавьте новую Web-форму по имени UpdatePanelDemo. aspx и сделайте ее стартовой страницей приложения. 3. Из категории AJAX Extensions (Расширения AJAX) в окне Toolbox (Элементы управления) добавьте на эту страницу элемент управления ScriptManager: <asp:ScriptManager ID="ScriptManagerl" runat="server"> </asp:ScriptManager> 4. Затем добавьте элемент управления UpdatePanel: <asp:UpdatePanel ID="UpdatePanell" runat="server"> </asp:UpdatePanel> 5. Теперь добавьте по одному элементу управления Label и Button внутрь UpdatePanel и по одному элементу управления Label и Button за его пределами. Установите для свойства Text элемента управления Button, находящегося внутри UpdatePanel, и находящегося за его пределами, соответственно значение AJAX Postback и ASP.NET Postback (рис. 22.1). <form id="forml" runat="server"> <div> <asp:ScriptManager ID="ScriptManagerl" runat="server"> </asp:ScriptManager> </div> <asp:UpdatePanel ID="UpdatePanel1" runat="server"> <ContentTemplate> <asp:Label ID="Labell" runat="server" Text="Label"> </asp:Label> <asp:Button ID="Buttonl" runat="server" Text="AJAX Postback" /> </ContentTemplate > </asp:UpdatePanel > <asp:Label ID="Label2" runat="server" Text="Label"> </asp:Label> <asp:Button ID="Button2" runat="server" Text="ASP.NET Postback" /> </form> 6. Назначьте обеим кнопкам обработчик событий Click с именем OnButtonClick и реализуйте его показанным ниже образом: protected void OnButtonClick(object sender, EventArgs e) { Labell.Text = DateTime.Now.ToLongTimeString() ; Label2.Text = DateTime.Now.ToLongTimeString();
722 Часть III. Программирование для Web 7. Запустите приложение и попробуйте пощелкать на кнопках. Выполнение щелчка на кнопке A J AX Postback (Отправка AJAX) будет приводить к обновлению только первой надписи, а на кнопке ASP.NET Postback (Отправка ASP.NET) — к обновлению всей страницы. 1 Ш UrtMtd Page - Windows Intonwt Explorer О О " ' *' Нй&ЯхаЛоЛЯКМУ " | *f | X 11 Live Storx* w <* fUnttledPagt j Ъ - Л Q • *» 3^5:34 PM[~^AXPo»tbeck 3:23:23 PM Г ASP-NET Postb** [ __ <t Local MraiMtl Protected Vote: Оя —г"^гййГ| p 1 *-ц£**-« j «IMt* » 11 Puc. 22.1. Форма сайта AJAXWebsi te Описание полученных результатов Для любой страницы ASP.NET требуется один объект ScriptManager. Этот класс предусматривает загрузку JavaScript-функций для нескольких средств. Его еще также можно использовать и для загрузки своих собственных специальных сценариев. Все доступные свойства этого класса кратко описаны в табл. 22.3. Таблица 22.3. Свойства класса ScriptManager Свойство ScriptManager Описание EnablePageMethods EnablePartialRendenng LoadScriptsBeforeUI ScriptMode ScriptPath Scripts Services Указывает, должны ли общедоступные статические методы, определенные в ASPX-странице, быть пригодными для вызова из клиентского сценария в качестве методов Web-службы Для включения функции частичной визуализации в UpdatePanel для этого свойство должно быть установлено значение true, которое является значением по умолчанию Определяет позицию, в которой сценарии должны размещаться в возвращаемой HTML-странице. В случае их размещения внутри элемента <head> они будут загружаться перед загрузкой пользовательского интерфейса Указывает, какая версия сценариев должна использоваться — отладочная или рабочая Указывает путь к корневому каталогу, в котором находятся специальные сценарии Содержит коллекцию файлов специальных сценариев, которые должны визуализироваться для клиента Содержит коллекцию ссылок на Web-службы, которые могут вызываться из клиентского сценария
Глава 22. Программирование с использованием Ajax 723 Элементы управления ASP.NET типа Button на странице приводят к созданию клиентом HTML-кнопок типа Submit (Отправка). Button2 отправляет серверу обычный HTTP-запрос POST, a Buttonl — Ajax-запрос POST, потому что она находится внутри Update Panel и клиентский сценарий присоединяется к ее событию Click. Ajax-запрос POST подразумевает использование объекта XmlHttpRequest для отправки запроса серверу. Сервер возвращает только данные, требуемые для обновления пользовательского интерфейса. Эти данные интерпретируются, после чего код JavaScript изменяет HTML-элементы управления внутри Update Panel для получения нового пользовательского интерфейса: <form id="forml" runat="server"> <div> <asp:ScriptManager ID="ScriptManagerl" runat="server"> </asp:ScriptManager> </div> <asp:UpdatePanel ID="UpdatePanell" runat="server"> <ContentTemplate> <asp:Label ID="Labell" runat="server" Text="Label"> </asp:Label> <asp:Button ID="Buttonl" runat="server" Text="AJAX Postback" onclick="OnButtonClick" /> </ContentTemplate> </asp:UpdatePanel> <asp:Label ID="Label2" runat="server" Text="LabelM> </asp:Label> <asp:Button ID="Button2" runat="server" Text="ASP.NET Postback" onclick="OnButtonClick" /> </form> На странице может присутствовать и несколько элементов управления Update Panel. Давайте испробуем эту возможность в следующем практическом занятии. Элемент управления UpdatePanel с триггерами 1. Откройте созданный ранее Web-сайт AJAXWebsite. 2. Добавьте новую Web-форму AJAX по имени UpdatePanelWithTrigger .aspx и сделайте ее стартовой страницей приложения. В состав этого шаблона уже входит элемент управления ScriptManager. 3. Добавьте два элемента управления UpdatePanel. 4. Добавьте в каждый из элементов управления UpdatePanel элементы управления Label и Button. 5. Назначьте обоим элементам управления Button в качестве обработчика событий Click метод OnButtonClick и реализуйте этот метод, как показано ниже: protected void OnButtonClick (object sender, EventArgs e) { Labell.Text = DateTime.Now.ToLongTimeString (); Label2.Text = DateTime.Now.ToLongTimeString(); } 6. Запустите приложение. Щелчок на любой из кнопок будет приводить к изменению обоих элементов управления Label (как показано на рис. 22.2).
724 Часть III. Программирование для Web Г Ш Untitled Page - Windows Internet \t \) 1 #_ Mtp://tocalhost*03 w 42 ,£unt»edPage 932.26РМГ^0П1 9^32 26Pm[^?^J [% Local tntranetl Protected Mode: !ышийЪ 1 Pwc. 22.2. Изменение элементов управления Label после щелчков на кнопках 7. Измените значение свойства UpdateMode обоих элементов управления UpdatePanel с Always на Conditional. 8. Запустите приложение снова. Теперь выполнение щелчка на каждой из кнопок будет приводить к изменению только элемента управления Label, который находится внутри того же элемента UpdatePanel, что и данная кнопка. 9. Выберите первый элемент управления UpdatePanel и щелкните на кнопке с изображением символа троеточия напротив свойства Triggers, чтобы открыть диалоговое окно UpdatePanelTrigger Collection Editor (Редактор коллекции UpdatePanelTrigger), показанное на рис. 22.3. Добавьте триггер AsyncPostback, укажите для свойства ControlID в качестве значения кнопку второго элемента управления UpdatePanel, т.е. Button2, а для свойства EventName — значение Click. 10. Запустите приложение. Теперь выполнение щелчка на кнопке Button2 должно приводить к обновлению содержимого обоих элементов управления UpdatePanel, а на кнопке Buttonl — к обновлению только содержимого первого элемента управления UpdatePanel. г-шШ UpdetePanefTngger СоЯесвоп Editor Membert УГП'Я^Н AsyncPostBacfc Buttor»2 ChO properties- 4 В Be*»** ControlID Button.? EventName CSrtr. The trigger s target control ID Рис. 22.3. Диалоговое окно UpdatePanelTrigger Collection Editor
Глава 22. Программирование с использованием Ajax 725 Описание полученных результатов На поведение, связанное с обновлением элемента управления UpdatePanel, можно влиять. По умолчанию обновление осуществляется всякий раз, когда происходит операция обратной отправки данных Ajax. При желании можно изменять это поведение и делать так, чтобы элементы управления обновлялись только в случае инициации обновления либо изнутри самого элемента управления UpdatePanel, либо из элементов управления, находящихся за его пределами, путем определения триггера. Ниже приведен ASPX-код, который необходимо использовать для определения триггера AsyncPostBackTrigger для элемента управления UpdatePanel 1. <form id="forml" runat="server"> <div> <asp:ScriptManager ID="ScriptManagerl" runat="server"> </asp:ScriptManager> <asp:UpdatePanel ID="UpdatePanell" runat="server" UpdateMode="Conditional" > <ContentTemplate> <asp:Label ID="Labell" runat="server" Text="Label"> </asp:Label> <asp:Button ID="Buttonl" runat="server" OnClick="OnButtonClick" Text="Button" /> </ContentTemplate> <Trigg*rs> <asp:AsyncPostBackTrigger ControlID= "Button2"EventNam*="Click" /> </Triggers> </asp:UpdatePane1> </div> <asp:UpdatePanel ID="UpdatePanel2" runat="server" UpdateMode="Conditional"> <ContentTemplate> <asp:Label ID="Label2" runat="server" Text="Label"> </asp:Label> <asp:Button ID="Button2" runat="server" OnClick="OnButtonClick" Text="Button" /> </ContentTemplate> </asp:UpdatePanel> </form> В табл. 22.4 перечислены свойства элемента управления UpdatePanel. Таблица 22.4. Свойства элемента управления UpdatePanel Свойство UpdatePanel Описание chiidrenAsTriggers в случае установки для этого свойства значения true, содержимое элемента управления UpdatePanel обновляется при выполнении его дочерними элементами управления обратной отправки RenderMode Указывает, как элемент управления UpdatePanel должен визуализироваться. Возможными значениями являются UpdatePanelRenderMode.Block и UpdatePanelRenderMode.Inline. Значение Block указывает, что должен визуализировать дескриптор <div>, а значение inline - что должен визуализироваться дескриптор <span> UpdateMode Устанавливается в одно из значений перечисления UpdatePanelUpdateMode. Значение Always указывает обновлять элемент управления UpdatePanel при каждой обратной отправке данных Ajax, а значение Conditional - обновлять его только в зависимости от триггеров Triggers Содержит коллекцию элементов AsyncPostbackTrigger и PostbackTrigger, указывающих, когда должно обновляться содержимое элемента управления UpdatePanel
726 Часть III. Программирование для Web Элемент управления Timer Элемент управления Timer в ASP.NET AJAX может применяться для повторного выполнения какого-то метода по истечении указанного интервала времени. практическое занят*в| Использование элемента управления Timer 1. Откройте созданный ранее Web-сайт AJAXWebsite. 2. Добавьте новую Web-форму AJAX по имени TimerDemo. aspx и сделайте ее стартовой страницей приложения. 3. Далее добавьте в эту Web-форму элемент управления UpdatePanel. 4. Теперь добавьте в этот элемент управления UpdatePanel элемент управления Label и элемент управления Timer. 5. Установите для свойства Interval элемента управления Timer значение 5000 и добавьте в его событие Tick метод OnTick. 6. Реализуйте метод OnTick () так, чтобы он обновлял элемент управления Label: protected void OnTick(object sender, EventArgs e) { Labell.Text = DateTime.Now.ToLongTimeString (); } 7. Запустите Web-форму и удостоверьтесь в том, элемент управления Label в ней действительно обновляется каждые пять секунд (рис. 22.4). tf Untitled Page - Win... * It. мдолоаяюдеоз "Г £ <k #uiMtd 5:17:06 PM «4§ Local mtranet | Рго1 \ 100% » Рис. 22.4. Web-форма с элементом управления Timer Описание полученных результатов Элемент управления Timer использует клиентский сценарий JavaScript для выполнения операций обратной отправки через указанный промежуток времени. В случае его размещения внутри элемента управления UpdatePanel, как было сделано в этом примере, он может принимать участие и в операциях частичного визуального обновления. Элемент управления UpdateProgress Элемент управления UpdateProgress позволяет обеспечивать пользователей своего рода подсказкой касательно того, сколько времени осталось до завершения того или иного длительного процесса.
Глава 22. Программирование с использованием Ajax 727 Практическое занятие ИсПОЛЬЗОВЭНИе Элемента управления UpdateProgress 1. Откройте созданный ранее Web-сайт AJAXWebsite. 2. Добавьте в него новую Web-форму AJAX по имени UpdateProgressDemo.aspx и сделайте ее стартовой страницей приложения. 3. Далее добавьте на эту страницу элемент управления UpdateProgress, а потом внутри уже этого элемента управления добавьте текст Wait. . . (Ожидайте). 4. Теперь добавьте в эту страницу элемент управления Update Panel, после чего добавьте внутри этого элемента управления такой элемент управления ASP.NET, как TextBox и Button. 5. Установите для свойства AssociatedUpdatePanelld элемента управления UpdateProgress значение UpdatePanell. 6. Для свойства Text элемента управления TextBox установите значение 1000. 7. Импортируйте пространство имен System. Threading вверху файла UpdateProgressDemo. aspx. cs. Это пространство имен требуется для получения возможности использования в следующем шаге класса Thread. using System.Threading; 8. Назначьте событию Click элемента управления Button метод Button_Click и реализуйте его следующим образом: protected void Button_Click(object sender, EventArgs e) System.Threading.Thread.Sleep(int.Parse(TextBoxl.Text)); } 9. Запустите Web-форму и удостоверьтесь в том, что при выполнении щелчка на Button в ней отображается содержимое элемента управления UpdateProgress (рис. 22.5) и что это содержимое остается видимым до самых тех пор, пока процесс не будет завершен. Понаблюдайте за поведением данной страницы, вводя меньшие и большие значения в TextBox Перед выполнением щелчка на элементе управления Button. £ UntW^J Ра«е Wndow» Internet Exptorer U <H gUMMtfPagt 1 -^ 8000 r&JJtoT] ♦■Local.г Л I Protected Moo«: On <Чшт Рис. 22.5. Отображение содержимого элемента управления UpdateProgress после щелчка на Button
728 Часть III. Программирование для Web Описание полученных результатов Элемент управления UpdateProgress отображает содержимое своего шаблона <ProgressTemplate> в случае, если фоновая операция длится дольше указанного в его свойстве DisplayAfter временного промежутка. По умолчанию для свойства DisplayAfter устанавливается значение, равное 500 миллисекундам. То, какой именно процесс выполняется, роли не играет. В примере был использован процесс Thread. Sleep для создания задержки в ходе обработки, но вместо этого также могла бы вызываться Web-служба или выполняться какое-то другое длительное действие. <form id="forml" runat="server"> <div > <asp:ScriptManager ID="ScriptManagerl" runat="server" EnablePageMethods="True"> </asp:ScriptManager> <asp:U)pdateProgress ID="UpdateProgr«ssl" runat="server" AeeociatedUpdatePanelID="UpdatePanell"> <Progr*ssT«nplate> Wait </PrograeeTamplata> </asp:UpdatePrograss> </div> <asp:UpdatePanel ID="UpdatePanell" runat="server"> <ContentTemplate > <asp:TextBox ID="TextBoxl" runat="server" > 1000 </asp:TextBox> <br /> <br /> <br /> <asp:Button ID="Buttonl" runat="server" Text="Button" onclick="Button_Click" /> <br /> </ContentTemplate> </asp:UpdatePanel> </form> Во всех приводившихся до сих пор практических занятиях использовались лишь серверные элементы управления ASP.NET. В случае применения ASP.NET AJAX можно еще также пользоваться и библиотекой клиентских сценариев, например, вызывать Web-службу прямо из клиента. В следующем разделе будет показано, как это делать. Web-службы Web-службы используются для не зависящих от платформы коммуникаций. В главе 21 уже показывалось, как можно создавать и вызвать Web-службы ASP.NET, а в главе 35 еще будет показано, как создавать Web-службы с помощью WCF (Windows Communication Foundation). В следующем практическом занятии демонстрируется создание новой Web-службы и вызов ее непосредственно из клиентского сценария. Практическое занятие ВЫЗОВ Web-СЛужбы 1. Откройте созданный ранее Web-сайт AJAXWebsite. 2. Добавьте в него службу WCF, поддерживающую возможности AJAX (как показано на рис. 22.6), и присвойте ей имя HelloService. svc.
Глава 22. Программирование с использованием Ajax 729 [ Add New Item C:\BeflVCSharp\AJAXWeberte\ 1 j Jemplatci Visual Studio installed templates ; Jj Web Form II 'P ADO.NET Entity Data Model 4lAJAX Client Library i £» AJAX enabled WCF Service j *J 6'легк Handler 1 i 4JJScnpt File *J "«port Wizard j^ Skin File J " f ■' File 3j Web Service : ^xsiTFtle , My Templates H Master Page j^jAJAX Client Behavior ^] AJAX Master Page ij Browser File £J Crystal Report щ] Global Application Class _,& UNQ to SQl Classes JJl Resource File J SQLServer Database 3}WCF Service jj -mi File 1 A service tor providing data to an AJAX page 1 Иагле: HelloServiceJjvc 1 Language: [VfcuaiG» \шшшшяшшшяшвшшяш *] ' Place code ir ieiect maste 1 Web User Control ;£}AJAX Client Control 3 AJAX Web Form £ Class .Sj DataSet •) HTML Page J Report Zg Site Map Aj Style Sheet _j,-.Veb Configuration File Aj XML Schema sepaintefile page L_j-_J 1 ' Ш1Ш\ яВ|| м ipl 1 [ j j 1 1 -J 1 1 ■ ■<»«. i 1 Рис. 22.6. Добавление службы WCF, поддерживающей возможности AJAX 3. Откройте файл HelloService. cs и добавьте в него метод Greeting (): using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; [ServiceContract(Namespace = "") ] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class HelloService { [WebGet] [OperationContract] public string Greeting (string name) { return "Hello, " + name; } 4. Добавьте новую Web-форму AJAX по имени CallService. aspx. 5. Откройте редактор свойств для содержащегося внутри этой ASPX-страницы элемента управления ScriptManager. Щелкните на кнопке с изображением символа троеточия напротив свойства Services, чтобы открыть такое диалоговое окно ServlceReference Collection Editor (Редактор коллекции ServiceReference), показанное на рис. 22.7. В этом диалоговом окне добавьте ссылку на службу с помощью кнопки Add (Добавить) и измените значение в свойстве Path на HelloService.svc. 6. Далее добавьте в страницу три следующих HTML-элемента управления: Input (Text), Input (Button) и Div. 7. Установите для идентификатора элемента управления Div значение result. 8. Установите для идентификатора элемента управления Input (Text) значение input.
730 Часть III. Программирование для \yeb — HeUoServKt »»< properties В Behavior InimeScnpt False HdtoScrvkesvc ~;:j с The path to the jerwe bemo, referenced. Pwc. 22.7. Диалоговое окно ServiceReference Collection Editor 9. Замените значение свойства Value у buttonl на Invoke Service. . . (Вызов службы). 10. Измените блок сценария в странице путем объявления в нем сначала переменной helloService, как показано ниже: <script type="text/javascript"> var helloService; function pageLoadO { } </script> 11. Внутри метода pageLoad создайте экземпляр переменной helloService. Ее тип должен совпадать с тем, что указан в свойстве Services элемента управления ScriptManager, которое было определено ранее. HelloService — это прокси- объект для доступа к Web-службе: <script type="text/javascript"> var helloService; function pageLoad() { helloService = new HelloService () ; 12. Создайте функции helloSucceed и helloFailed, т.е. методы, которые должны вызываться асинхронным образом при успешной или неудачной попытке вызова Web-службы. Внутри метода helloSucceed установите свойство innerHTML дескриптора div по имени result равным результату, полученному от Web-службы: <script type="text/javascript"> var helloService; function pageLoad() { helloService = new HelloService(); } function helloSucceed (result) { var d = document.getElementByld("result") ; d.innerHTML = result; } function helloFailed(error, userContext, methodName) { } </script>
Глава 22. Программирование с использованием Ajax 731 13. Внутри функции pageLoad вызовите принадлежащие прокси-объекту методы set_defaultSucceededCallback и set_defaultFailedCallback для указания того, что в случае успешного или неудачного выполнения асинхронного вызова Web-службы должны вызываться функции helloSucceed и helloFailed: <script type="text/javascript"> var helloService; function pageLoad() { helloService = new HelloService(); helloService.set_defaultSucceededCallback(helloSucceed); helloService.set_defaultFailedCallback(helloFailed); } function helloSucceed(result) { var d = document.getElementByld ("result"); d.innerHTML = result; } function helloFailed(error, userContext, methodName) { } function callService () { var 1 = document.getElementByld("input"); helloService.GreetingA.value); } </script> 14. Добавьте функцию callService, использующую прокси-переменную hello для вызова предоставляемой Web-службой операции Greeting, как показано в приведенном ниже коде. Для отправки значения из поля ввода доступ к нему получается с помощью принадлежащей объекту document функции getElementByld. Под input подразумевается то имя, которое было присвоено этому полю ввода: <script type="text/javascript"> var helloService; function pageLoad() { helloService = new HelloService(); helloService.set_defaultSucceededCallback(helloSucceed); helloService.set_defaultFailedCallback(helloFailed); } function helloSucceed(result) { var d = document.getElementByld("result"); d.innerHTML = result; } function helloFailed(error, userContext, methodName) { } function callService () { var 1 = document. getElementByld ("input") ; helloService. Greeting A. value) ; } </script> 15. Укажите, что функция callService должна вызываться щелчком на кнопке Invoke Service. Для обеспечения ее вызова назначьте buttonel событие onclick так, как показано ниже: <input id="buttonl" type="button" value="Invoke Service..." onclick="callService ();" / > 16. Запустите Web-страницу, введите имя в текстовом элементе управления и вызовите Web-службу, щелкнув на кнопке.
732 Часть III. Программирование для Web Описание полученных результатов Создание службы WCF, поддерживающей возможности AJAX, приводит к добавлению в web. conf ig следующего конфигурационного раздела. Элемент enableWebScript в его подразделе <behaviors> снабжает Web-службу поддержкой JSON, благодаря которой Web-служба приобретает возможность напрямую получать и возвращать объекты JSON, которые оптимально подходят для клиентов JavaScript: <system.serviceModel> <behaviors> <endpointBehaviors> <behavior name="HelloServiceAspNetAjaxBehavior"> <enableWebScript /> </behavior> </endpointBehaviors> </behaviors> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" /> <services> <service name="HelloService"> <endpoint address="" behaviorConfiguration="HelloServiceAspNetAjaxBehavior" binding="webHttpBinding" contract="HelloService" /> </service> </services> </system.serviceModel> Внутри ASPX-страницы Web-служба конфигурируется с помощью свойства Services элемента управления ScriptManager. В приведенном примере служба была назначена с использованием специального редактора— ServiceReference Collection Editor. Этот редактор добавляет в ScriptManager элементы Services, содержащие элементы ServiceReference, которые ссылаются на Web-службу с помощью свойства Path: <asp:ScriptManager ID="ScriptManagerl" runat=llserver"> <Services> <asp:ServiceReference Path="HelloService.svc" /> </Services> </asp:ScriptManager> При наличии клиентского сценария Web-служба вызывается асинхронно. Здесь она вызывается обращением к методу Greeting и получает ту строку, которая была определена в операции Greeting: helloService.GreetingA.value); Результат возвращается тоже асинхронно. За тот метод, который должен вызываться после завершения Web-службой своей операции, отвечает метод setde fault SucceededCallback. В случае возврата Web-службой неудачного результата за вызываемый метод отвечает set_defaultFailedCallback: helloService.set_defaultSucceededCallback(helloSucceed) ; helloService.set_defaultFailedCallback(helloFailed); Благодаря использованию методов set_defaultSucceedCallback и set_default FailedCallback, назначенные им функции будут вызываться при каждом удачном и неудачном завершении Web-службой ее операции, какая бы из ее операций не вызывалась. Вместо применения этих стандартных методов можно еще назначать функции для успешного и неудачного завершения прямо вызову Web-службы. В рассматриваемом примере Web-служба предоставляет операцию Greeting (). Функция Greeting () прокси-объекта отправляет Web-службе запрос на получение приветствия (Greeting).
Глава 22. Программирование с использованием Ajax 733 Вместо простых входных параметров этой Web-службе могут передаваться и непосредственно сами функции helloSucceed и helloFailed, как показано ниже: helloService.Greeting(l.value, helloSucceed, helloFailed, null); Такой подход предоставляет возможность создавать разные функции для успешного и неудачного завершения в зависимости от вызываемой операции. Расширяющие элементы управления Технология ASP.NET AJAX предлагает замечательные преимущества в плане расширения существующих элементов управления. В частности, в ее составе предоставляется отдельный каркас для создания новых расширяющих элементов управления; никаких заготовленных расширяющих элементов управления в ней первоначально нет. Однако в наборе ASP.NET AJAX Control Toolkit таковых имеется довольно много. Поэтому в настоящем разделе пример основан на использовании именно этого набора. Практическое занятие ВЫ30В Web-СЛужбы 1. Загрузите набор ASP.NET AJAX Control Toolkit, упакованный в файл AjaxControl Toolkit-Framework3.5-NoSource.zip, со страницы www.asp.net/ajax/ downloads, сохраните его в каком-нибудь каталоге на своей системе и распакуйте. 2. Откройте созданный ранее Web-сайт AJAXWebsite в Visual Studio 2008. 3. В окне Toolbox (Панель управления) щелкните правой кнопкой мыши на пустой области, выберите в контекстном меню, которое появится после этого, пункт Add Tab (Добавить вкладку), чтобы создать новую категорию, а затем присвойте это новой категории имя AJAX Control Toolkit. 4. Щелкните правой кнопкой мыши на содержимом новой категории и выберите в контекстном меню пункт Choose Items (Выбрать элементы). После этого появится диалоговое окно Choose Toolbox Items (Выбор элементов для окна элементов управления). Щелкните в этом окне на кнопке Browse (Обзор) и выберите сборку AjaxControlToolkit.dll (она находится в каталоге SampleWebsite/bin/ того Zip-архива, который вы должны были распаковать в первом шаге). 5. После этого в новой категории внутри окна Toolbox появятся расширяющие элементы управления. 6. Добавьте в файл web.config элемент ajaxToolkit tagPrefix, ссылающийся на сборку AjaxControlToolkit: <pages> <controls> <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <add tagPrefix="asp" namespace="System.Web.UI.WebControls" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <add namespace="AjaxControlToolkit" assembly="AjaxControlToolkit" tagPrefix="ajaxToolkit"/> </controls> </pages>
734 Часть III. Программирование для Web 7. Добавьте новую Web-форму AJAX по имени TextBoxWatermark. aspx и сделайте ее стартовой страницей приложения. 8. Добавьте два ASRNET-элемента управления TextBox. 9. В конструкторе щелкните на контекстной метке первого элемента управления TextBox и выберите из меню вариант Add Extender (Добавить расширитель). (После наведения на элемент управления TextBox курсора мыши появится смарт-тег.) Это приведет к отображению диалогового окна Extender Wizard (Мастер добавления расширителя), как показано на рис. 22.8. Выберите в этом окне элемент TextBoxWatermarkExtender и щелкните на кнопке ОК. txtender Wizard у ШШ Choose an Extender Оихие th* functionjfcty to «dd to ТгжЮож* В" ericUp... 4 PasswordStr.. PopupContr... 1 ResizabieCo.. D RoundedCo \— SliderExtender f ■l _J_ mmJH Description: Specify »n Ю for the extender TextBoxllTeitBoxWatermarkExtender <ж J | cmaf] Рис. 22.8. Диалоговое окно Extender Wizard 10. Повторите все перечисленные в шаге 9 действия для второго элемента управления TextBox, чтобы добавить еще один элемент TextBoxWatermarkExtender. 11. Выделите первый элемент управления TextBox. В окне редактора свойств разверните группу TextBoxlTextBoxWatermarkExtender. Установите для свойства WatermarkText (Текст водяного знака) значение Enter your first name (Введите свое имя). 12. Выделите второй элемент управления TextBox. В окне редактора свойств разверните группу TextBox2_TextBoxWatermarkExtender и установите для свойства WatermarkText значение Enter your last name (Введите свою фамилию). 13. Запустите Web-страницу. В элементах управления TextBox, еще до добавления в них какого-либо текста, должен быть сразу же виден текст соответствующего водяного знака.
Глава 22. Программирование с использованием Ajax 735 Описание полученных результатов TextBoxWatermarkExtender — это расширяющий элемент управления, который позволяет дополнять элемент управления TextBox водяным знаком. При использовании диалогового окна Extender Wizard он добавляется в ASPX-файл и привязывается к элементу управления TextBox с помощью свойства TargetControlID, после чего определяет такие свойства, как WatermarkText и WatermarkCssClass. <asp:TextBox ID="TextBoxl" runat="server"> </asp:TextBox> <ajaxToolkit:TextBoxWatermarkExtender ID="TextBoxl_TextBoxWatermarkExtender" runat="server" Enabled="True" TargetControlID="TextBoxl" WatermarkText="Enter your firstname"> </ajaxToolkit:TextBoxWatermarkExtender> Резюме В этой главе было показано, как с помощью технологии ASP.NET AJAX можно делать Web-страницы ASP.NET более интерактивными. ASP.NET AJAX позволяет продолжить применять модель программирования ASP за счет применения новых серверных элементов управления, вроде Update Panel, с помощью которого можно выполнять частичные обновления Web-страницы, Timer, с помощью которого можно делать так, чтобы страница обновлялась через определенные промежутки времени, и UpdateProgress, который можно использовать для отображения пользователю какой-нибудь информации в ходе длительных процессов. Еще здесь было показано, как применять клиентские сценарии для вызова Web- службы не из файла отделенного кода, а прямо из кода JavaScript. В наборе ASP.NET Control Toolkit предлагаются расширяющие элементы управления, вроде TextBoxWatermarkExtender, который позволяет расширять возможности элемента управления TextBox. В этом наборе также доступно еще несколько других элементов управления. В следующей главе речь пойдет о развертывании Web-приложений и Web-служб. Упражнения 1. Какой элемент управления можно использовать для обновления только части Web-страницы? 2. Если Web-страница содержит несколько элементов управления Update Panel, как предотвратить обновление каждой содержащейся в Update Panel области во время частичной обратной отправки? 3. Как отображать анимированное GIF-изображение, чтобы оно становилось видимым только в ходе выполнения какого-то длительного действия? 4. Как сделать так, чтобы Web-служба вызывалась непосредственно из клиентского сценария?
Развертывание Web-приложений В предыдущих трех главах рассказывалось о том, как разрабатывать Web-приложения и Web-службы с помощью ASP.NET. Для всех этих типов приложений существуют различные варианты развертывания, вроде копирования Web-страниц, публикации Web-сайта и создания инсталляционной программы. В этой главе мы расскажем о преимуществах и недостатках каждого из этих вариантов, а так же о том, как они применяются на практике. В частности, в этой главе будут рассматриваться следующие темы. □ Компонент IIS (Internet Information Services). □ Конфигурирование IIS. □ Копирование Web-сайтов. □ Публикация Web-сайтов. □ Инсталлятор Windows. Компонент IIS Компонент IIS (Internet Information Services — информационные службы Internet) не нужно устанавливать для разработки Web-приложений с помощью Visual Studio 2008, потому что у Visual Studio 2008 имеется свой собственный Web-сервер, который называется Visual Web Developer Web Server. Он представляет собой простой Web-сервер, который работает только на локальной машине. Поэтому в производственной системе для запуска Web-приложений необходимо использовать все-таки IIS. В среде Windows Vista Home Edition компонент IIS не доступен. В других версиях Vista его можно устанавливать точно так же, как и другие компоненты Windows. В частности, для этого требуется просто открыть окно панели управления, щелкнуть на ссылке Programs (Программы), отыскать категорию Programs and Features
Глава 23. Развертывание Web-приложений 737 (Программы и компоненты) со ссылкой, позволяющей включать и отключать компоненты Windows, щелкнуть на этой ссылке и в списке компонентов Windows выбрать компонент Internet Information Services. При желании можно попросить установить IIS на системе своего системного администратора. Исполняющая среда ASP.NET должна обязательно быть сконфигурирована с IIS для того, чтобы в ней можно было запускать Web-приложения ASP.NET. Сконфигурирована ли исполняющая среда ASP.NET надлежащим образом, можно удостовериться, проверив отображения обработчиков в оснастке IIS Manager (рис. 23.1). Если IIS установлен, эта оснастка будет доступна в категории Administrative Tools (Администрирование) в виде элемента Internet Information Services (IIS) Manager (Диспетчер информационных служб Internet). В случае если категория Administrative Tools не была сконфигурирована так, чтобы к ней можно было получать доступ прямо из меню Start (Пуск), ее можно будет найти в окне панели управления. В окне оснастки Internet Information Services (IIS) Manager нужно дважды щелкнуть на Handler Mappings (Отображения обработчиков). Просмотрев информацию, которая появится после этого, можно будет увидеть, что путь * .aspx сконфигурирован два раза: один с IsapiModule и второй с System.Web.UI.PageHandlerFactory. Рис. 23.1. Отображения обработчиков в окне оснастки IIS Manager Если на системе не сконфигурированы отображения обработчиков с исполняющей средой ASP.NET, тогда можно запустить программу aspnetregiis -i, чтобы установить необходимые расширения файлов и модули с помощью IIS. Главным процессом IIS является inetinfo.exe. Он выполняется с повышенными привилегиями от имени учетной записи System. При сконфигурированном модуле IsapiModule, запрос ASPX-файла будет перенаправляться рабочему процессу (w3wp.exe). Можно делать так, чтобы разные рабочие процессы выполняли разные версии исполняющей среды .NET, а еще указывать пользователя, от имени которого должен выполняться каждый процесс, и задавать опции, связанные с возобновлением выполнения.
738 Часть III. Программирование для Web Конфигурирование IIS IIS нужно обязательно соответствующим образом конфигурировать перед тем, как запускать с его помощью какие-то Web-приложения. В следующем практическом занятии демонстрируется пример создания Web-сайта с помощью оснастки Internet Information Services (IIS) Manager. Первым делом этому Web-сайту необходим виртуальный каталог, т.е. каталог, который будет использовать клиент при получении доступа к Web-приложению. Например, в http: //server/mydirectory виртуальным каталогом будет mydirectory. Виртуальный каталог совершенно не зависит от физического каталога, отвечающего за хранение файлов на диске. Например, для mydi rectory физический каталог вполне может выглядеть как D: \someotherdirectory. Пракп ическое Создание нового пула приложений 1. Запустите оснастку IIS Manager (рис. 23.2). Найти ее можно в окне панели управления, в разделе Administrative Tools (Администрирование). "G фф \% * FARABOvT и ^j| FARABOVEHome «j FAJtAIOVE (taraboveVCta ^^ J' * 4 WebSftH Apportion Development .NET .NET Application СопциШмп GtotMKutton Settings PR - Connection P*o** *od Providers Strtno* CootroH Setuon SUte SMTP E Рис. 23.2. Окно Internet Information Services (IIS) Manager 2. В древовидном представлении выберите узел Application Pools (Пулы приложений), щелкните на нем правой кнопкой мыши и выберите в контекстном меню, которое появится после этого, пункт Add Application Pool (Добавить пул приложений). 3. Когда появится диалоговое окно Add Application Pool (Добавление пула приложений), показанное на рис. 23.3, введите Beg C# App Pool в текстовом поле Name (Имя) и выберите вариант .NET Framework v2.0.50727 в поле .NET Framework version (Версия .NET Framework). Поскольку в ASP.NET 3.5 используется та же исполняющая среда, что и в ASP.NET 2.0, версия .NET Framework v2.0.50727 подходит и для приложений ASP.NET 2.0, и для приложений ASP.NET 3.5.
Глава 23. Развертывание Web-приложений 739 Add Application Pool Шп* | Beg C# App Pool J .NET Framework version: J j ЛСТ Гпи—Of» W2A50727 Managed pipeline mode: i [**■■** • Start application роЫ immediately 1 <* 1! Hi Canal я 1] - 1 I Pwc. 23.3. Диалоговое окно Add Application Pool После создания пула приложений можете настроить дополнительные параметры в окне Advanced Settings (Дополнительные параметры), показанном на рис. 23.4, чтобы указать идентификационные данные, с которыми должен выполняться процесс; а также то, должны ли в системе с множеством процессоров использоваться только конкретные процессоры, и то, какое количество рабочих процессов должно выполняться в данном пуле. Получить доступ к этим дополнительным параметрам можно либо выбором созданного пула приложений из категории Actions (Действия) в правой части окна Internet Information Services (IIS) Manager, либо выбором соответствующего пункта из контекстного меню. Advanced Settings и В (General) *j .NET Framework Version vU Enable 32 Bit Applications False Managed Pipeline Mode Integrated Name вер С* Лрр РоЫ Queue Length 1000 Start Automat к ally False В CPU limit 0 Limit Action NoActton Limit Interval (minutes) S Processor Affinity Enabled False Processor Affinity Mask 4294967295 EI Process Model Identity NetworfcService Identity Specif к User Credentials Idle Timeout (minutes) 20 Maximum Worker Processes 1 Ping Enabled True Ping Maximum Response Time (« 90 Ping Period (seconds) 30 Name (name] The application pool name is the unique identifier for the application pool. Рис. 23.4. Окно Advanced Settings Описание полученных результатов Пулы приложений позволяют использовать для разных Web-сайтов разные версии исполняющей среды ASP.NET, а также создавать для них разные учетные записи пользователей и предусматривать различную степень стабильности.
740 Часть III. Программирование для Web После настройки пула приложений можно переходить к созданию нового Web- приложения. практическое занятие Создание нового Web-приложения 1. В окне IIS Manager выберите в древовидном представлении узел Default Web Site (Web-сайт по умолчанию). 2. Щелкните на нем правой кнопкой мыши и выберите в контекстном меню пункт Add Application (Добавить приложение). После этого на экране появится диалоговое окно Add Application (Добавление приложения), как показано на рис. 23.5. (Add Application Web Site name: Default Web Site Path: / Alias: BegVCSharpWebsite Example: vales Phyucal path: c:\BegVCSnarp\Webvite Рис. 23.5. Диалоговое окно Add Application 3. В этом окне введите в поле Path (Путь) желаемый физический путь к Web-сайту, а в поле Alias (Псевдоним) — псевдоним BegVCSharpWebsite. В поле Application pool (Пул приложений) с помощью кнопки Select (Выбрать) выберите созданный только что пул Beg C# App Pool. 4. Щелкните на кнопке ОК. Теперь Web-приложение сконфигурировано, а это значит, что Web-сайт можно начинать использовать для копирования или публикации Web-приложений из Visual Studio. Копирование Web-сайта В Visual Studio 2008 разрешается копировать файлы с исходного Web-сайта на удаленный. Исходным Web-сайтом называется Web-сайт того Web-приложения, которое было открыто в Visual Studio. Доступ к нему получается либо из локальной файловой системы, либо из IIS — в зависимости о того, как Web-приложение создавалось. Доступ к удаленному Web-сайту, на который требуется скопировать файлы, может получаться как с помощью файловой системы, так и по протоколу FTP или посредством FrontPage Server Extensions в IIS. Копирование файлов может осуществляться в обоих направлениях, т.е. как с исходного Web-сайта на удаленный, так и наоборот. В следующем практическом занятии демонстрируется пример использования Visual Studio для копирования Web-приложения, которое было создано в главе 20, на Web-сайт, который был сконфигурирован ранее в этой главе. Application pool: Bey С» App Pool SetecL. В
Глава 23. Развертывание Web-приложений 741 практическое занятие Копирование Web-сайта 1. В случае использования Windows Vista запустите Visual Studio 2008 с повышенными административными правами, потому что для копирования Web-сайта на локальный сервер IIS необходимо иметь права администратора. В случае использования Windows XP можете запустить Visual Studio обычным образом при условии, если вошли в систему с помощью учетной записи с правами администратора. 2. Откройте Web-приложение из главы 20. 3. Выберите в меню Website (Web-сайт) пункт Copy Web Site (Копировать Web- сайт). После этого на экране появится такое диалоговое окно, показанное на рис. 23.6. Copy We* С Л.. \ connections Source Art wte: С begcsharplventRegistrationWeb СИ CDS аиа Ими Admin App.Code App.Deta Intro IJOetauJtasp» {J Default aspics *) Global isi. _|logm.asp« 3)logmasp> cs 3 R«u«^«g« jj ResultsPage _J(W«b COnlig «£; ""'"••• Status Nr.s ИМ ИМ Nrv. MM Им MfA New L-Jljilj'JU Date Modified W2/-3007 840AM 10.2/2007 8:36 AM 10/2/2007 8 46 AM 10/2-2007 951 AM lO-'lVOm 9.06 AM 10/2-2007 836 AM 10/2-2007 8:39 AM 10/2 2007 1011 AM mi riiiiiiViYnl » Date Modified и a last refresh 1 7 006 1 39 PM У Show deleted tiles since the last copy operation «*L Рис. 23.6. Диалоговое окно копирования Web-сайта 4. Щелкните на расположенной вверху этого окна кнопке Connect (Подключиться). Это приведет к открытию диалогового окна Open Web Site (Открытие Web-сайта), показанного на рис. 23.7. 5. В этом окне можно выбирать файлы для копирования в следующие места: локальную файловую систему, локальный сервер IIS, сайты FTP и удаленные сайты (с установленным компонентом FrontPage Server Extensions). Щелкните здесь на кнопке Local IIS (Локальный сервер IIS) и выберите созданный ранее в главе Web-сайт BegVCSharpWebsite. В версии Windows Vista Home Edition файлы можно копировать только в локальную файловую систему, потому что сервер IIS в этой версии не доступен. 6. В списке Source Web site (Исходный Web-сайт) выберите файлы, которые хотите скопировать с исходного Web-сайта на удаленный Web-сайт.
742 Часть III. Программирование для Web [ ор*л wtt> s«» L? i g J] II FMeSrrten II localB FTPSMe RcanoteSMe i\ 1 1 I* Local НИШИ bklwiRHBHI S#fV#r Select the Web site you want to open % 4 \ local Web Servers ♦. <# GSS3ZEE3 H U»e Secure Sockets Layer Open ; Cancel i Pwc. 23.7. Диалоговое окно Open Web Site 7. Щелкните на кнопке Copy Selected Files (Скопировать выбранные файлы). Эта кнопка расположена между представлением Source Web Site (Исходный Web- сайт) и Remote Web Site (Удаленный Web-сайт) и содержит изображение стрелки. Наведение курсора мыши на любую из расположенных здесь кнопок позволит узнать о ее предназначении. Направление стрелки на них указывает на то, в каком направлении будут копироваться файлы: с исходного на удаленный сайт или наоборот. Кнопка со стрелками, указывающими в обоих направлениях, подразумевает выполнение проверки на предмет того, какие файлы являются новее, и копирование на другую сторону только их. 8. После этого все выбранные файлы будут скопированы на новый Web-сайт. С помощью средства Copy Web Site можно также выбирать и файлы для копирования с удаленного Web-сайта на исходный. Кнопка Synchronize Selected Files (Синхронизировать выбранные файлы) с изображением стрелок, указывающих в обоих направлениях, позволяет копировать с удаленного Web-сайта на исходный и с исходного на удаленный только более новые файлы. Она является очень полезной опцией, при наличии общего Web-сервера, на котором другие разработчики синхронизируют файлы. Выполнение синхронизации в обоих направлениях тогда позволяет легко осуществлять копирование своих более новых файлов на общий Web-сервер, а файлов с удаленного Web-сервера коллег — на свой локальный сайт. В случае только копирования файлов не может быть никакой уверенности в том, что они будут компилироваться. Компиляция будет происходить лишь при получении доступа к этим файлам через браузер. Однако существует утилита командной строки, с помощью которой можно выполнять предварительную компиляцию Web-сайта — aspnet_compiler.ехе. Достаточно ввести команду aspnet_compiler -v /BegVCSharpWebsite и Web-сайт BegVCSharpWebsite будет предварительно скомпилирован. Благодаря этому, первому же пользователю не придется ждать, пока будут скомпилированы страницы ASPX, потому что они будут уже скомпилированы. Найти эту утилиту можно в каталоге исполняющей среды .NET.
Глава 23. Развертывание Web-приложений 743 Публикация Web-сайта Вместо того чтобы копировать все исходные файлы (.cs) на целевой Web-сайт, можно предварительно скомпилировать Web-сайт с опцией публикации. При публикации сначала создаются сборки, а затем на целевой Web-сайт копируются лишь файлы презентации (т.е. файлы . aspx, . ascx и т.д.). Исходные файлы . cs при таком подходе на целевой Web-сайт не копируются. В следующем практическом занятии показано, как использовать Visual Studio для публикации Web-приложения из главы 20. практическое занятие Публикация Web-сайта для развертывания 1. Откройте Web-приложение EventRegistrationWeb из главы 20 в Visual Studio. 2. Создайте новый каталог для предварительно скомпилированного Web-сайта на диске С: и присвойте ему имя precompiledWeb. 3. Выберите в меню Build (Сборка) пункт Publish Web Site (Опубликовать Web- сайт). Это приведет к отображению на экране диалогового окна Publish Web Site (Публикация Web-сайта), как показано на рис. 23.8. Publish Web Site Target location: (ftp://.... http://... or oVive:\p*th) C\precompiledweb , V Allow this precompiled site to be updatable Use fmfd naming and single page assemblies Enable jtrong naming on precompiled assemblies Ф Use a ley file generated with t [9 , Д -J ig Nime tool ;gning Use a ►ey container Mar» assemblies with AHowPart.atfyTiustedCaileT*ttnbute iAPTCAj —_—J I—rr1*^—1 Рис. 23.8. Диалоговое окно Publish Web Site Ниже приведено краткое описание опций, которые можно сконфигурировать в этом окне. • Allow this precompiled site to be updatable (Сделать этот предварительно скомпилированный сайт пригодным для обновления). Файлы отделенного кода компилируются в сборки, но файлы ASPX публикуются на исходном сайте. При желании сделать так, чтобы администратор Web-сайта (или приобретающий данный Web-сайт клиент) не мог обновлять его компоновку (т.е. файлы ASPX), снимите отметку с этого флажка. Тогда ваши файлы ASPX все равно будут опубликованы, но их содержимое будет сопровождаться таким текстом: This is a marker file generated by the precompilation tool, and should not be deleted! Этот маркерный файл был сгенерирован программой предварительной компиляции, и его нельзя удалять!
744 Часть III. Программирование для Web Содержимое файлов ASPX компилируется в сборки. • Use fixed naming and single page assemblies (Использовать одностраничные сборки с фиксированными именами). Эта опция обеспечивает создание одной сборки для каждой страницы и присваивание ей фиксированного имени, которое при следующей публикации будет выглядеть точно так же. Отметка этого флажка сделает возможным обновление отдельных частей Web-сайта, а также позволит обновлять и добавлять другие сборки. • Enable strong naming on precompiled assemblies (Включить механизм присваивания строгих имен предварительно компилируемым сборкам). Эта опция позволяет указать файл ключей, который должен использоваться для подписания генерируемых сборок. 4. Щелкните на кнопке ОК, чтобы опубликовать Web-сайт. Преимущество подхода с предварительной компиляцией Web-сайтов состоит в том, что в случае его применения исходные файлы на целевой Web-сайт не копируются, а это значит, что Web-администратор не сможет изменять никакие исходные файлы на сервере. Инсталлятор Windows Еще можно создавать инсталляционную программу Windows для инсталляции Web- приложения. Создавать инсталляционные программы требуется только в том случае, если в приложении необходимо использовать разделяемые сборки. Преимущество подхода с созданием инсталляционных программ состоит в том что, в случае его применения виртуальный каталог конфигурируется с помощью IIS и, следовательно, создавать его вручную не требуется. Человеку, инсталлирующему Web-приложение, достаточно просто запустить программу setup.exe, и весь процесс установки будет выполнен автоматически. Разумеется, для ее запуска обязательно нужны привилегии администратора. Создание установочной программы В Visual Studio 2008 для создания инсталляционных программ для Web-приложения предлагается проект специального типа, который называется Web Setup Project (Проект программы установки Web-приложения). В его составе доступны такие редакторы, как File System Editor (Редактор файловой системы), Registry Editor (Редактор реестра), File Types Editor (Редактор типов файлов), User Interface Editor (Редактор пользовательского интерфейса), а также Custom Action Editor (Редактор специальных действий) и Launch Conditions Editor (Редактор условий запуска). Об этих редакторах уже рассказывалось в главе 18 при рассмотрении Windows-приложений, поэтому здесь будут показываться только те из них, которые необходимо использовать для Web-приложений. В следующем практическом занятии демонстрируется пример создания установочной программы, инсталлирующей Web-приложение.
Глава 23. Развертывание Web-приложений 745 практическое занятие Создание установочной программы 1. Откройте Web-приложение EventRegistrationWeb из главы 20 с помощью Visual Studio 2008. 2. Добавьте в него новый проект типа Web Setup Project, как показано на рис. 23.9. Назовите его EventRegistrationWebSetup и щелкните на кнопке ОК. Add New Project ~r froject types Smart Device Office Database Acropolis UNQ to XSD Preview Reporting Test WCF Workflow Database Projects Other Languages Other Project Types Setup and Deployment Database Extensibility T4t Pr<>i»rt« Iemplates: Visual Studio installed templates 3 Setup Project _CJ Merge Module Project J] CAB Project My Templates ^Search Опйпе Templates. j|cag ^ Web Setup Project 3 Setup Wizard щ Smart Device CAB Project Create a Windows Installer web project to which files can be added flame: Location EventRegistrationWeb c\BegVCSharp\Websetup I <* Рис. 23.9. Добавление в приложение проекта типа Web Setup Project 3. После этого на экране появится окно File System Editor (Редактор файловой системы). Щелкните в нем на File System on Target Machine (Файловая система на целевой машине) и выберите в меню Project (Проект) пункт Add1^Project Output (Добавить^Вывод проекта). Затем в диалоговом окне Project Output (Вывод проекта) выберите опцию Content Files of Web Application (Файлы содержимого Web- приложения) и щелкните на кнопке ОК. 4. Вернувшись в окно File System Editor, щелкните на Web Application Folder (Папка Web-приложения). После этого можете сконфигурировать Web-приложение с помощью редактора свойств. В табл. 23.1 вкратце описаны все доступные свойства. 5. Откройте окно Launch Conditions Editor (Редактор условий запуска) путем выбора в меню View (Вид) пункта Editors Launch Conditions (Редактор^Редактор условий запуска). Условия запуска позволяют указывать, какие продукты должны быть обязательно установлены на целевой системе перед выполнением инсталляции. 6. Проверьте сконфигурированные условия запуска. Конфигурация Search for IIS (Поиск сервера IIS) предусматривает выполнение проверки на предмет наличия на целевой системе установленной копии IIS путем просмотра ключа реестра SYSTEM\CurrentControlSet\Services\W3SVC\Parameters и выяснения ее версии. Условие запуска IIS Condition (Условие наличия IIS) гарантирует получение уверенности в том, что на целевой системе установлена как минимум версия IIS 4. Для Web-приложений ASP.NET 3.5 это значение можно повышать до 5, чтобы для их установки требовалась как минимум версия IIS 5.
746 Часть III. Программирование для Web Таблица 23.1. Свойства, доступные для настройки папки Web-приложения Свойство папки Web-приложения Описание AllowDirectoryBrowsing AllowReadAccess AllowScriptSourceAccess AllowWriteAccess DefaultDocument ExecutePermissions LogVisits VirtualDirectory Представляет собой опцию конфигурации IIS. В случае установки его в true разрешает отыскивать файлы на Web- сайте путем просмотра. По умолчанию для него устанавливается значение false По умолчанию для этого свойства устанавливается значение true. Для осуществления доступа к ASPX-страницам обязательно требуется доступ для чтения По умолчанию получать доступ к исходным файлам сценария запрещается за счет установки этого свойства в false Получать доступ для записи по умолчанию тоже запрещается Задает домашнюю страницу Web-сайта, например, это может быть страница Default. aspx По умолчанию для этого свойства устанавливается значение vsdepScriptsOnly, которое разрешает получать доступ к страницам ASP.NET, но не разрешает запускать на сервере специальные исполняемые файлы. При необходимости, чтобы на сервере можно было запускать специальные исполняемые файлы, это свойство можно устанавливать в vsdepScriptsAndExecutables В случае установки для него значения true, обеспечивает регистрацию попыток доступа клиентов Указывает на имя виртуального каталога, который конфигурируется с помощью IIS 7. Скомпонуйте установочное приложение путем выбора в меню Build (Сборка) пункта Build EventRegistrationWebSetup (Собрать приложение Event RegistrationWebSetup). 8. После этого вы сможете найти в каталоге установочного проекта файл setup.exe и установочный пакет по имени EventRegistrationWebSetup.msi. Инсталляция Web-приложения Запуск программы setup.exe позволит инсталлировать Web-приложение. Инсталляция Web-приложения 1. Щелкните на файле setup. exe для запуска процесса инсталляции Web-приложения. Появится окно со страницей приветствия мастера установки (рис. 23.10). Щелкните в нем на кнопке Next (Далее). 2. На странице Select Installation Address (Выбор адреса установки), которая показана на рис. 23.11, измените имя виртуального каталога в поле Virtual Directory (Виртуальный каталог) так, чтобы оно отличалось от того, которое уже было сконфигурировано с помощью IIS. В поле Application Pool (Пул приложений) выберите созданный ранее пул Beg C# App Pool и щелкните на кнопке Next. 3. На следующей странице подтвердите установку, выполнив щелчок на кнопке Next, как показано на рис. 23.12.
Глава 23. Развертывание Web-приложений 747 Welcome to the EventRegistrationWeb Setup Wizard |м;&1.; а~ The rutaler wi gurfe you through the steps requved to ratal E ventRegnttebonWeb on you WARNING T hi$ computer program is protected by copyright lew and international treates Unauthorized dupkcabon or drsmbubon of the program, or any portion of t may resul n severe crvi or crmnal penates. and mi be prosecuted to the maximum ««tent possfcte under the lew Puc. 23.10. Страница приветствия мастера установки [ Ш EvtntRttftlrattonWtto Select Installation Address !■»■»■■ f\ ш> The rafler wi ratal EventflegstiabonWeb to the ktmbQ тЛ beaten To ratal to the web location, clck "Next" TorataltoaoWerent web location, enter it below У «fuel directory |EventRegstiebonWeb Ajjpicabon Poot ikg CI Аде Pool w\ глг WwffCL. 1 1 II N**> j i Puc. 23.1L Страница Select Installation Address Contirm Installation Ш> The nsialer is ready to ratal EventPegotrahonWeb on you computet Ckck ■Next" to start the ratalarion 1 *Ш> Рис. 23.12. Страница Confirm Installation
748 Часть III. Программирование для Web 4. Начнется процесс установки, о протекании которого будет свидетельствовать панель индикации хода выполнения, как показано на рис. 23.13. 5. После успешной установки откроется страница Installation Complete (Установка завершена), как показано на рис. 23.14. Щелкните на кнопке Close (Закрыть). 6. Теперь можно попробовать запустить Web-сайт из нового виртуального каталога. [ jf EwntftogtotrattoWWb Installing EventRegistrationWeb : EvertRegrclratonWebisbttng™'**^ Please wait. С*Ы < Back NM > Рис. 23.13. Страница хода установки приложения Installation Complete : Е ventRegistrationWeb has been successful instated i Cfck "Close" »o ex* Carvel ' Etack [ do» Puc. 23.14. Страница Installation Complete 0>
Глава 23. Развертывание Web-приложений 749 Резюме В этой главе были описаны различные варианты для развертывания Web-приложений. Средство Copy Web Site позволяет копировать файлы на Web-серверы за счет использования общих файловых ресурсов, FTP и FrontPage Server Extensions. Синхронизация файлов может происходить в обоих направлениях. Публикация Web-сайтов является таким подходом, при котором в существующие виртуальные каталоги развертываются лишь сборки, что уже исключает возможность доступа Web-администратора к исходным файлам. И, наконец, создание установочного проекта предусматривает не только копирование ASP.NET-страниц и сборок, но и также создание виртуального каталога с помощью IIS. В этой главе были рассмотрены следующие вопросы. □ Создание нового виртуального каталога с помощью IIS. □ Копирование Web-сайта с помощью Visual Studio. □ Публикация Web-сайта с помощью Visual Studio. □ Создание установочной программы для инсталляции Web-приложения. В следующей главе мы приступим к рассмотрению вопросов, связанных с доступом к данным, начав с файловой системы. Упражнения 1. В чем состоит разница между копированием и предварительной компиляцией Web-приложения? В каких случаях следует делать первое, а в каких — второе? 2. Когда предпочтительнее создавать установочную программу, чем выполнять копирование сайта? 3. Что необходимо обязательно делать перед тем, как выполнять публикацию Web- сайта? 4. Опубликуйте Web-службу из главы 21 в виртуальный каталог, сконфигурированный с помощью IIS.
ЧАСТЬ Доступ к данным В ЭТОЙ ЧАСТИ... Глава 24. Данные файловой системы Глава 25. XML Глава 26. Введение в LINQ Глава 27. LINQ to SQL Глава 28. ADO.NET и LINQ поверх DataSet Глава 29. LINQ to XML V
24 Данные файловой системы Чтение и запись файлов — важнейшие аспекты многих приложений .NET. В этой главе мы покажем, как использовать основные классы для создания, чтения и записи файлов, вместе с поддерживающими классами для манипуляций файловой системой из кода С#. Хотя мы не станем здесь детально рассматривать все эти классы, настоящая глава даст достаточное представление о концепциях и основах этих операций. Файлы могут быть замечательным средством хранения данных между экземплярами вашего приложения; их также можно использовать для передачи данных между приложениями. Конфигурационные установки пользователя и приложения могут сохраняться и извлекаться при следующем запуске вашего приложения. Текстовые файлы с разделителями, такие как файлы, содержащие строки, разделенные запятыми, применяются во многих унаследованных системах, и для того, чтобы взаимодействовать с этими системами, нужно знать, как работать с такими разделенными данными. Как будет показано, .NET Framework предоставляет все необходимые инструменты для эффективного использования файлов в приложениях. В частности, в этой главе будут рассматриваться следующие темы. □ Что такое поток и как .NET использует потоковые классы для доступа к файлам. □ Как использовать объект File для манипуляции файловой структурой. □ Как записывать в файл и читать из него. □ Как читать и записывать форматированные данные в файлы. □ Как читать и записывать сжатые файлы. □ Как сериализовывать и десериализовывать объекты. □ Как наблюдать за изменениями в файлах и каталогах.
Глава 24. Данные файловой системы 753 Потоки Весь ввод и вывод в .NET Framework подразумевает использование потоков. Поток (stream) — это абстрактное представление последовательного устройства. Последовательное устройство (serial device) — это нечто такое, что хранит данные в линейной манере и точно таким же образом обеспечивает доступ к ним: по одному байту за раз. Это устройство может быть дисковым файлом, сетевым каналом, местом в памяти или любым другим объектом, поддерживающим чтение и запись в линейном режиме. Сохранение устройства абстрактным означает, что лежащие в основе источник/ приемник данных могут быть скрыты. Такой уровень абстракции обеспечивает повторное использование кода и позволяет писать более обобщенные процедуры, потому что нет необходимости заботиться о действительной специфике передачи данных. Таким образом, сходный код может быть передан и повторно использован, когда приложение читает из входного файлового потока, сетевого входного потока или любого другого вида потока. Поскольку физические механизмы каждого устройства можно игнорировать, не приходится беспокоиться, например, о головках жесткого диска или выделении памяти, имея дело с файловым потоком. Существуют два типа потоков. □ Выходные. Выходные потоки используются, когда данные пишутся в некоторое внешнее место назначения, которым может быть физический дисковый файл, местоположение в сети, принтер или другая программа. Понимание потокового программирования открывает множество замечательных возможностей. Материал этой главы сконцентрирован на данных файловой системы, поэтому мы сосредоточимся только на записи в дисковые файлы. □ Входные. Входные потоки используются для чтения данных в память или переменные, к которым может обращаться ваша программа. Наиболее часто используемой формой входного потока, с которой вы работали до сих пор, была клавиатура. Входной поток может поступать почти из любого источника, но данная глава сосредоточена на чтении дисковых файлов. Концепции, применимые к чтению/записи дисковых файлов, применимы к большинству устройств, так что вы получите базовое представление о потоках и увидите в действии испытанный подход, применимый ко многим ситуациям. Классы ввода и вывода Пространство имен System. 10 содержит почти все классы, с которыми вы ознакомитесь в этой главе. System. 10 содержит классы для чтения данных из файлов и записи их в файлы, и вы можете ссылаться на эти пространства имен в приложении С#, чтобы получить доступ к этим классам без полной квалификации имен типов. Как показано на рис. 24.1, в System. 10 содержится относительно немного классов, но вы будете работать только с первичными классами, необходимыми для файлового ввода и вывода. Классы, описанные в этой главе, перечислены в табл. 24.1.
754 Часть IV. Доступ к данным Object (System) MarshalByRefObject (System) J T System (System.10) Directory (System.10) File (System.10) Path (System.10) FileStream (System.10) FileSystemlnfo (System. 10) Component (System) Filelnfo (System.10) Directorylnfo (System.10) FileSystemWatcher (System.10) TextReader (System. 10) TextWriter (System.10) Stream Reader (System.10) StreamWriter (System.10) Рис. 24.1. Классы ввода-вывода Таблица 24.1. Классы ввода-вывода, рассматриваемые в настоящей главе Класс Описание File Directory Path Filelnfo Directorylnfo FileSystemlnfo FileStream StreamReader Статический служебный класс, предоставляющий множество статических методов для перемещения, копирования и удаления файлов Статический служебный класс, предоставляющий множество статических методов для перемещения, копирования и удаления каталогов Служебный класс, используемый для манипулирования путевыми именами Представляет физический файл на диске, имеет методы для манипулирования этим файлом. Для любого, кто читает или пишет в этот файл, должен быть создан объект stream Представляет физический каталог на диске и имеет методы для манипулирования этим каталогом Служит базовым классом ДЛЯ Filelnfo и Directorylnfo, обеспечивая возможность работы с файлами и каталогами одновременно, используя полиморфизм Представляет файл, который может быть записан, прочитан или то и другое. Этот файл может быть записан или прочитан как синхронно, так и асинхронно Читает символьные данные из потока и может быть создан с использованием класса FileStream в качестве базового
Глава 24. Данные файловой системы 755 Окончание табл. 24.1 Класс Описание streamWriter Пишет символьные данные в поток и может быть создан с использованием класса FileStream в качестве базового FileSystemWatcher Наиболее развитый класс, который рассматривается в этой главе. Используется для мониторинга файлов и каталогов и представляет события, которые приложение может перехватить, когда в этих объектах происходят какие-то изменения. Этой функциональности всегда недоставало в программировании для Windows, но теперь .NET Framework значительно облегчает задачу реагирования на события файловой системы Вы также ознакомитесь с пространством имен System. 10.Compression, которое позволяет читать и писать в сжатые файлы, применяя либо сжатие GZIP, либо схему сжатия Deflate. □ Def lateStream. Представляет поток, в котором данные сжимаются автоматически при записи или автоматически распаковываются при чтении. Сжатие обеспечивается посредством алгоритма Deflate. □ GZipStream. Представляет поток, в котором данные сжимаются автоматически при записи или автоматически распаковываются при чтении. Сжатие обеспечивается алгоритмом GZIR И, наконец, вы изучите сериализацию объектов с использованием пространства имен System. Runtime. Serialization и его дочерних пространств имен. Прежде всего, мы рассмотрим класс BinaryFormatter из пространства имен System.Runtine. Serialization.Formatters.Binary, который позволяет сериализовывать объекты в потоки в двоичном виде, а затем десериализовывать их. Классы File и Directory Служебные классы File и Directory предоставляют множество статических методов для удивительно исчерпывающего манипулирования файлами и каталогами. Эти методы дают возможность перемещать файлы, опрашивать и обновлять атрибуты, а также создавать объекты FileStream. Как известно из главы 8, статические методы могут вызываться на классах без создания их экземпляров. Некоторые из наиболее полезных статических методов класса File перечислены в табл. 24.2. Таблица 24.2. Некоторые статические методы класса File Метод Описание Сору () Копирует файл из исходного местоположения в целевое Create () Создает файл по указанному пути Delete () Удалят файл Open () Возвращает объект FileStream, находящийся по указанному пути Move () Перемещает указанный файл в новое место. Вы можете специфицировать другое имя файла в его новом местоположении Некоторые полезные статические методы класса Directory описаны в табл. 24.3.
756 Часть IV. Доступ к данным Таблица 24.3. Некоторые статические методы класса Directory Метод Описание CreateDirectory () Создает каталог с указанным путем Delete () Удалят указанный каталог и все файлы внутри него GetDirectories () Возвращает массив объектов string, представляющих имена каталогов внутри указанного каталога GetFiles () Возвращает массив объектов string, представляющих имена файлов, находящихся в указанном каталоге GetFileSystemEntnes () Возвращает массив объектов string, представляющих имена файлов и каталогов внутри указанного каталога Move () Перемещает указанный каталог в новое место. Вы можете специфицировать другое имя папки в его новом местоположении Класс Filelnf о В отличие от класса File, класс Filelnf о не является статическим и не имеет статических методов. Этот класс полезен только при создании его экземпляров. Объект File Info представляет файл на диске или сетевом ресурсе, и вы можете создавать его, указывая путь к файлу: Filelnfo aFile = new Filelnfo(@"C:\Log.txt"); Поскольку на протяжении настоящей главы вы будете работать со строковым представлением пути файла, а это означает большое количество символов \ в строках, напоминаем, что символ @, предваряющий показанную выше строку, означает, что эта строка будет интерпретирована как литерал. Поэтому символ \ будет интерпретирован как \, а не как символ начала управляющей последовательности. Без префикса @ понадобилось бы применять \ \ вместо \, чтобы избежать интерпретации этого символа как начала управляющей последовательности. В этой главе мы постоянно будем использовать префикс @ со строками. Вы можете также передать имя каталога конструктору Filelnfo, хотя в практическом смысле это не слишком удобно, поскольку требует инициализации базового класса для Filelnfo, которым является FileSystemlnfo, информацией каталога, но ни один из методов Filelnfo или свойств, относящихся к файлам, не будет использован. Многие методы, представленные классом Filelnfo, подобны методам класса File, но поскольку File — статический класс, он требует строкового параметра, специфицирующего местоположение файла для каждого вызова метода. Поэтому показанные ниже вызовы делают одно и то же: Filelnfo aFile = new Filelnfo("Data.txt"); if (aFile.Exists) Console.WriteLme("File Exists"); // файл существует if (File.ExistsC'Data.txt") ) Console.WriteLme("File Exists"); // файл существует В этом коде выполняется проверка существования файла Data. txt. Обратите внимание, что здесь не специфицируется никакой информации о каталоге; это означает, что наличие файла проверяется только в текущем рабочем каталоге. Этот каталог — тот, что содержит приложение, вызвавшее данный код. Чуть позже мы рассмотрим это немного подробнее (в разделе "Путевые имена и относительные пути").
Глава 24. Данные файловой системы 757 Большинство методов Filelnfo отражают методы File, но на свой лад. В большинстве случаев не имеет значения, какую технику вы предпочтете, тем не менее, следующие критерии могут помочь выбрать более подходящую. □ Имеет смысл использовать методы статического класса File, если вы осуществляете единственный вызов метода — такой вызов будет быстрее, потому что .NET Framework не нужно проходить процесс создания экземпляра нового объекта с последующим вызовом его метода. □ Если ваше приложение выполняет несколько операций с файлом, то более оправдано создать экземпляр объекта Filelnfo и пользоваться его методами; это позволит сэкономить время, потому что объект будет уже ссылаться на правильный файл в файловой системе, в то время как статический класс вынужден находить его каждый раз заново. Класс Filelnfo также предоставляет свойства лежащего в основе файла, часть из которых позволяет выполнять его обновление. Многие из этих свойств унаследованы от FileSystemlnf о и потому применимы как к File, так и к Directory. Свойства класса FileSystemlnf о перечислены в табл. 24.4. Таблица 24.4. Свойства класса FileSystemlnf о Свойство Описание Attributes Получает или устанавливает атрибуты текущего файла или каталога, используя перечисление FileAttributes CreationTime Получает или устанавливает дату и время текущего файла Extension Извлекает расширение файла. Это свойство доступно только для чтения Exists Определяет, существует ли файл. Это доступное только для чтения абстрактное СВОЙСТВО, переопределенное в Filelnfo и Directorylnfo FullName Извлекает полный путь файла. Это свойство доступно только для чтения LastAccessTime Получает или устанавливает дату и время последнего обращения к текущему файлу LastwnteTime Получает или устанавливает дату и время последней записи в текущий файл Name Извлекает полный путь файла. Это доступное только для чтения абстрактное СВОЙСТВО, переопределенное в Filelnfo и Directorylnfo Свойства, специфичные для Filelnfo, описаны в табл. 24.5. Таблица 24.5. Свойства класса Filelnfo Свойство Описание Directory Извлекает объект Directorylnfo, представляющий каталог, который содержит текущий файл. Свойство только для чтения DirectoryName Возвращает путь каталога файла. Свойство только для чтения isReadOnly Ссылка на атрибут файла "только для чтения". Это свойство также доступно через Attributes Length Получает размер файла в байтах в виде значения типа long. Свойство только для чтения
758 Часть IV. Доступ к данным Объект File Info сам по себе не представляет поток. Чтобы прочесть или записать в файл, необходимо создать объект Stream. Объект File Info помогает в этом, предлагая несколько методов, возвращающих экземпляры объектов Stream. Класс Directorylnf о Класс Directorylnf о работает точно так же, как класс Filelnf о. Это объект, представляющий отдельный каталог на машине. Подобно классу Filelnf о, многие из вызовов методов дублируются между Directory и Directorylnfo. Все инструкции по выбору между File и Filelnf о также применимы к методам Directorylnfo. □ Если вы выполняете единственный вызов, используйте класс Directory. □ Если вы выполняете серию вызовов, используйте экземпляр объекта Directorylnfo. Класс Directorylnfo наследует большинство свойств от FileSystemlnfo, как и Filelnf о, хотя эти свойства оперируют каталогами, а не файлами. Есть также два специфичных для Directorylnfo свойства, показанные в табл. 24.6. Таблица 24.6. Свойства класса Directorylnfo Свойство Описание Parent Извлекает объект Directorylnfo, представляющий каталог, который содержит текущий каталог. Свойство только для чтения Root Извлекает объект Directorylnfo, представляющий корневой каталог текущего тома, например, с: \. Свойство только для чтения Путевые имена и относительные пути При указании путевого имени в коде .NET вы можете использовать как абсолютные, так и относительные путевые имена. Абсолютный путь явно специфицирует файл или каталог из известного местоположения, такого как привод С:. Примером этого может служить С: \Work\LogFile. txt — этот путь точно определяет местоположение файла, без какой-либо неоднозначности. Относительные пути определены относительно некоторого начального местоположения. Используя относительные путевые имена, не нужно указывать привод или некоторое известное местоположение. Вы говорите это раньше, указав, что текущий рабочий каталог будет начальной точкой, что является поведением по умолчанию для относительных путевых имен. Например, если приложение запускается в каталоге C:\Development\FileDemo и использует относительный путь LogFile.txt, это указывает на файл С: \Development\FileDemo\LogFile. txt. Чтобы перейти вверх по иерархии каталогов, используется строка . .. Таким образом, путь . . \Log. txt указывает на файл С: \Development\Log.txt. Как было сказано ранее, рабочий каталог изначально устанавливается там, откуда запущено приложение. Когда вы ведете разработку в Visual Studio или Visual Studio Express, это означает, что приложение находится несколькими каталогами ниже созданной вами папки проекта. Обычно оно располагается в Имя_Проекта\Ып\ВеЪид. Чтобы обратиться к файлу в корневой папке проекта, потребуется перейти на два каталога выше, указав префикс пути . . \ . . \ — вы не раз встретитесь с этим на протяжении настоящей главы.
Глава 24. Данные файловой системы 759 При необходимости можете определить рабочий каталог, используя Directory. GetCurrentDirectory (), или же установить его в новый путь с помощью Directory. SetCurrentDirectory(). Объект FileStream Объект FileStream представляет поток, указывающий на дисковый файл или сетевой путь. В то время как класс предоставляет методы для чтения и записи байт в файл, чаще всего для выполнения этих функций вы будете использовать StreamReader или StreamWriter. Это потому, что класс FileStream оперирует байтами и байтовыми массивами, а классы Stream— символьными данными. Работать с символьными данными проще, но некоторые операции, например, произвольный доступ к файлу (доступ к данным в некоторой точке посреди файла), могут выполняться только посредством объекта FileStream. Далее в этой главе вы узнаете об этом подробнее. Есть несколько способов создания объекта FileStream. Конструктор имеет множество различных перегрузок, но простейшая из них принимает два аргумента: имя файла и значение перечисления FileMode: FileStream aFile = new FileStream{filename, FileMode.Member); Перечисление FileMode состоит из нескольких членов, которые специфицируют способ открытия или создания файла. Вскоре вы увидите эти способы. Другой часто используемый конструктор выглядит так: FileStream aFile = new FileStream (filename, FileMode. Member, File Access. Member) ; Третий параметр— член перечисления FileAccess — задает способ специфицирования назначения потока. Члены перечисления FileAccess приведены в табл. 24.7. Таблица 24.7. Члены перечисления FileAccess Член Описание Read Открывает файл только для чтения Write Открывает файл только для записи ReadWrite Открывает файл только для чтения или записи Попытка выполнить действие, отличное от специфицированного членом перечисления FileAccess, приведет к генерации исключения. Это свойство часто используется в качестве способа варьировать доступ пользователя к файлу на основе его уровня авторизации. В версии конструктора FileStream, не использующего параметр-перечисление FileAccess, применяется значение по умолчанию, которым является FileAccess. ReadWrite. Члены перечисления FileMode показаны в табл. 24.8. Что в действительности происходит, когда каждое из этих значений используется, зависит от того, ссылается ли указанное имя файла на существующий файл. Обратите внимание, что элементы этой таблицы ссылаются на позицию в файле, на которую установлен поток при его создании; эта тема будет подробнее раскрыта в следующем разделе. Если не установлено иначе, поток устанавливается в начало файла.
760 Часть ГУ. Доступ к данным Таблица 24.8. Члены перечисления FileMode Член Поведение при существующем файле Поведение при отсутствии файла Append Файл открыт, поток установлен в конец файла. Может использоваться только в сочетании с FileAccess .Write Create Файл уничтожается и на его месте создается новый CreateNew Генерируется исключение Open Файл открывается, поток позиционируется в начало файла OpenCreate файл открывается, поток позиционируется в начало файла Truncate файл открывается и очищается. Поток позиционируется в начало файла. Исходная дата создания файла остается неизменной Создается новый файл. Может использоваться только в сочетании с FileAccess.Write Создается новый файл Создается новый файл Генерируется исключение Создается новый файл Генерируется исключение Оба класса — и File, и Filelnf о — предоставляют методы OpenRead () и OpenWrite (), облегчающие создание объектов FileStream. Первый открывает файл с доступом только для чтения, а второй позволяет доступ только для записи. Эти методы являются сокращениями, так что вам не нужно предоставлять всю необходимую информацию в форме параметров конструктора FileStream. Например, следующая строка кода открывает файл Data. txt с доступом только для чтения: FileStream aFile = File.OpenReadCData.txt"); Следующий код выполняет ту же функцию: Filelnfo aFilelnfo = new Filelnfo("Data.txt"); FileStream aFile = aFilelnfo.OpenRead(); Позиция в файле Класс FileStream поддерживает внутренний указатель файла, который указывает на местоположение внутри файла, где произойдет следующая операция чтения или записи. В большинстве случаев, когда файл открывается, этот указатель установлен на начало файла, но этот указатель можно модифицировать. Это позволит приложению читать или писать в любом месте внутри файла, что, в свою очередь, позволяет организовать произвольный доступ к файлу с возможностью перепрыгнуть непосредственно к определенному месту в файле. Это позволяет сэкономить массу времени при работе с очень большими файлами, поскольку вы можете мгновенно перемещаться в необходимое место. Метод, реализующий эту функциональность — Seek () — принимает два параметра. Первый параметр указывает, насколько далеко в байтах нужно переместить указатель файла. Второй параметр специфицирует, откуда начинать подсчет, в форме значения из перечисления SeekOrigin. Перечисление SeekOrigin содержит три значения: Begin, Center и End. Например, следующая строка переместит указатель файла к восьмому байту файла, начиная с самого первого в этом файле: aFile.Seek (8, SeekOrigin.Begin);
Глава 24. Данные файловой системы 761 Следующая строка переместит указатель файла на два байта вперед, начиная с текущей позиции. Если ее исполнить сразу после предыдущей строки, то указатель файла теперь будет указывать на десятый байт в файле: aFile.Seek B, SeekOrigm .Current) ; Когда вы читаете или пишете в файл, файловый указатель также изменяется. После прочтения 10 байт файловый указатель будет установлен на байт, находящийся после десятого прочитанного байта. Вы можете также специфицировать отрицательное значение смещения, которое может быть скомбинировано со значением перечисления SeekOrigin.End, чтобы работать недалеко от конца файла. Следующий оператор переносит указатель файла на пять байтов от конца файла: aFile.Seek(-5, SeekOrigin.End); Файлы с возможностью доступа в такой манере иногда называют файлами произвольного доступа, потому что приложение может обращаться к любой позиции внутри файла. Классы Stream, описанные далее, обращаются к файлам последовательно и не позволяют манипулировать указателями файла подобным образом. Чтение данных Чтение данных с использованием класса FileStream не так просто, как посредством класса StreamReader, с которым вы ознакомитесь в конце главы. Это связано с тем, что класс FileStream имеет дело исключительно с "сырыми" байтами. Работа с "сырыми" байтами делает класс FileStream полезным для любого рода файлов данных, а не только для текстовых. Читая байтовые данные, объект FileStream может применяться для чтения таких файлов, как файлы изображений или звуковые файлы. Ценой гибкости является то, что вы не можете использовать FileStream для чтения данных непосредственно в строку, как это делается с классом StreamReader. Однако несколько классов преобразования позволяют довольно легко конвертировать байтовые массивы в символьные массивы и наоборот. Метод FileStream.Read () — главное средство доступа к данным из файла, на который указывает объект FileStream. Этот метод читает данные из файла и затем пишет их в массив byte. Существуют три параметра, первый из которых представляет позицию в массиве byte, куда нужно писать данные — обычно это ноль, чтобы начать писать данные из файла в начало массива. Последний параметр специфицирует количество байт для прочтения из файла. В следующем практическом занятии демонстрируется чтение данных из файла произвольного доступа. Файл, из которого вы будете читать — это на самом деле файл класса, который создается для примера. практическое занятие Чтение данных из файла произвольного доступа 1. Создайте новое консольное приложение по имени ReadFile и сохраните его в каталоге С:\BegVCSharp\Chapter24. 2. Добавьте следующую директиву using в начало файла Program, cs: using System; using System.Collections.Generic; using System.Ling; using System.Text; using System. 10;
762 Часть ГУ. Доступ к данным 3. Добавьте следующий код в метод Main (): static void Main(string[] args) { byte[] byData = new byte[200]; char[] charData = new Char [200]; try { FileStream aFile = new FileStream("../../Program.cs", FileMode.Open) aFile.SeekA13, SeekOrigm.Begin) ; aFile.Read(byData, 0, 200); } catch(IOException e) { Console.WriteLine("An 10 exception has been thrown!"); // Console.WriteLine("Сгенерировано исключение ввода-вывода!"); Console.WriteLine(e.ToString ()); Console.ReadKey(); return; } Decoder d = Encoding.UTF8.GetDecoder() ; d.GetChars(byData, 0, byData.Length, charData, 0) ; Console.WriteLine(charData); Console.ReadKey(); } 4. Запустите приложение. Результат показан на рис. 24.2. Рис. 24.2. Результат работы приложения ReadF'He Описание полученных результатов Это приложение открывает файл . cs для чтения. Оно делает это, переходя на два каталога выше по файловой структуре строкой . . в следующем операторе: FileStream aFile = new FileStream("../../Program.cs", FileMode.Open); Два оператора, реализующих собственно поиск и чтение из определенной точки файла, выглядят так: aFile.SeekA35, SeekOrigin.Begin); aFile.Read(byData, 0, 200); Первая строка перемещает файловый указатель в файле на байт номер 113. Это будет буква п в namespace в файле Program, cs; предшествующие ей 113 символов — это директивы using. Вторая строка читает следующие 200 символов в байтовый массив byData.
Глава 24. Данные файловой системы 763 Обратите внимание, что эти две строки ограничены блоками try. . . catch для обработки любых исключений, которые могут быть сгенерированы: try { aFile.SeekA35, SeekOrigin.Begin); aFile.Read(byData, 0,100); } catch (IOException e) try { aFile.SeekA35, SeekOrigin.Begin); aFile.Read(byData,0,100); } catch(IOException e) Почти все операции, включающие файловый ввод-вывод, могут сгенерировать исключение типа IOException. Весь рабочий код должен содержать обработку ошибок, особенно при работе с файловой системой. Все примеры этой главы включают базовую форму обработки ошибок. Как только вы получите массив byte из файла, нужно будет преобразовать его в символьный массив, чтобы его можно было отобразить на консоли. Для этого воспользуйтесь классом Decoder из пространства имет System.Text. Этот класс спроектирован для преобразования "сырых" байт в более полезные элементы, такие как символы: Decoder d = Encoding.UTF8.GetDecoder(); d.GetChars(byData, 0, byData.Length, charData, 0) ; Эти строки создают объект Decoder на основе схемы кодирования UTF-8, которая представляет собой схему кодирования Unicode. Затем вызывается метод GetChars (), принимающий массив байт и преобразующий его в массив символов. После того, как это будет сделано, символьный массив может быть выведен на консоль. Запись данных Процесс записи в файл произвольного доступа очень похож. Сначала должен быть создан байтовый массив; простейший способ сделать это состоит в том, чтобы сначала построить байтовый массив, который планируется записать в файл. Затем используйте объект Encoder для преобразования его в байтовый массив, почти так же, как вы используете объект Decoder. И, наконец, вызовите метод Write () для отправки массива в файл. Рассмотрим простой пример, демонстрирующий, как это делается. "t^™™™ Запись данных в файл произвольного доступа 1. Создайте консольное приложение по имени WriteFile и сохраните его в каталоге C:\BegVCSharp\Chapter24. 2. Добавьте следующую директиву using в файл Program, cs: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System. 10;
764 Часть ГУ. Доступ к данным 3. Добавьте следующий код в метод Main (): static void Main(string[] args) { byte[] byData; char[] charData; try { FileStream aFile = new FileStream("Temp.txt", FileMode.Create) ; charData = "My pink half of the drainpipe.".ToCharArray(); byData = new byte[charData.Length]; Encoder e = Encoding.UTF8.GetEncoder(); e.GetBytes(charData, 0, charData.Length, byData, 0, true); // Переместить файловый указатель в начало файла. aFile.Seek@, SeekOrigin.Begin); aFile.Write(byData, 0, byData.Length); } catch (IOException ex) { Console.WriteLine("An 10 exception has been thrown!"); Console.WriteLine (ex.ToString()); Console.ReadKey(); return; } } 4. Запустите приложение. Оно должно быстро выполниться и закрыться. 5. Перейдите к каталогу приложения — файл должен быть сохранен там, потому что применялся относительный путь. Это будет в папке WriteFile\bin\Debug. Откройте файл Temp.txt. Вы должны увидеть в файле текст, как показано на рис. 24.3. 'Temp- 1 |Mi ЕМ 1 ну pink ^^^^н Notepad Formal half of Vtow the —sra и* 1 dra1np1pe.| * 1 Рис. 24.3. Результирующий файл Temp, txt Описание полученных результатов Это приложение открывает файл в его собственном каталоге и пишет в него строку По структуре этот пример очень похож на предыдущий, за исключением того, что используется Write () вместо Read () и Encoder вместо Decoder. Следующая строка кода создает символьный массив с использованием статического метода ToCharArray () класса String. Поскольку все в С# является объектами, и текст My pink half of the drainpipe. — это на самом деле объект string (хотя и несколько странный), такие статические методы могут быть вызваны даже на строке символов: CharData = "My pink half of the drainpipe.".ToCharArray(); Следующие строки показывают, как преобразовать символьный массив в корректный байтовый массив, необходимый объекту FileStream: Encoder e = Endoding.UTF8.GetEncoder(); е.GetBytes(charData, 0, charData.Length, byData, 0, true);
Глава 24. Данные файловой системы 765 На этот раз объект Encoder создается на базе кодировки UTF-8. Unicode также используется и для декодирования, и на этот раз нужно закодировать символьные данные в корректный байтовый формат, прежде чем его можно будет записать в поток. Метод GetBytes () — место, где происходит вся магия. Он преобразует символьный массив в байтовый массив. GetBytes () принимает символьный массив в качестве первого параметра (charData в рассматриваемом примере) и индекс, указывающий на начало массива, в качестве второго параметра @ — для начала массива). Третий параметр— количество символов, подлежащих преобразованию (charData.Length — количество элементов в массиве charData). Четвертый параметр— байтовый массив, в который нужно поместить данные (byData), а пятый параметр— индекс, указывающий на начало записи в байтовом массиве @ — для начала массива byData). Шестой и последний параметр определяет, должен ли объект Encoder сбрасывать свое состояние после завершения работы. Это отражает тот факт, что объект Encoder запоминает место в памяти, где он остановился в байтовом массиве. Это предназначено для последовательных вызовов объекта Encoder, но бессмысленно при единственном его вызове. Финальный вызов Encoder должен установить этот параметр в true, чтобы очистить его память и освободить объект для сборки мусора. После этого остается лишь записать байтовый массив в FileStream, используя метод Write (): aFile.Seek@, SeekOrigin.Begin); aFile.Write(byData, 0, byData.Length); Объект StreamWriter Работа с массивами байт не приводит в восторг большинство людей; имея дело с объектом FileStream, вы можете предположить существование другого пути. И действительно: получив объект FileStream, вы обычно помещаете его в оболочку StreamWriter или StreamReader и используете его методы для манипулирования файлом. Если возможность переставлять файловый указатель в произвольную позицию не нужна, эти классы значительно облегчают работу с файлами. Класс StreamWriter позволяет записывать символы и строки в файл, оставляя классу внутреннее преобразование и запись объекта FileStream. Существует много способов создания объекта StreamWriter. Если объект FileStream уже имеется, можно использовать следующее для создания StreamWriter: FileStream aFile = new FileStream("Log.txt", FileMode.CreateNew); StreamWriter sw = new StreamWriter(aFile) ; Объект StreamWriter можно также создать непосредственно из файла: StreamWriter sw = new StreamWriter("Log.txt", true); Этот конструктор принимает имя файла и булевское значение, которое специфицирует, следует ли дописывать файл или создать новый. □ Если оно установлено в false, создается новый файл, или же существующий файл усекается до нулевого размера, а затем открывается. □ Если оно установлено в true, файл открывается, и его данные сохраняются. Если файла нет, он создается. В отличие от создания объекта FileStream, создание StreamWriter не предоставляет аналогичного набора опций. Помимо булевского значения для дописывания или создания нового файла, нет возможности специфицировать свойство FileMode, как это делалось с классом FileStream. He имея возможности установить свойство
766 Часть ГУ. Доступ к данным FileAccess, вы всегда имеете привилегии чтения/записи файла. Чтобы использовать любые из расширенных параметров, их сначала потребуется специфицировать в конструкторе FileStreeam, а затем создать StreamWriter из объекта FileStream, как это показано в следующем практическом занятии. Практическое занятие ВЫХОДНОЙ ПОТОК 1. Создайте консольное приложение по имени StreamWriter и сохраните его в каталоге С:\BegVCSharp\Chapter24. 2. Вы снова будете использовать пространство имен System. 10, поэтому добавьте следующую директиву using в начало файла Program, cs: using System; using System.Collections .Generic- using System.Ling; using System.Text; using System. 10; 3. Добавьте следующий код в метод Main (): static void Main(string [ ] args) { . try { FileStream aFile = new FileStream("Log.txt", FileMode.OpenOrCreate) ; StreamWriter sw = new StreamWriter(aFile); bool truth = true; // Записать данные в файл. sw.WriteLine("Hello to you."); sw.WriteLine ("It is now {0} and things are looking good.", DateTime.Now.ToLongDateStringO); sw.Write("More than that,"); sw.Write(" it's {0} that C# is fun.", truth); sw.Close (); } catch(IOException e) { Console.WriteLine("An 10 exception has been thrown!"); Console.WriteLine(e.ToString()); Console.ReadLine(); return; 4. Соберите и запустите проект. Если никаких ошибок не будет найдено, он должен быстро отработать и закрыться. Поскольку на консоли ничего не отображается, то и смотреть тут особо не на что. 5. Перейдите в каталог приложения и найдите файл Log.txt. Он расположен в папке StreamWrite\bin\Debug, так как указывался относительный путь. 6. Откройте файл. Вы должны увидеть то, что показано на рис. 24.4.
Глава 24. Данные файловой системы 767 — неПо to you. It Is now Friday. November 09, 2007 and things are looking good. More than that, It's True that c# 1s fun.| Рис. 24.4. Результирующий файл Log. txt Описание полученных результатов Это простое приложение демонстрирует два наиболее важных метода класса StreamWriter — Write () и WriteLine (). Оба они имеют множество перегруженных версий для выполнения более развитого файлового вывода, но в этом примере используется базовый вывод строк. Метод WriteLine () пишет переданную строку, немедленно дополняя ее символом новой строки. Вы можете видеть в примере, что это заставляет следующую операцию записи начинаться с новой строки. Точно так же, как вы пишете форматированный вывод на консоль, это можно делать и при записи в файл. Например, вы можете вывести значения переменных в файл, используя стандартные параметры формата: sw.WriteLine("It is now {0} and things are looking good.", DateTime.Now.ToLongDateStringO); DateTime .Now хранит текущую дату; метод ToLongDateString () используется для преобразования этой даты в легко читаемую форму. Метод Write () просто пишет переданную строку в файл без добавления символа новой строки, позволяя записывать целые предложения или абзацы с использованием дополнительных операторов Write (): sw.Write("More than that,"); sw.Write (" it's {0} that C# is fun.", truth); Здесь, опять-таки, применяются параметры формата— на этот раз с вызовом Write (), — чтобы отобразить булевское значение truth; вы устанавливаете эту переменную в true ранее, и ее значение при форматировании автоматически преобразуется в строку "True". Метод Write () и параметры формата можно использовать для записи разделенных запятыми полей: [06%eKT_StreamWriter].Write("{0},{1},{2}", 100, "A nice product", 10.50); В более сложном примере данные могли бы поступать из базы данных или другого источника. Объект StreamReader Входные потоки используются для чтения данных из внешних источников. Часто это бывает файл на диске или где-то в сети, но помните, что этот источник может быть почти чем угодно, что посылает данные, например, сетевым приложением, Web- службой или даже консолью. Класс StreamReader — это то, что вы будете применять для чтения данных из файлов. Подобно классу StreamWriter, это обобщенный класс, который может использоваться с любым потоком. В следующем практическом занятии вы снова сконструируете объект FileStream, который будет указывать на правильный файл.
768 Часть ГУ. Доступ к данным Объекты StreamReader создаются почти так же, как объекты StreamWriter. Наиболее распространенный способ его создания — применение ранее созданного объекта FileStream: FileStream aFile = new FileStream("Log.txt", FileMode.Open); StreamReader sr = new StreamReader(aFile); Подобно StreamWriter, объект класса StreamReader может быть создан непосредственно из строки, содержащей путь к определенному файлу: StreamReader sr = new StreamReader("Log.txt") ; Практическое занятие ВХОДНОЙ ПОТОК 1. Создайте новое консольное приложение по имени StreamRead и сохраните его в каталоге С: \BegVCSharp\Chapter24. 2. Импортируйте пространство имен System. 10, поместив следующую строку кода в начало файла Program.cs: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System. 10; 3. Добавьте следующий код в метод Main (): static void Main(string[] args) { string strLine; try { FileStream aFile = new FileStream("Log.txt", FileMode.Open); StreamReader sr = new StreamReader(aFile); strLine = sr.ReadLine (); // Прочитать данные строка за строкой. while(strLine !=null) { Console.WriteLine(strLine) ; strLine = sr.ReadLine(); } sr.Close (); } catch(IOException e) { Console.WriteLine("An 10 exception has been thrown1"); Console.WriteLine(e.ToString()); return; } Console.ReadKey(); } 4. Скопируйте файл Log. txt, созданный в предыдущем примере, в каталог StreamRead\bin\Debug. Если файла по имени Log.txt нет, конструктор FileStream сгенерирует исключение. 5. Запустите приложение. Вы должны увидеть выведенное на консоль содержимое файла, как показано на рис. 24.5.
Глава 24. Данные файловой системы 769 Рис. 24.5. Результат работы приложения StreamRead Описание полученных результатов Это приложение очень похоже на предыдущее, но с очевидным отличием — на этот раз осуществляется чтение файла вместо записи в него. Как и ранее, вы должны импортировать пространство имен System. 10, чтобы иметь доступ к необходимым классам. Для чтения текста из файла применяется метод ReadLine (). Метод читает текст до тех пор, пока не встретит символ новой строки, и возвращает прочтенный текст в виде строки. Метод возвращает null, когда достигнут конец файла, что можно использовать для проверки наступления этого условия. Обратите внимание на применение цикла while; это гарантирует, что прочтенная строка не будет равна null, прежде чем выполнится любой код в теле цикла— таким образом, будет отображено только настоящее содержимое файла: strLine = sr.ReadLine () ; while(strLine != null) { Console.WriteLine(strLine); strLine = sr.ReadLine(); } Чтение данных Метод ReadLine () — не единственный способ получить доступ к данным в файле. Класс StreamReader включает множество методов для чтения данных. Простейший из методов чтения — Read (). Он возвращает следующий символ из потока как положительное целое число или -1, если достигнут конец потока. Это значение может быть преобразовано в символ с использованием служебного класса Convert. В предыдущем примере основные части программы можно было бы переписать так, как показано ниже: StreamReader sr = new StreamReader(aFile); int nChar; nChar = sr .ReadO ; while(nChar != -1) { Console.Write(Convert.ToChar(nChar)); nChar = sr.Read(); } sr.Close (); Очень удобен при работе с файлами поменьше метод ReadToEnd (). Он читает весь файл целиком и возвращает его содержимое в виде строки. В этом случае предыдущее приложение может быть упрощено до следующего: StreamReader sr = new StreamReader(aFile); strLine = sr.ReadToEnd() ; Console.WriteLine(strLine); sr.Close();
770 Часть IV. Доступ к данным Хотя это может показаться простым и удобным, будьте осторожны. Читая все данные в строковый объект, вы помещаете все данные файла в память. В зависимости от размера этих данных это может быть крайне нежелательно. Если данные очень велики, лучше оставить их в файле и обращаться к ним методами StreamReader. Файлы с разделителями Файлы с разделителями — распространенная форма хранения данных, используемая во многих унаследованных системах. Если ваше приложение должно взаимодействовать с такой системой, вы часто будете встречаться с форматом данных с разделителями. Наиболее распространенный разделитель — запятая. Например, данные из электронных таблиц Excel, из базы данных Access или SQL Server могут быть экспортированы в файл с разделителем-запятой (comma-separated value — CVS). Вы уже видели, как используется класс StreamWriter для записи таких файлов. Читать их также несложно. Возможно, вы помните из главы 5 метод Split () класса String, применяемый для преобразования строки в массив на основе указанного символа-разделителя. Если вы специфицируете в качестве такого разделителя запятую, он создаст строковый массив правильной размерности, содержащий все данные из исходной строки с разделителем-запятой. Следующее практическое занятие демонстрирует, насколько это может оказаться полезным. В примере используются значения, разделенные запятыми, которые загружаются в объект List<Dictionary<string, string». Этот полезный пример носит достаточно общий характер, и вы можете счесть удобным применение такой техники в собственном приложении, если необходимо работать с разделенными запятыми значениями. Практическое занятие Значения, раЗДвЛвННЫе ЗЭПЯТЫМИ 1. Создайте новое консольное приложение по имени CommaValues и сохраните его в каталоге С: \BegVCSharp\Chapter24. 2. Поместите следующую строку в код в начало Proram.cs. Для работы с файлами нужно импортировать пространство имен System. 10: using System; using System.Collections .Generic- using System.Ling; using System.Text; using System. 10; 3. Добавьте следующий метод GetData () в тело Program, cs перед методом Main (): private static List<Dictionary<string/ string>> GetData (out List < string > columns) { string strLine; string[] strArray; char [ ] charArray = new char[] {','}; List<Dictionary<string, string» data = new List<Dictionary<string, string» (); columns = new List<string>(); try { FileStream aFile = new FileStream(@"..\..\SomeData.txt", FileMode.Open); StreamReader sr = new StreamReader(aFile); // Получить столбцы из первой строки.
Глава 24. Данные файловой системы 771 // Разделить строку данных в строковый массив. strLine = sr.ReadLine() ; strArray = strLine.Split(charArray); for (int x = 0; x <= strArray.GetUpperBound @); x++) { columns.Add(strArray[x]); } strLine = sr.ReadLine(); while (strLine != null) { // Разделить строку данных в строковый массив. strArray = strLine.Split(charArray); Dictionary<string, string> dataRow = new Dictionary<string, string>(); for (int x = 0; x <= strArray.GetUpperBound @); x++) { dataRow.Add(columns[x], strArray[x]); } data.Add(dataRow); strLine = sr.ReadLine (); } sr.Close(); return data; } catch (IOException ex) { Console.WriteLine("An 10 exception has been thrown!"); // исключение // ввода-вывода Console.WriteLine(ex.ToString()); Console.ReadLine(); return data; } } 4. Добавьте следующий код в метод Main (): static void Main(string[] args) { List<string> columns; List<Dictionary<string, string>> myData = GetData(out columns); foreach (string column in columns) { Console.Write("{0,-20}", column); } Console.WriteLine (); foreach (Dictionary<string, string> row in myData) { foreach (string column in columns) { Console.Write("{0,-20}", row[column]); } Console.WriteLine(); } Console.ReadKey(); } 5. Добавьте новый текстовый файл по имени SomeData.txt, выбрав элемент Text File (Текстовый файл) в диалоговом окне, доступном через пункт меню Project^Add New Item (ПроектеДобавить новый элемент).
772 Часть IV. Доступ к данным 6. Введите следующий текст в новый текстовый файл: ProductID,Name,Price 1,Spiky Pung,1000 2,Gloop Galloop Soup,25 4,Hat Sauce,12 7. Запустите приложение. Вы должны увидеть текст файла, выведенный на консоль, как показано на рис. 24.6. Рис. 24.6. Результат работы приложения CommaValues Описание полученных результатов Как и в предыдущем примере, это приложение читает файл строку за строкой в объект string. Однако поскольку известно, что этот файл содержит разделенные запятыми текстовые значения, они обрабатываются по-другому. Кроме того, в действительности прочитанные значения будут сохраняться в структуру данных. Сначала нужно взглянуть на сами разделенные запятыми данные: ProductID,Name,Price 1,Spiky Pung,1000 Первая строка содержит имена столбцов данных, а следующие за ней строки — сами данные. Таким образом, вы должны получить имена столбцов из первой строки файла и затем извлечь данные из остальных строк. Метод Get Data () объявлен статическим, поэтому можно вызвать этот метод без создания экземпляра вашего класса. Этот метод возвращает объект List<Dictionary<string, string>>, который вы создадите и затем наполните данными из разделенного запятыми текстового файла. Он также вернет объект List<string>, содержащий имена заголовков. Следующий код инициализирует эти объекты: List<Dictionary<string, string>> data = new List<Dictionary<string, string>>(); columns = new List<stnng> () ; columns будет содержать имена столбцов, взятые из первой строки текстового файла, разделенной запятыми, a data — значения последующих строк файла. Все начинается с создания объекта FileStream, а затем вокруг него конструируется StreamReader, как это делалось в предыдущих примерах. Теперь можно прочитать первую строку файла и создать массив строк из этой одной строки: strLine = sr.ReadLine (); strArray = strLine.Split(charArray); Метод Split (), который рассматривался в главе 5, принимает символьный массив— в данном случае содержащий только ' , ', чтобы strArray содержал массив строк, сформированный путем разбиения strLine по экземплярам ' , '. Поскольку в настоящий момент осуществляется чтение из первой строки файла, и эта строка содержит имена столбцов данных, необходимо пройтись по каждой строке в strArray и добавить ее в columns:
Глава 24. Данные файловой системы 773 for (int х = 0; х < = strArray.GetUpperBound(O) ; х++) { columns.Add(strArray[x]); } Теперь, имея имена столбцов ваших данных, можно приступить к чтению самих данных. Код, необходимый для этого, по существу тот же, что и в ранее приведенном примере StreamRead, за исключением присутствия кода, необходимого для добавления к data объекта Dictionary<string, string>: strLine = sr.ReadLine (); while (strLine != null) { // Разбиение строки данных на строковый массив. strArray = strLine.Split(charArray); Dictionary<stnng, string> dataRow = new Dictionary<string, stnng>(); for (int x = 0; x <= strArray .GetUpperBound @) ; x+ + ) { dataRow.Add(columns[x], strArray[x]); } data.Add(dataRow); strLine = sr.ReadLine (); } Для каждой строки файла создается новый объект Dictionary<string/ string>, который заполняется элементами этой строки. Каждое вхождение в эту коллекцию имеет ключ, соответствующий имени столбца, и значение, представляющее собой значение столбца этой строки. Ключи извлекаются из созданного ранее объекта columns и значений, поступающими из строкового массива, который получен из Split () для строки текста, извлеченной из файла данных. Прочтя все данные из файла, StreamReader закрывается, а данные возвращаются. Код в методе Main () получает данные из метода Get Data () в переменные myData и columns и отображает информацию на консоли. Сначала отображается имя каждого столбца: foreach (string column in columns) { Console.Write("{0,-20}", column); ) Console.WriteLine() ; Часть -20 форматной строки {0, -20} гарантирует, что отображаемое имя будет выровнено влево в столбце шириной 20 символов; это поможет форматировать вывод. И, наконец, осуществляется проход в цикле по объекту Dictionary<string, string> в коллекции myData и отображение значений в строке, каждый раз с использованием форматирующей строки для вывода: foreach (Dictionary<string, string> row in myData) { foreach (string column in columns) { Console.Write ("{0,-20}", row[column]); } Console.WriteLine(); } Как видите, извлечь осмысленные данные из файла со значениями, разделенными запятыми (CVS), с помощью .NET Framework достаточно просто. Эту технику также
774 Часть IV. Доступ к данным легко комбинировать с приемами доступа к данным, которые будут показаны в последующих главах, и это значит, что данными, полученными из файла CSV, можно манипулировать так же, как и любыми другими источниками данных (такими, как базы данных). Однако из файла CSV не извлекается никакой информации о типах данных. В настоящее время все данные трактуются как строки. Для бизнес-приложения масштаба предприятия следует выполнить дополнительный шаг добавления информации о типе к извлекаемым данным. В зависимости от конкретного приложения, это может поступать из дополнительной информации, хранимой в файле CSV, конфигурироваться вручную или же выводиться из строк файла. Даже несмотря на то, что XML, описанный в следующей главе — это превосходный метод хранения и транспортировки данных, файлы CSV все еще достаточно распространены, и останутся таковыми еще долгое время. Файлы, хранящие данные, разделенные запятыми, весьма лаконичны, и потому они меньше, чем их XML-аналоги. Чтение и запись сжатых файлов Часто при работе с файлами требуется значительное пространство на жестком диске. Это особенно верно для графических и звуковых файлов. Вероятно, вам встречались утилиты, позволяющие сжимать и распаковывать файлы, что удобно для их передачи по электронной почте. Пространство имен System. 10.Compression содержит классы, позволяющие сжимать файлы из кода с использованием алгоритма GZIP или Deflate — оба они общедоступны для любого применения. Однако при работе со сжатыми файлами есть еще кое-что помимо собственно сжатия. Коммерческие приложения позволяют множеству файлов размещаться в единственном сжатом файле и т.п. То, что вы увидите в настоящем разделе, много проще: сохранение текстовых данных в сжатом файле. Вряд ли удастся манипулировать этим файлов с помощью внешней утилиты, но этот файл будет намного меньше, чем его несжатый эквивалент! Два класса сжимающих потоков из пространства имен System. 10.Compression, которые здесь рассматриваются— DeflateStream и GZipStream— работают очень похоже. В обоих случаях они инициализируются существующим потоком, которым в случае файлов является объект FileStream. После этого их можно использовать вместе с StreamReader и StreamWriter, как и любой другой поток. Все, что потребуется специфицировать в дополнение — будет ли поток использоваться для сжатия (сохранения файлов) или распаковки (загрузки файлов), чтобы класс знал, что делать с переданными ему данными. Хорошая иллюстрация предлагается в следующем практическом занятии. ^^^^^:\ Сжатые данные 1. Создайте новое консольное приложение по имени Compressor и сохраните его в каталоге С: \BegVCSharp\Chapter24. 2. Поместите следующие строки кода в начало Program.cs. Вы должны импортировать пространство имен System. 10 для работы с файлами, и System. 10.Compression для использования сжимающих классов: using System; using System.Collections .Generic- using System.Linq; using System.Text; using System.10; using System. 10.Compression;
Глава 24. Данные файловой системы 775 3. Добавьте следующие методы в тело Program, cs перед методом Main (): static void SaveCompressedFile(string filename, string data) { FileStream fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write); GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Compress); StreamWriter writer = new StreamWriter(compressionStream); writer.Write(data) ; writer.Close() ; } static string LoadCompressedFile(string filename) { FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read); GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Decompress); StreamReader reader = new StreamReader(compressionStream); string data = reader.ReadToEnd(); reader.Close(); return data; } 4. Добавьте следующий код в метод Main (): static void Main(string [ ] args) { try { string filename = "compressedFile.txt"; Console.WriteLine( "Enter a string to compress (will be repeated 100 times):"); // Console.WriteLine( // "Введите строку для сжатия (она будет повторена 100 раз):"); string sourceString = Console.ReadLine(); StringBuilder sourceStringMultiplier = new StringBuilder(sourceString.Length * 100); for (int i = 0; i < 100; i++) { sourceStringMultiplier.Append(sourceString); } sourceString = sourceStringMultiplier.ToString (); Console.WriteLine("Source data is {0} bytes long.", sourceString.Length); // Console.WriteLine("Исходные данные имеют длину {0} байт.", // sourceString.Length); SaveCompressedFile(filename, sourceString); Console.WriteLine("\nData saved to {0}.", filename); // Console.WriteLine("ХпДаные сохранены в {0}.", filename); Filelnfo compressedFileData = new Filelnfo(filename); Console.WriteLine("Compressed file is {0} bytes long.", compressedFileData.Length) ; // Console.WriteLine("Сжатый файл имеют длину {0} байт.", // compressedFileData.Length); string recoveredString = LoadCompressedFile(filename); recoveredString = recoveredString.Substring@, recoveredString.Length / 100);
776 Часть ГУ. Доступ к данным Console.WriteLine(M\nRecovered data: {0}", recoveredString); // Console.WriteLine("ХпИзвлеченные данные: {0}", recoveredString); Console.ReadKey(); } catch (IOException ex) { Console.WriteLine("An 10 exception has been thrown!"); // исключение // ввода-вывода Console.WriteLine(ex.ToString () ) ; Console.ReadKey(); } } 5. Запустите приложение и введите достаточно длинную строку. Пример результата показан на рис. 24.7. |W*://AC:/B^»VCSh«rp/Chapter24/Compfwsor/Compfeseof/tHiVOebooK: omprea50f.EXE Рис. 24. 7. Результат работы приложения Compressor 6. Откройте файл compressedFile. txt в Notepad. Содержимое этого файла показано на рис. 24.8. compressedFile Notepad <а J лУ» l-%A/me{j6Jxat,o€ i$0©+iA"l5,ilG#)«-eeve]fTeiiy^P{iyi-P {-lyi-ojN'-flyAfd 1dlJ0e2'€*e-'~r',Pl6'N<<I-k§l]1e<»,- \ёАИъ>тЫи\МЬ\ЕЪпи2й Xdm[»< | ^MteKjuvOnsOC^E-eCy/'p'tufKlnveGl h; '0mMyiTKnc56Ev!u>z_t<56*f(myiI^'E5lllxau*a„n7 пО^^Авс/^аЛО^7 lb* иЛ0|эй<nhtMlKlHni^Daau^a^bu^yiiaaubU^eGtyi^Ycty'^AeGtyuAfiK^lD^7 AB^/^ddOM^lb* „fi0bu< fihLHl4l„ni_ciDSau#(lbbu#-yDaau6^fJ0GtyoaYi.ty">c7 A0GtyuA0G4b *>cy«&aa7"E | им Рис. 24.8. Содержимое файла compressedFile. txt Описание полученных результатов В этом примере были определены два метода для сохранения и загрузки сжатого текстового файла. Первый из них — SaveCompressedFile () — выглядит так: static void SaveCompressedFile(string filename, string data) { FileStream fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write); GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Compress); StreamWriter writer = new StreamWriter(compressionStream); writer.Write(data); writer.Close ();
Глава 24. Данные файловой системы 777 Код начинается с создания объекта FileStream и затем использует его для создания объекта GZipStream. Обратите внимание, что в приведенном коде все вхождения GZipStream можно было бы заменить DefalateStream— эти классы работают одинаково. Перечисление CompressionMode.Compress используется для специфицирования того, что данные должны быть сжаты. После этого применяется StreamWriter для записи данных в файл. Метод LoadCompressedFile () -зеркальное отображение метода SaveCompressedFile (). Вместо сохранения в файл с заданным именем, он загружает сжатый файл в строку: static string LoadCompressedFile(string filename) { FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read); GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Decompress); StreamReader reader = new StreamReader(compressionStream); string data = reader.ReadToEnd(); reader.Close(); return data; } Отличие состоит в том, чего и следовало ожидать — в разных значениях перечислений FileMode, FileAccess и CompressionMode для загрузки и распаковки данных, а также использовании StreamReader для получения распакованного текста из файла. Код в Main () — просто тест для этих методов. Он просто запрашивает строку, дублирует ее 100 раз, чтобы сделать все интереснее, сжимает в файл, а затем извлекает все это оттуда. В рассмотренном примере начальная строфа из "Рыцарей круглого стола", повторенная 100 раз, имеет длину в 17 800 символов, но в сжатом виде состоит всего лишь из 441 байт, что составляет степень сжатия 40:1. Правда, здесь присутствует элемент мошенничества — известно, что алгоритм GZIP особенно эффективно работает с повторяющимися данными, однако это иллюстрирует сжатие в действии. Вы также видели текст, хранимый в сжатом файле. Очевидно, что он нечитабелен, и это надо учитывать, если планируется разделять данные между приложениями. Однако поскольку файл был сжат известным алгоритмом, по крайней мере, другие приложения могут распаковывать его. Сериализованные объекты Как известно, приложения часто нуждаются в хранении данных на жестком диске. До сих пор в этой главе вы видели конструирование текста и файлов данных кусочек за кусочком, но часто такой способ не совсем удобен. Иногда лучше сохранять данные в той форме, в которой они используются, а именно — в виде объектов. .NET Framework представляет инфраструктуру для сериализации объектов в пространствах имен System.Runtime.Serialization и System.Runtime.Srialization. Formatters, со специфическими классами, реализующими эту инфраструктуру в пространствах имен, которые вложены в упомянутые два. Доступны две реализации: □ System.Runtime.Serialization.Formatters.Binary — это пространство имен содержит класс BinaryFormatter, который способен сериализовать объекты в двоичные данные и обратно; □ System. Runtime. Serialization. Formatters . Soap — это пространство имен содержит класс SoapFormatter, который способен сериализовать объекты в SOAP-формат XML-данных и наоборот.
778 Часть ГУ. Доступ к данным В этой главе рассматривается только BinaryFormatter, потому что вы еще не изучали XML-данные. Однако поскольку эти классы реализуют интерфейс I Formatter, большая часть последующего обсуждения касается обоих классов. Интерфейс I Formatter предоставляет методы, перечисленные в табл. 24.9. Таблица 24.9. Методы интерфейса IFormatter Метод Описание void Serialize (Stream stream, Сериализует объект source в ПОТОК stream object source) object Deserialize (Stream stream) Десериализует данные в stream и возвращает результирующий объект , Важным и удобным для этой главы является то, что эти методы работают с потоками. Это облегчает их подключение к механизмам доступа к файлам, уже показанным в этой главе — вы можете легко использовать объекты FileStream. Сериализация с применением BinaryFormatter проста: IFormatter serializer = new BinaryFormatter() ; serializer.Serialize(myStream, myObject); Десериализация не сложнее: IFormatter serializer = new BinaryFormatter() ; MyObjectType myNewObject = serializer.Deserialize(myStream) as MyObjectType; Очевидно, что нужны потоки и объекты, с которыми можно работать, но приведенный выше синтаксис подходит к большинству случаев. Следующее практическое занятие демонстрирует, как это реально работает. Сериализация объектов 1. Создайте новое консольное приложение по имени ObjectStore и сохраните его в каталоге С: \BegVCSharp\Chapter24. 2. Добавьте к проекту новый класс по имени Product и модифицируйте код следующим образом: namespace ObjectStore { public class Product { public long Id; public string Name; public double Price; [NonSerialized] string Notes; public Product(long id, string name, double price, string notes) { Id = id; Name = name; Price = price; Notes = notes;
Глава 24. Данные файловой системы 779 public override string ToStringO { return string.Format ("{0} : {1} (${2:F2}) {3}", Id, Name, Price, Notes); } } } 3. Поместите следующие строки кода в начало Program, cs. Вам нужно импортировать пространство имен System. 10 для работы с файлами, и другие пространства имен — для сериализации: using System; using System.Collections .Generic- using System.Linq; using System.Text; using System.10; using System.Runtime . Serializations- using Systern.Runtime.Serialization.Formatters.Binary; 4. Добавьте следующий код в метод Main () в Program, cs: static void Main(string [] args) { try { // Создать продукт. List<Product> products = new List<Product> () ; products.Add(new Product A, "Spiky Pung", 1000.0, "Good stuff.")); products.Add(new Product B, "Gloop Galloop Soup", 25.0, "Tasty.")); products.Add (new Product D, "Hat Sauce", 12.0, "One for the kids.")); Console.WriteLine("Products to save:"); // Console.WriteLine("Продукты для сохранения:"); foreach (Product product in products) { Console.WriteLine(product); } Console.WriteLine(); // Получить сериализатор. IFormatter serializer = new BinaryFormatter(); // Сериализовать продукты. FileStream saveFile = new FileStream("Products.bin", FileMode.Create, FileAccess.Write); serializer.Serialize(saveFile, products); saveFile.Close (); // Десериализовать продукты. FileStream loadFile = new FileStreamCProducts.bin", FileMode.Open, FileAccess.Read); List<Product> savedProducts = serializer.Deserialize(loadFile) as List<Product>; loadFile.Close (); Console.WriteLine("Products loaded:"); // Console.WriteLine("Загруженные продукты:"); foreach (Product product in savedProducts) { Console.WriteLine(product); } }
780 Часть IV. Доступ к данным catch (SerializationException e) { Console.WriteLine("A serialization exception has been thrown!"); // Console.WriteLine("Сгенерировано исключение сериализации!"); Console.WriteLine(e.Message); } catch (IOException e) { Console.WriteLine("An 10 exception has been thrown!"); // ошибка ввода-вывода Console.WriteLine(e.ToString()); } Console.ReadKey(); } 5. Запустите приложение. Результат должен получиться подобным показанному на рис. 24.9. Рис. 24.9. Исключение во время работы приложения ObjectStore 6. Модифицируйте код Product. cs следующим образом: namespace ObjectStore { [Serializable] public class Product { } 7. Запустите приложение снова. Результат представлен на рис. 24.10 Рис. 24.10. Результат работы приложения ObjectStore Откройте файл Products .bin в Notepad. Содержимое этого файла показано на рис. 24.11. ProAKtt.bm Notepad уууу », BObjectstore, verslon-l.o.0.0, culture-neutral. РиЬИскеуток en-null' ISyst em.col1ect1ons.Gener1 с.l1st 1[ [Object St ore.Product, obiectstore, version-!.. 0.0.0, Culture-neutral, Puol 1 скеуток en-nul 1 ] 11 -_11 ems |_s 1 zeo_ver s 1 orvi iObjectstore. Product []i ев i i i .i j '•object store. Product, ' | - lJ •ObjectStore.Producti ,ldJName|Pr1ce -, -• Spiky Pung C© | j , -o iGloop Gal loop Soup 90-i j Hat sauce (СИ Рис. 24.11. Содержимое файла Products. bin
Глава 24. Данные файловой системы 781 Описание полученных результатов В этом примере создается коллекция объектов Product, которая сохраняется на диске, а затем перезагружается оттуда. При первом запуске приложения, однако, было сгенерировано исключение, потому что объект Product не был помечен как cepucuiu- зуемый (serializable). .NET Framework требует помечать объекты как сериализуемые, чтобы можно было выполнять их сериализацию. На то есть несколько причин, включая следующие. □ Некоторые объекты не очень хорошо сериализуются. Они могут требовать ссылок на локальные данные, которые, например, существуют только до тех пор, пока находятся в памяти. □ Некоторые объекты могут содержать важные данные, которые не нужно сохранять небезопасным способом или передавать другому процессу. Как показано в примере, пометить объект сериализуемым очень просто — для этого применяется атрибут Serializable: namespace ObjectStore { [Serializable] public class Product { } Обратите внимание, что этот атрибут не наследуется производными классами. Он должен быть применен к каждому классу, который планируется подвергнуть сериали- зации. Также следует отметить, что класс List<T>, использованный для генерации коллекции объектов Product, имеет этот атрибут; в противном случае применение его к Product не позволит сделать коллекцию сериализуемой. Когда коллекция products успешно сериализована и десериализована (со второй попытки), становится очевидным другой важный факт. При десериализации восстанавливаются только поля Id, Name и Price. Это связано с использованием другого атрибута, а именно — NonSearialized: [NonSerialized] string Notes; Любой член класса может быть помечен этим атрибутом, и тогда он не будет сохраняться вместе с другими членами. Это может быть полезно, если, например, только одно поле или свойство содержит ответственные данные. Вы также видели результирующие сохраненные данные в этом примере. Некоторые данные здесь читабельны для человека; это может быть нежелательно или неожиданно. Класс BinaryFormatter не предпринимает серьезных попыток защитить данные от посторонних глаз. Конечно, поскольку вы используете потоки, относительно легко перехватить данные при сохранении их на диске или загрузке приложением и применить собственный маскирующий или шифрующий алгоритм. То же касается сжатия — применяя технику из предыдущего раздела, вы довольно легко можете сжать данные объекта при сохранении на диск. Тема сериализации включает еще многое, но изложенной информации достаточно, чтобы заложить основу. Одной из наиболее развитых техник, которые имеет смысл изучить, является специальная сериализация с применением интерфейса I Serializable, который позволяет точно настраивать, какие именно данные подлежат сериализации. Это может оказаться важным, например, при обновлении клас-
782 Часть IV. Доступ к данным сов в последующем выпуске. Изменение перечня членов, подлежащих сериализации, может привести к тому, что сохраненные предыдущей версией данные окажутся нечитабельными, если только вы не предусмотрите собственную логику сохранения и извлечения данных. Мониторинг структуры файла Иногда приложения должны делать нечто большее, чем просто запись файлов в файловую систему. Например, может быть важно знать, когда модифицируются файлы или каталоги. .NET Framework облегчает задачу создания таких приложений, которые делают это. Класс, помогающий в этом, называется FileStreamWatcher. Он предоставляет несколько событий, которые приложение может перехватить. В результате приложение может реагировать на события файловой системы. Базовая процедура использования FileStreamWatcher проста. Сначала вы должны установить несколько свойств, которые укажут, где нужно выполнять мониторинг, что отслеживать и когда должно возникнуть событие, которое приложение обработает. Затем вы предоставляете ему адреса обработчиков событий, чтобы они вызывались при наступлении какого-то существенного события. И, наконец, вы запускаете его и ждете событий. Свойства, которые должны быть установлены перед включением объекта FileStreamWatcher в работу, перечислены в табл. 24.10. Таблица 24.10. Свойства, которые должны быть установлены для объекта FileStreamWatcher Свойство Описание Path Должно быть установлено в местоположение или каталог, который нужно отслеживать NotifyFilter Комбинация значений перечисления NotifyFilters, специфицирующих, что именно нужно отслеживать в файлах, подверженных мониторингу. Это представляет свойства файла или папки, подверженной мониторингу. Если любое из указанных свойств изменяется, инициируется событие. Возможные значения перечисления — Attributes, CreationTime, DirectoryName, FileName, LastAccess, LastWrite, Security и Size. Обратите внимание, что их можно комбинировать двоичной операцией "ИЛИ" Filter Фильтр, специфицирующий файлы, которые нужно подвергать мониторингу, например, *.txt Установив все это, вы должны написать обработчики четырех событий: Changed, Created, Deleted и Renamed. Как было показано в главе 13, для этого нужно всего лишь разработать собственный метод и назначить его событию объекта. Назначая собственные обработчики этим методам, вы обеспечиваете возможность их вызова при возникновении события. Каждое событие инициируется, когда модифицируется файл или каталог, соответствующий Path, NotifyFilter и Filter. Установив названные свойства и события, установите свойство EnableRaisingEvents в true, чтобы запустить мониторинг. В следующем практическом занятии применяется FileSystemWatcher в простом клиентском приложении для слежения за выбранным каталогом.
Глава 24. Данные файловой системы 783 ———И—И—Ч——————■——И——1—1 Практическое занятие МОНИТОРИНГ фаЙЛОВОЙ СИСТвМЫ 1. Создайте новое консольное приложение по имени FileWatch и сохраните его в каталоге С:\BegVCSharp\Chapter24. 2. Установите различные свойства формы, как показано в табл. 24.11 Таблица 24.11. Значения свойств формы Свойство Установки FormBorderStyle MaximizeBox MinimizeBox Size StartPosition Text FixedDialog False False 302, 160 CenterScreen File Monitor 3. Используя информацию из табл. 24.12, добавьте на форму необходимые элементы управления и установите соответствующим образом их свойства. Таблица 24.12. Элементы управления формы Элемент управления Имя Местоположение Размер Текст TextBox Button Button Label txtLocation cmdBrowse cmdWatch lblWatch 8, 26 208, 24 88, 56 8, 104 184, 20 64, 24 80, 32 0, 13 Browse... Watch! Удостоверьтесь, что вы установили свойство Enabled кнопки cmdWatch в False, поскольку нельзя отслуживать файл до того, как он специфицирован, и установите свойство AutoSize метки lblWatch в True, чтобы видеть ее содержимое. Кроме того, добавьте на форму элемент управления OpenDialog, установив его свойство Name в FileDialog, свойство Filter — в All Files | * . *. В результате должна получиться форма, подобная представленной на рис. 24.12. Рис. 24.12. Результирующая форма
784 Часть IV. Доступ к данным 4. Теперь, когда форма выглядит хорошо, можете добавить некоторый код, чтобы выполнить некоторую работу. Для начала добавьте обычную директиву using для пространства имен System. 10 к существующему списку директив: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System. 10; 5. Добавьте класс FileSystemWatcher к классу Forml, вместе с делегатом для облегчения изменения текста lblWatch из разных потоков. Добавьте следующий код к Forml.cs: namespace FileWatch { partial class Forml : Form { // Объект наблюдения за файловой системой. private FileSystemWatcher watcher; private delegate void UpdateWatchTextDelegate (string newText); 6. Также необходимо добавить некоторый код в конструктор формы. Сразу после вызова метода InitializeComponent () поместите приведенный ниже код. Код необходим для инициализации объекта FileStreamWatcher и ассоциации событий с методами, которые будут создана впоследствии. public Forml() { InitializeComponent(); this.watcher = new System.10.FileSystemWatcher (); this.watcher.Deleted += new System.10.FileSystemEventHandler(this.OnDelete); this.watcher.Renamed += new System.10.RenamedEventHandler(this.OnRenamed); this.watcher.Changed += new System.10.FileSystemEventHandler(this.OnChanged); this.watcher.Created += new System.10.FileSystemEventHandler(this.OnCreate); } 7. Добавьте следующие пять методов в класс Forml. Первый метод используется для асинхронного обновления текста в lblWatch из потоков, которые запустят обработчики событий FileSystemWatcher. Другие методы представляют собой сами обработчики событий. // Служебный метод для обновления текста наблюдения. public void UpdateWatchText (string newText) { lblWatch.Text = newText; } // Определение обработчиков событий. public void OnChanged(object source, FileSystemEventArgs e) { try { StreamWriter sw = new StreamWriter("C:/FileLogs/Log.txt", true);
Глава 24. Данные файловой системы 785 sw.WriteLmeC'File: {0} {1}", e.FullPath, e.ChangeType.ToString()) ; sw.Close (); this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Wrote change event to log"); // Запись в журнал события изменения } catch (IOException) { this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Error Writing to log"); // Ошибка при записи в журнал } } public void OnRenamed(object source, RenamedEventArgs e) { try { StreamWriter sw = new StreamWriter("C:/FileLogs/Log.txt", true); sw.WriteLine("File renamed from {0} to {1}", e.OldName, e.FullPath); sw.Close(); this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Wrote renamed event to log"); // Запись в журнал события переименования } catch (IOException) { this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Error Writing to log"); } } public void OnDelete(object source, FileSystemEventArgs e) { try { StreamWriter sw = new StreamWriter("C:/FileLogs/Log.txt", true); sw.WriteLine("File: {0} Deleted", e.FullPath); sw.Close(); this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Wrote delete event to log"); // Запись в журнал события удаления } catch (IOException) { this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Error Writing to log"); } } public void OnCreate(object source, FileSystemEventArgs e) { try { StreamWriter sw = new StreamWriter("C:/FileLogs/Log.txt", true); sw.WriteLine("File: {0} Created", e.FullPath); sw.Close() ; this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Wrote create event to log"); // Запись в журнал события создания } catch (IOException) { this.Beginlnvoke(new UpdateWatchTextDelegate(UpdateWatchText), "Error Writing to log"); } }
786 Часть IV. Доступ к данным 8. Добавьте обработчик событий Click для кнопки Browse. Код в этом обработчике событий открывает диалог Open File (Открытие файла), позволяя пользователю выбирать файл для мониторинга. Выполните двойной щелчок на кнопке Browse и введите следующий код: private void cmdBrowse_Click(object sender, EventArgs e) { if (FileDialog.ShowDialogO != DialogResult.Cancel ) { txtLocation.Text = FileDialog.FileName; cmdWatch.Enabled = true; } } Метод ShowDielogO возвращает значение перечисления DialogResult, отражающее способ выхода пользователя из диалога File Open. (Пользователь может щелкнуть на кнопке ОК или Cancel (Отмена).) Необходимо удостовериться, что пользователь не щелкнул на кнопке Cancel, поэтому перед сохранением выбора пользователя в TextBox результат вызова метода сравнивается со значением перечисления DialogResult .Cancel. И, наконец, свойство Enabled кнопки Watch устанавливается в true, чтобы можно было отслеживать файл. 9. Выполните ту же процедуру с кнопкой Watch. Добавьте следующий код для запуска FileSystemWatcher: private void cmdWatch_Click(object sender, EventArgs e) { watcher.Path =Path.GetDirectoryName(txtLocation.Text); watcher.Filter = Path.GetFileName(txtLocation.Text); watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size; lblWatch.Text = "Watching " + txtLocation.Text; // Начать наблюдение, watcher.EnableRaisingEvents = true; } 10. Убедитесь, что каталог FileLogs существует, чтобы можно было записывать в него данные. Добавьте следующий код в конструктор Forml, который проверит существование каталога, и создаст его в случае его отсутствия: public Forml() { Directorylnfo aDir = new Directorylnfo(@"C:\FileLogs"); if (!aDir.Exists) aDir.Create (); } 11. Создайте каталог по имени C:\TempWatch и файл в этом каталоге по имени temp.txt. 12. Запустите приложение. Если все пройдет успешно, щелкните на кнопке Browse (Обзор) и выберите файл С: \TempWatch\temp. txt. 13. Щелкните на кнопке Watch (Отслеживание) для начала отслеживания файла. Единственное изменение, которое вы увидите в вашем приложении — это элемент управления типа метки, показывающий наблюдаемый файл.
Глава 24. Данные файловой системы 787 14. Используя Windows Explorer, перейдите в каталог C:\TempWatch. Откройте файл temp. txt в редакторе Notepad, добавьте некоторый текст к этому файлу и сохраните его. 15. Переименуйте файл. 16. Теперь можете проверить журнальный файл, чтобы увидеть в нем записи о проведенных изменениях. Перейдите к C:\FileLogs\Log.txt и откройте файл в редакторе Notepad. Вы должны увидеть описание изменений в выбранном для наблюдения файле (рис. 24.13). i J) Log • Notepad ЕЖИНТ I Hto EsU Formal Vtow Hatp Щ I JFlle: c:\fempwatch\temp.txt changed * 1 Щ I File: c:\Tempwatch\temp.txt changed I I File renamed from temp.txt to c:\Tempwatch\RenamedTextDoc.txt ш V—^— '" '■'' ■■ ■' ' # Рис. 24.13. Содержимое файла Log. txt Описание полученных результатов Это приложение весьма просто, однако оно демонстрирует работу класса FileSystemWatcher. Поэкспериментируйте со строкой, которую вы помещаете в текстовое поле txtLocation. Если вы укажете для каталога *. *, будут отслеживаться все изменения в каталоге. Большая часть кода приложения касается настройки объекта FileSystemWatcher для наблюдения за корректным местоположением: watcher.Path =Path.GetDirectoryName(txtLocation.Text); watcher.Filter = Path.GetFileName(txtLocation.Text); watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size; lblWatch.Text = "Watching " + txtLocation.Text; // Начать наблюдение. watcher.EnableRaisingEvents = true; Сначала код устанавливает путь к каталогу для мониторинга. Он использует новый объект, который вы еще не видели, а именно— System. 10. Path. Это статический класс, во многом подобный статическому объекту File, который предоставляет множество статических методов для манипулирования и извлекает информацию из строк, указывающих местоположение файлов. Сначала он применяется для извлечения имени каталога, которое пользователь ввел в текстовое поле, с помощью метода GetDirectoryName(). Следующая строка устанавливает фильтр на объекте. Это может быть действительный файл — в этом случае будет отслеживаться только этот файл. Или же это может быть нечто вроде * . txt, и тогда будут отслеживаться все файлы . txt в указанном каталоге. Опять-таки, для извлечения информации из указанного местоположения файла применяется статический объект Path. NotifyFilter— это комбинация значений перечисления NotifyFilters, которые специфицируют, из чего именно состоит изменение. В данном примере указано, что если изменяется временная метка последней операции записи, имя или размер файла, приложение получит извещение об изменении. После обновления пользовательского интерфейса свойство EnableRaisingEvents устанавливается в true для запуска мониторинга.
788 Часть IV. Доступ к данным Однако перед этим потребуется создать объект и установить обработчики событий: this.watcher = new System.10.FileSystemWatcher() ; this.watcher.Deleted += new System.10.FileSystemEventHandler(this.OnDelete) ; this.watcher.Renamed += new System.10.RenamedEventHandler(this.OnRenamed); this.watcher.Changed += new System.10.FileSystemEventHandler(this.OnChanged); this.watcher.Created += new System.10.FileSystemEventHandler(this.OnCreate); Подобным образом обработчики событий для наблюдаемого объекта привязываются к созданным приватным методам. Здесь получаются обработчики для событий, связанных с наблюдаемым объектом, когда файл удаляется, переименовывается, изменяется или создается. В ваших собственных методах вы сами решаете, как обрабатывать эти события. Обратите внимание, что вы получаете уведомление после возникновения события. В настоящих методах обработки событий вы просто записываете сообщение о событии в журнальный файл. Очевидно, можно запрограммировать и более изощренную реакцию на событие, в зависимости от конкретного приложения. Когда в каталог добавляется файл, вы можете переместить его куда-то еще или прочитать содержимое и запустить новый процесс на основе прочитанной информации. Возможности практически безграничны! Резюме В этой главе вы узнали о потоках и о том, почему они используются в .NET Framework для доступа к файлам и другим последовательным устройствам. Вы познакомились с базовыми классами в пространстве имен System. 10, включая следующие: □ File □ Filelnfo □ FileStream Вы видели, что класс File предлагает множество методов для перемещения, копирования и удаления файлов, a Filelnfo представляет физический файл на диске и предоставляет методы манипулирования этим файлом. Объект FileStream представляет файл, куда может быть выполнена запись, который может быть прочитан, либо то и другое. Вы также ознакомились с классами StreamReader и StreamWriter и увидели, насколько они полезны для записи потоков, а также научились читать и писать файлы произвольного доступа с использованием класса FileStream. Основываясь на полученных знаниях, вы использовали классы из пространства имен System. 10.Compression для сжатия потоков при записи их на диск, а также изучили сериализацию объектов в файлы. И, наконец, вы построили целое приложение для слежения за файлами и каталогами, используя для этого класс FileSystemWatcher. В этой главе рассмотрены следующие вопросы. □ Как открывать файл, читать из него и писать в файл. □ Разница между классами StreamWriter и StreamReader, с одной стороны, и классом FileStream— с другой. □ Как работать с файлами с разделителями для заполнения структур данных. □ Сжимающие и распаковывающие потоки. □ Как сериализовать и десериализовать объекты. □ Мониторинг файловой системы классом FileSystemWatcher.
Глава 24. Данные файловой системы 789 Упражнения 1. Какие пространства имен нужны приложению для работы с файлами? 2. Когда для записи файла следует использовать объект File St ream вместо объекта StreamWriter? 3. Какие методы класса StreamWriter позволяют читать данные из файлов, и что делает каждый их них? 4. Какой класс вы использовали бы для сжатия потока с применением алгоритма Deflate? 5. Как предотвратить сериализацию созданного вами класса? 6. Какие события представляет класс FileSystemWatcher, и для чего они служат? 7. Модифицируйте приложение FileWatch, построенное в этой главе, добавив возможность включения и отключения мониторинга файловой системы, не выходя из приложения.
XML Расширяемый язык разметки (Extensible Markup Language — XML) представляет собой технологию, которая привлекла к себе огромное внимание за последние несколько лет. XML — не нов, и он определенно не был изобретен Microsoft для использования в среде .NET, но в Microsoft рано оценили возможности, которые открывает этот язык для разработки. Как вы убедитесь, он охватывает широчайший круг применений — от описания конфигурации приложений до передачи информации между Web-службами. XML — это способ хранения данных в простом текстовом формате, что означает, что он может быть прочитан почти на любом компьютере. Как уже было показано в предыдущих главах, посвященных программированию для Web, это делает его непревзойденным форматом для передачи данных через Internet. Его даже несложно читать и человеку! Детали XML могут оказаться очень сложными, поэтому мы не будем в них слишком углубляться. Однако базовый формат очень прост, и большинство задач даже не требуют глубоких знаний XML, поскольку Visual Studio обычно берет на себя большую часть работы, и вам редко придется писать XML-документы вручную. С учетом сказанного, XML чрезвычайно важен в мире .NET, поскольку используется в качестве формата по умолчанию для передачи данных, а потому важно понимать хотя бы его основы. В этой главе будут рассматриваться следующие темы. □ Структура и элементы XML. □ Схема XML. □ Использование XML в приложениях. Более детальную информацию по XML вы найдете в книге Дэвида Хантера и др., "XML. Базовый курс, 4-е издание" (изд. "Диалектика", 2009г.).
Глава 25. XML 791 Документы XML Полный набор данных в XML известен как документ XML. Документом XML может быть физический файл на компьютере или просто строка в памяти. Однако он должен быть завершен сам по себе, и должен следовать определенным правилам (которые будут описаны ниже). Документ XML состоит из множества различных частей. Наиболее важные из них — элементы XML, которые содержат сами данные документа. Элементы XML Элементы XML состоит из открывающего дескриптора (имени элемента, заключенного в угловые скобки, например, <myElement>), данных внутри элемента и закрывающего дескриптора (такого же, как открывающий дескриптор, но с ведущим слэшем после открывающей скобки: </myElement>). Например, определить элемент для хранения заголовка книги можно следующим образом: <book>Tristram Shandy</book> Если вы хотя бы немного знаете HTML, вам может показаться, что это очень похоже — и вы будете правы. Фактически HTML и XML разделяют почти один и тот же синтаксис. Главное отличие в том, что XML не имеет предопределенных элементов — вы сами выбираете имена для своих элементов, так что ничто не ограничивает их количество. Наиболее важный момент, о котором нужно помнить, состоит в том, что XML, несмотря на его имя — это на самом деле вовсе не язык. Скорее, это стандарт для определения языков (известных как XML-приложения). Каждый язык имеет свой собственный отличный от других словарь — определенный набор элементов, которые могут применяться в документе, и структуру, которую могут принимать эти элементы. Как вы вскоре убедитесь, можно явно ограничивать допустимые элементы в документе XML. Альтернативно можно позволить любые элементы, и позволить самой программе, использующей документ, решать, какая должна быть структура. Имена элементов зависят от регистра, поэтому <book> и <ВООК> трактуются как разные элементы. Это значит, что если вы попытаетесь закрыть элемент <book>, используя закрывающий дескриптор, записанный в другом регистре (например, </ВООК>), то XML-документ не будет правильным. Программы, читающие XML-документы и анализирующие их индивидуальные элементы, известные как XML-анализаторы (XML parsers), отклоняют любой документ, содержащий неправильный XML. Элементы также могут содержать в себе другие элементы, так что вы можете модифицировать элемент <book> таким образом, чтобы включить информацию об авторе наряду в заголовком книги в двух подэлементах: <book> <title>Tristram Shandy</title> <author>Lawrence Sterne</author> </book> Перекрытие элементов не допускается, поэтому подэлементы должны закрываться перед появлением закрывающего дескриптора родительского элемента. Это значит, например, что нельзя сделать так: <book> <title>TristramShandy <author>LawrenceSterne </titlex/author> </book>
792 Часть ГУ. Доступ к данным Это некорректно, поскольку элемент <author> открыт внутри элемента <title>, но закрывающий дескриптор </title> находится перед закрывающим дескриптором </author>. Существует одно исключение из правила, требующего, чтобы все элементы имели закрывающий дескриптор. Допускается иметь "пустые" элементы, не имеющие вложенных данных или текста. В этом случае можно просто добавлять закрывающий дескриптор немедленно после открывающего, как показано в предыдущем коде, или же использовать сокращенный синтаксис, добавляя слэш закрывающего дескриптора в конец открывающего: <book /> Это идентично такому синтаксису: <bookx/book> Атрибуты Наряду с хранением данных в теле элемента вы можете также сохранять их внутри атрибутов, которые добавляются к открывающему элемент дескриптору. Атрибуты имеют следующую форму: name="value" Здесь значение атрибута должно быть заключено в одиночные или двойные кавычки. Например: <booktitle="TristramShandy"x/book> или <booktitle=,TristramShandylx/book> Оба приведенных вариантов правильны, а следующий — нет: <book title=Tristram Shandy></book> И здесь вы можете удивиться — зачем нужны два способа хранения данных в XML? В чем разница между следующими двумя способами записи? <book> <title>Tristram Shandy</title> </book> и <booktitle="Tristram Shandy"></book> Фактически между ними и нет какого-то фундаментального отличия. Ни один, ни другой способ не дают большого преимущества перед вторым. Элементы — более удачный выбор, если есть шанс, что понадобится добавлять информацию об этой части данных позднее — вы всегда можете добавить подэлемент или атрибут к элементу, но не можете делать то же самое с атрибутами. Возможно, элементы более читабельны и более элегантны (но это — дело личного вкуса). В противоположность этому атрибуты потребляют меньше полосы пропускания, когда документ пересылается по сети без сжатия (при сжатии разница не велика), и удобны для хранения информации, которая не существенна каждому пользователю документа. Возможно, лучшим советом будет использовать и то, и другое, выбирая то, что наиболее удобно для вас, чтобы хранить определенный элемент данных, так что здесь не существует жестких правил.
Глава 25. XML 793 Объявление XML В дополнение к элементам и атрибутам XML-документы могут содержать множество других составных частей. Эти индивидуальные части документа XML известны, как узлы (nodes); элементы, текст внутри элементов и атрибуты — все это узлы документа XML. Многие из них важны только в том случае, если вы действительно хотите углубиться в XML. Однако один тип узла появляется почти в каждом документе XML. Это объявление XML, и если вы его включаете, оно должно находиться в первом узле документа. Объявление XML по своему формату подобно элементу, но имеет внутри себя вопросительные знаки и угловые скобки. Оно всегда имеет имя xml и всегда снабжено атрибутом по имени version; в настоящее время единственное допустимое значение для него— .0". Простейшая возможная форма XML-объявления такова: <?xml version="l.0"?> В феврале 2004 г. консорциум W3C (www. w3c. org) издал рекомендацию для XML 1.1, но на момент написания настоящей книги существовало лишь несколько, или вообще никаких реальных воплощений этой рекомендации. Дополнительно объявление XML также включает атрибуты encoding (со значением, указывающим символьный набор, который должен быть использован для прочтения документа, такой как "UTF-16", чтобы показать, что документ использует 16- битный символьный набор Unicode) и standalone (со значениями "yes" или "по", чтобы указать, зависит ли XML-документ от любых других файлов). Однако эти атрибуты не обязательны, и вы, вероятно, включите только атрибут version в собственные XML-файлы. Структура документа XML Одна из наиболее важных характеристик XML состоит в том, что он предоставляет способ структурирования данных, который существенно отличается от реляционных баз данных. Большинство современных систем управления базами данных хранят информацию в таблицах, которые связаны друг с другом через значения определенных столбцов. Таблицы хранят данные в строках и столбцах: каждая строка представляет отдельную запись, а каждый столбец в ней — определенный элемент данных в этой строке. В отличие от этого, данные XML структурированы иерархически, подобно организации папок и файлов в Windows Explorer. Каждый документ должен иметь единственный корневой элемент, внутри которого содержатся все элементы и текстовые данные. Если на вершине документа находится более одного элемента, то такой документ не считается правильным документом XML. Однако вы можете включить другие узлы XML в верхний уровень — особенно объявление XML. Таким образом, ниже представлен правильный документ XML: <?xml version=.0"?> <books> <book>Tristram Shandy</book> <book>Moby Dick</book> <book>Ulysses</book> </books> А этот фрагмент не является правильным документом XML: <?xml version="l.0"?> <book>Tristram Shandy</book> <book>Moby Dick</book> <book>Ulysses</book>
794 Часть IV. Доступ к данным Под корневым элементом предоставлена значительная свобода относительно структуры ваших данных. В отличие от реляционных данных, в которых каждая строка имеет одинаковое количество столбцов, здесь нет ограничений на число подэле- ментов, которые может содержать элемент. Вдобавок, хотя документы XML часто структурированы подобно реляционным данным, с элементом для каждой записи, документы XML не нуждаются ни в каких предопределенных структурах вообще. Это — одно из основных отличий между традиционными реляционными базами данных и XML. В то время как реляционные базы данных всегда определяют структуру информации перед добавлением каких-либо данных, информация может быть сохранена в XML без начальных накладных расходов, что очень удобно для хранения небольших блоков данных. Как вы вскоре убедитесь, вполне возможно представить структуру вашего XML, но в отличие от реляционных баз данных, никто не требует от вас указания этой структуры явным образом. Пространства имен XML Как известно из главы 9, любой может определять собственные классы С#, и любой может определять свои собственные элементы XML, что приводит к возникновению очевидной проблемы: как узнать, какие элементы к какому словарю относятся? Ответ — с помощью пространств имен. Точно так же, как вы определяете пространства имен для организации ваших типов С#, вы используете пространства имен XML для определения собственных словарей XML. Это позволяет включать элементы из множества различных словарей внутри единственного документа XML, не рискуя неправильно интерпретировать их, потому что (например) два разных словаря определяют элемент <customer>. Пространства имен XML могут быть достаточно сложными, поэтому в данном разделе мы не станем углубляться в детали. Однако базовый синтаксис прост. Определенные элементы или атрибуты ассоциированы с определенным пространством имен посредством префикса, за которым следует двоеточие. Например, <wrox:book> представляет элемент <book>, находящийся в пространстве имен wrox. Как вы узнаете, какое пространство имен представляет wrox? Чтобы этот подход работал, вы должны гарантировать уникальность каждого пространства имен. Простейший способ добиться этого — отобразить префиксы на нечто, уникальность чего известна, и именно так и делается. Где-то в вашем документе XML вы должны ассоциировать любые префиксы пространства имен с универсальным идентификатором ресурсов (Uniform Resource Identifier— URI). URI существуют в нескольких ипостасях, но наиболее распространенный вид — простой адрес Web наодобие www. wrox. com. Чтобы идентифицировать префикс с определенным пространством имен, используйте атрибут xmlns :prefix внутри элемента, установив это значение в уникальный URI, который идентифицирует пространство имен. Префикс может применяться в любом месте элемента, включая любые вложенные дочерние элементы: <?xml version=.0"?> <books> <book xmlns:wrox="http://www.wrox.com"> <wrox: title>Beginnmg C#</wrox: title> <wrox:author>Karli Watson</wrox:author> </book> </books> Здесь можно использовать префикс wrox: с элементами <title> и <author>, потому что они находятся внутри элемента <book>, где определен префикс. Однако если
Глава 25. XML 795 вы попытаетесь добавить этот префикс к элементу <books>, то XML станет неправильным, поскольку префикс для этого элемента не определен. Можно также определить пространство имен по умолчанию для элемента, используя атрибут xmlns: <?xml version=.0"?> <books> <book xmlns="http://www.wrox.com"> <title>Beginning Visual C# </title> <author>Karli Watson </author> <html:img src="begvcsharp.gif" xmlns:html="http://www.w3.org/1999/xhtml" /> </book> </books> Здесь пространство имен по умолчанию для элемента <book> определено как "http://www.wrox.com". Поэтому все, что находится внутри этого элемента, будет относиться к этому пространству имен, если только вы явно не специфицируете противоположное, добавив префикс другого пространства имен, как делаете это для элемента <img> (когда вы устанавливаете пространство имен, используемое XML-совместимыми документами HTML). Правильно оформленный и действительный XML До сих пор мы говорили о правильном (legal) XML. Фактически XML делает различие между двумя формами правильности. Документы, соответствующие всем правилам стандарта XML, считаются правильно оформленными (well-formed). Если документ XML не является правильно оформленным, то анализаторы не смогут интерпретировать его корректно, и отклонят такой документ. Чтобы быть правильно оформленным, документ может отвечать перечисленным ниже требованиям. □ Иметь один, и только один корневой элемент. □ Иметь закрывающие дескрипторы для каждого элемента (за исключением сокращенного синтаксиса, упомянутого ранее). □ Не иметь перекрывающихся элементов — все дочерние элементы должны быть полностью помещены внутри родителя. □ Иметь все атрибуты, заключенные в скобки. Это не полный список требований, но он высвечивает наиболее распространенные ошибки, допускаемые программистами — новичками в XML. Однако документы XML могут соответствовать всем этим правилам и, тем не менее, не быть действительными (valid). Вспомните, что ранее мы упоминали, что XML сам по себе не является языком, а скорее стандартом для определения приложений XML. Правильно оформленные документы XML просто соответствуют стандарту XML; чтобы быть действительными, они также должны отвечать всем правилам, специфицированным для приложений XML. Не все анализаторы проверяют действительность документов; те, что делают это, называются проверяющими анализаторами. Чтобы проверить, соответствует ли документ правилам приложения, сначала нужен способ определения этих правил. Проверка действительности документов XML XML поддерживает два пути определения того, какие элементы и атрибуты могут быть помещены в документ, и в каком порядке: определения типов документов (Document Type Definitions— DTD) и схемы. DTD используют отличный от XML синтаксис, унас-
796 Часть IV. Доступ к данным ледованный от родителя XML, и постепенно заменяются схемами. DTD не позволяют специфицировать типы данных элементов и атрибуты, а потому относительно не гибкие и не особенно широко используются в контексте .NET Framework. В противоположность этому, схемы используются часто; они позволяют специфицировать типы данных, и написаны в XML-совместимом синтаксисе. К сожалению, схемы очень сложны, и существуют разные формы для их определения, даже внутри мира .NET! Схемы Существуют два формата схем, поддерживаемых .NET — язык определения схем XML (XML Schema Definition — XSD) и сокращенные схемы XML-данных (XML-Data Reduced — XDR). Определение схем XDR— более старый стандарт, который принадлежит Microsoft; обычно он не используется и не распознается анализаторами, не принадлежащими Microsoft. XSD — открытый стандарт, рекомендованный W3C, и потому его определение присутствует здесь. Схемы могут как включаться внутрь XML- документа, так и храниться в отдельном файле. Вы должны быть хорошо знакомы с XML, прежде чем приступать к написанию схем, но стоит уметь распознавать главные элементы схемы, потому ниже будут изложены базовые принципы. Чтобы достичь понимания, мы рассмотрим простую схему XSD для простого документа XML, содержащую базовые детали о паре книг Wrox на тему С#. Этот документ XML можно найти в загружаемом коде, сопровождающем эту книгу (book.xml): <?xml version=.0"?> <books> <book> <title>Beginning Visual C#</title> <author>Karli Watson</author> <code>7582</code> </book> <book> <title>Professional C# 2nd Edition</title> <author>Simon Robinson</author> <code>7043</code> </book> </books> Схемы XSD Элементы в схемах XSD должны относиться к пространству имен http://www. w3.org/2001/XMLSchema. Если это пространство имен не будет включено, элементы схемы не будут распознаны. Чтобы ассоциировать документ XML со схемой XSD в другом файле, необходимо добавить элемент schemalocation к корневому элементу: <?xml version=.0"?> <books schemalocation="file://C:\Beginning Visual C#\Chapter 25\books.xsd"> </books> Рассмотрим кратко пример схемы XSD: <schema xmlns="http://www.w3.org/2001/XMLSchema"> <element name="books"> <complexType> <choice maxOccurs="unbounded"> <element name="book"> <complexType>
Глава 25. XML 797 <sequence> <element name="title" /> <element name="author" /> <element name="code" /> </sequence> </complexType> </element> </choice> <attribute name="schemalocation" /> </complexType> </element> </schema> Первое, что здесь бросается в глаза — это то, что пространство имен по умолчанию установлено в пространство имен XSD. Это сообщает анализатору, что элементы документа относятся к схеме. Если вы не специфицируете это пространство имен, то анализатор будет считать, что это просто нормальные элементы XML, и не поймет, что он должен применять их для проверки. Вся схема содержится внутри элемента под названием <schema> (с прописной "s"; помните, что регистр символов важен). Каждый элемент, который может появиться в документе, должен быть представлен элементом <element>. Этот элемент имеет атрибут name, указывающий имя элемента. Если элемент должен содержать в себе дочерние элементы, то вы должны предусмотреть для них дескрипторы <element> внутри элемента <contextType>. Внутри этого вы специфицируете, как именно дочерние элементы должны появляться. Например, вы используете элемент <choice>, чтобы специфицировать, что допускается любой выбор дочерних элементов, или <sequence>— чтобы указать, что дочерние элементы должны появляться в том же порядке, как они перечислены в схеме. Если элемент может появляться более одного раза (как элемент <book>), потребуется поместить атрибут maxOccurs внутрь родительского элемента. Установка его в "unbounded" означает, что элемент может появляться неограниченное количество раз. И, наконец, любые атрибуты должны быть представлены элементами <attribute>, включая ваш атрибут schemalocation, который сообщает анализатору, где искать схему. Поместите это после конца списка дочерних элементов. Теперь, ознакомившись с основами теории XML, в следующем практическом занятии вы можете приступить к созданию документов XML. К счастью, среда Visual Studio выполняет массу черновой работы. Она даже создает схему XSD на основе вашего документа XML, не требуя написания ни единой строки кода. практическое занятие Создание документа XML в Visual Studio Для создания документа XML выполните следующие шаги. 1. Откройте Visual Studio и выберите пункт меню File^New^File (Файл•=>Создать1^ Файл). Если вы не видите этого пункта, создайте новый проект, выполните щелчок правой кнопкой мыши на проекте в Solution Explorer и выберите добавление нового элемента. Затем выберите в диалоге XML File (Файл XML). 2. В появившемся диалоге New File (Новый файл) выберите XML File и щелкните на кнопке Open (Открыть). Visual Studio создаст новый документ XML. Как видно на рис. 25.1, Visual Studio добавляет объявление XML, дополненное атрибутом encoding (он также раскрашивает атрибуты и элементы, но это не очень хорошо видно на черно-белой иллюстрации).
798 Часть IV. Доступ к данным XMLRfeLxml <?xml version-»!.0" encodmg-l'utf-8,,?>| Рис. 25.1. Автоматическое добавление объявления XML 3. Сохраните файл, нажав <Ctrl+S> или выбрав пункт меню File^Save XMLFilel.xml (Файл^Сохранить XMLFilel .xml). Visual Studio запросит согласие на сохранение файла и его имя; сохраните его в папке Beginning Visual C#\Chapter25 как GhostStories.xml. 4. Переместите курсор к строке, находящейся под объявлением XML и введите текст <stories>. Обратите внимание, как Visual Studio автоматически добавит закрывающий дескриптор, как только вы введете знак "больше" для закрытия открывающего дескриптора. 5. Введите следующий код в XML-файл и щелкните на кнопке Save (Сохранить): <stories> « <story> <title>A House in Aungier Street</title> <author> <name>Sheridan Le Fanu</name> <nationality>Irish</nationality> </author> <rating>eerie</rating> </story> <story> <title>The Signalman</title> <author> <name>Charles Dickens</name> <nationality>English</nationality> </author> <rating>atmospheric</rating> </story> <story> <title>The Turn of the Screw</title> <author> <name>Henry James</name> <nationality>American</nationality> </author> <rating>a bit dull</rating> </story> </stories> 6. Теперь можно позволить Visual Studio создать схему, которая отвечает написанному вами XML. Сделайте это, выбрав пункт Create Schema (Создать схему) из меню XML. Сохраните результирующий файл XSD, щелкнув на Save, как GhostStories.XSD. 7. Вернитесь к файлу XML и наберите следующий XML перед завершающим дескриптором </stories>: <story> <title>Number 13 </title> <author> <name>MR James</name> <nationality>English</nationality> </author> <rating>mysterious</rating> </story>
Глава 25. XML 799 Обратите внимание, что теперь вы получаете подсказки IntelliSense, когда начинаете набирать открывающие дескрипторы. Это потому, что Visual Studio знает, что к набираемому файлу XML нужно подключить вновь созданную схему XSD. В Visual Studio можно установить связь между XML и одной или более схемами. Выберите пункт меню XML^Schemas (XML1^ Схемы). Это вызовет появление диалогового окна XML Schemas (Схемы XML), показанного на рис. 25.2. В начале длинного списка схем, которые распознает Visual Studio, вы увидите GhostStories. XSD. Слева от нее будет галочка зеленого цвета, указывающая, что схема используется текущим документом XML. XML Schemes •| 1 EdH утх* arrcrt XML ich— Ш XML Schemes used in • schema set provide validation and inteHnense Select the desired schema usage with the Use column dropdown IrsL ► Use Target Namespace ' ■1 LaaM L -- _ n the XML Editor. - FrleName * I GhostStories jesd Booksjad » I <ж I ШШМ] Remove j Cane* j j Рис. 25.2. Диалоговое окно XML Schemas Использование XML в приложении Теперь, когда вы узнали о том, как создаются документы XML, самое время применить полученные знания на практике. .NET Framework включает множество пространств имен и классов, которые облегчают задачу чтения, манипулирования и записи XML. В последующих разделах будет описано множество классов, и мы посмотрим, как можно применять их для создания и программного манипулирования XML. Объектная модель документа XML Объектная модель документа XML (XML Document Object Model — XML DOM) — это набор классов, используемых для манипулирования XML интуитивно понятным образом. DOM — возможно, не самый быстрый способ чтения данных XML, но как только вы разберетесь в отношениях между классами и элементами документа XML, то обнаружите, что эта модель очень легка в применении. Классы, составляющие DOM, находятся в пространстве имен System.Xml. В этом пространстве содержится несколько классов и пространств имен, но в данной главе мы сосредоточим внимание только на некоторых классах, которые позволяют легко манипулировать XML. Эти классы описаны в табл. 25.1. Класс XmlDocument Обычно первое, что должно делать ваше приложение с XML — это прочитать его из файла. Как описано в табл. 25.1, за это отвечает класс XmlDocument. Вы можете воспринимать XmlDocument как представление дискового файла, расположенное в памяти.
800 Часть ГУ. Доступ к данным Таблица 25.1. Важные классы из пространства имен System. Xml Класс Описание XmlNode Представляет отдельный узел в документе. Служит базовым для многих классов, описанных в этой главе. Если узел представляет корень документа XML, вы можете выполнить из него навигацию к любой позиции документа XmlDocument Расширяет класс XmlNode, но часто является первым объектом, который используется при работе с XML. Это связано с тем, что этот класс применяется для загрузки и сохранения данных с диска или еще откуда-либо XmlElement Представляет отдельный элемент в документе XML. XmlElement унаследован от XmlLinkedNode, который, в свою очередь, унаследован от XmlNode XmlAttribute Представляет отдельный атрибут. Подобно классу XmlDocument, унаследован от класса XmlNode Xml Text Представляет текст между открывающим и закрывающим дескрипторами Xmicomment Представляет специальный тип узлов, которые не предназначены служить частью документа, а лишь предоставляют информацию читателю о частях документа XmlNodeList Представляет коллекцию узлов Воспользовавшись классом XmlDocument для загрузки файла в память, вы можете получить корневой узел документа из него и начать чтение и манипуляции XML: using System.Xml; XmlDocument document = new XmlDocument(); document.Load(@"C:\Beginning Visual C#\Chapter 25\books.xml"); Две строки кода создают новый экземпляр класса XmlDocument и загружают в него файл books.xml. Помните, что класс XmlDocument находится в пространстве имен System.Xml, и потому вы должны вставить строку using System.Xml; в начало кода. В дополнение к загрузке и сохранению XML, класс XmlDocument также отвечает за поддержку самой структуры XML. Поэтому вы найдете в нем многочисленные методы, отвечающие за создание, изменение и удаление узлов дерева документа. Ниже мы рассмотрим некоторые из этих методов, но прежде чем представить эти методы правильно, вам следует узнать немного больше о другом классе — XmlElement. Класс XmlElement После того как документ загружен в память, с ним понадобится что-то делать. Свойство DocumentElement экземпляра XmlDocument, созданного в предыдущем коде, возвращает экземпляр XmlElement, представляющий корневой элемент XmlDocument. Этот элемент важен, поскольку обеспечивает доступ к каждому фрагменту информации в документе: XmlDocument document = new XmlDocument(); document.Load(@"C:\Beginning Visual C#\Chapter 25\books.xml") ; XmlElement element = document. DocumentElement ; После получения корневого элемента документа вы готовы к использованию этой информации. Класс XmlElement содержит методы и свойства для манипуляции узлами и атрибутами дерева. Давайте сначала рассмотрим свойства, предназначенные для навигации по элементам XML, которые перечислены в табл. 25.2.
Глава 25. XML 801 Таблица 25.2. Свойства, предназначенные для навигации по элементам XML Свойство Описание FirstChild LastChild ParentNode NextSibling HasChildNodes Возвращает первый дочерний элемент после данного. Если вы вспомните файл books .xml, приведенный ранее в этой главе, то в нем корневой элемент документа называется books, а следующий за ним — book. Таким образом, в этом документе первым дочерним по отношению к корневому элементу books является book: <books> @@la Root node <book> @@la FirstChild nodeFirstChild возвращает объект XmlNode, и вы должны проверить тип возвращенного узла, потому что маловероятно, что он всегда будет экземпляром XmlElement. В примере books дочерним по отношению к элементу Title фактически является узел XmlText, представляющий текст Beginning Visual C#. Работает точно так же, как свойство FirstChild, за исключением того, что возвращает последний дочерний элемент текущего узла. В случае примера books последним дочерним элементом узла books также будет узел book, НО представляющий книгу Professional C# 2nd Edition. <books> @@la Root node <book> @@la FirstChild <title>Beginning Visual C#</title> <author>Karli Watson</author> <code>7582</code> </book> <book> @@la LastChild <title>Professional C# 2nd Edition</title> <author>Simon Robinson</author> <code>7043</code> </book> </books> Возвращает родителя текущего узла. В примере books узел books является родителем обоих узлов book В то время как свойства FirstChild и LastChild возвращают листовой узел текущего узла, узел NextSibling возвращает следующий узел, имеющий того же родителя. В случае примера books это означает, что NextSibling элемента title вернет элемент author, а вызов NextSibling на нем вернет элемент code Позволяет проверить наличие дочерних элементов у текущего элемента, не получая значения от FirstChild и не проверяя его на равенство null Используя четыре свойства из табл. 25.2, можно проходить по всему документу Xml Document, как показано в следующем практическом занятии. Практическое занятие Цикл прОХОДЭ ПО ВСвМ уЗЛЭМ Документа XML В этом примере вы создадите небольшое приложение Windows Forms, которое пройдет циклом по всем узлам документа XML и напечатает имя элемента или, в случае элемента XmlText, текст, содержащийся в элементе. Этот код использует book. xml, который был представлен в разделе "Схемы" ранее в главе; если вы не создали этот файл во время изучения упомянутого раздела, найдите его в загружаемом коде.
802 Часть ГУ. Доступ к данным 1. Начните с создания нового приложения Windows Forms, выбрав пункт меню Flle^New1^Project (Файл^Создать1^Проект). В появившемся диалоговом окне выберите вариант Windows Application (Приложение Windows). Назовите проект LoopThroughXmlDocument и нажмите <Enter>. 2. Спроектируйте форму, как показано на рис. 25.3, перетащив на нее элементы управления ListBox и Button. Рис. 25.3. Форма приложения LoopThroughXmlDocument 3. Присвойте ListBox имя listBoxXmlNodes, а кнопке — buttonLoopThroughDocument. 4. Выполните двойной щелчок на кнопке и введите приведенный ниже код. Не забудьте добавить строку using System.Xml; в начало файла: private void buttonLoopThroughDocument_Click(object sender, EventArgs e) { // Очистить ListBox. listBoxXmlNodes.Items.Clear(); // Загрузить документ XML. XmlDocument document = new XmlDocument() ; document.Load(@"C:\Beginning Visual C#\Chapter 25\Books.xml") ; // Использовать рекурсию для циклического прохода по документу. RecurseXmlDocument((XmlNode)document.DocumentElement, 0) ; private void RecurseXmlDocument(XmlNode root, int indent) { // Если корень равен null, не делать ничего, if (root == null) return; if (root is XmlElement) // Корень имеет тип XmlElement { // Сначала напечатать имя. listBoxXmlNodes.Items.Add(root.Name.PadLeft(root.Name.Length + indent))j // Затем проверить наличие дочерних элементов, и если они есть, // вызвать этот метод снова для их распечатки, if (root.HasChildNodes) RecurseXmlDocument(root.FirstChild, indent + 2) ; // И, наконец, проверить, есть ли соседние узлы, и если они есть, // вызвать этот метолд снова для их распечатки, if (root.NextSibling != null) RecurseXmlDocument(root.NextSibling, indent);
Глава 25. XML 803 else if (root is XmlText) { // Печатать текст. string text = ((XmlText)root).Value; listBoxXmlNodes.Items.Add(text.PadLeft(text.Length + indent)); } } 5. Запустите приложение и щелкните на кнопке Loop (Цикл). Вы должны получить результат, показанный на рис. 25.4. %> XML Nodes 1 booki book Begmng Vteud C« author KartWataon code 7582 book ttte Professorial CM 2nd Edbon author Svnon Rooineon code 7043 C_j l£j 111 til так ; У Рис. 25.4. Результат работы приложения LoopThroughXmlDocument Вывод не выглядит как корректный XML и очевидно таковым не является. Следует отметить, что когда вы проходите по элементам документа XML, то не встречаете ни одного закрывающего дескриптора. Другими словами, вы не беспокоитесь о том, является ли текущий элемент открывающим или закрывающим дескриптором — XmlNode или XmlElement представляют узел целиком, а не являются представлениями дескриптора. Описание полученных результатов После щелчка на кнопке первое, что происходит— это вызов метода Load класса XmlDocument. Этот метод загружает XML из файла в экземпляр XmlDocument, который впоследствии используется для доступа к элементам XML. Затем вы вызываете метод, который позволяет пройти циклом по XML рекурсивно, передавая методу корневой узел документа XML. Корневой элемент получается из свойства DocumentElement класса XmlDocument. Помимо проверки на null переданного в метод RecurseXmlDocument параметра root, первая строка, на которую следует обратить внимание — это оператор if: if (root is XmlElement) // Корень имеет тип XmlElement { } else if (root is XmlText) { } Напомним, что операция is позволяет проверить тип объекта и возвращает true, если экземпляр относится к указанному типу. Даже несмотря на то, что узел root объявлен как XmlNode, это всего лишь базовый тип объектов, с которыми вы будете работать. Используя операцию is для проверки типа объектов, код может определить тип объекта во время выполнения и вести себя соответственно.
804 Часть IV. Доступ к данным Внутри двух блоков if добавляется соответствующий текст к списочному представлению. Вы должны знать тип текущего экземпляра root, потому что информация, которую требуется отобразить, получается по-разному для разных элементов: необходимо отобразить имена XmlElement и значения XmlText. Причина в том, что RecurseXmlDocument вызывается рекурсивно только в том случае, если узел является XmlElement, так как XmlText никогда не имеет дочерних элементов. Изменение значений узлов Прежде чем рассмотреть, как изменяется значение узла, важно понять, что значение узла очень редко бывает простым. Фактически вы столкнетесь с тем, что почти все классы, унаследованные от XmlNode, включают свойство по имени Value, которое редко возвращает что-то полезное. Хотя поначалу это несколько разочаровывает, потом вы обнаружите, что это достаточно логично. Рассмотрим ранее приведенный пример books: <books> <book> <title>Beginning Visual C#</title> <author> Karli Watson</author> <code>7582</code> </book> <book> </books> Каждая отдельная пара дескрипторов разрешается в DOM в виде узла. Вспомните, что когда вы проходили циклом по всем узлам документа, то встречали множество узлов XmlElement и три элемента XmlText. Узлами XmlElement в этом XML являются <books>, <book>, <title>, <author> и <code>. Узлы XmlText представляют собой текст, находящийся между начальным и конечным дескрипторами title, author и code. Хотя можно утверждать, что значениями узлов title, author и code является текст между дескрипторами, этот текст сам по себе является узлом, и как таковой, содержит в себе значение. Другие узлы, очевидно, не имеют никаких ассоциированных с ними значений помимо вложенных узлов. Если вы заглянете в конец кода ранее приведенного метода Recur seXml Document, то найдете там следующую строку в блоке if, когда текущим узлом является XmlText: string text = ( (XmlText)root) .Value; Здесь можно видеть, что свойство Value узла XmlText используется для получения значения узла. Узлы типа XmlElement возвращают null, если вы используете свойство Value, но можно получить информацию, находящуюся между начальным и конечным дескрипторами XmlElement, если вы используете один из двух других методов: InnerText и InnerXml. Это значит, что можно манипулировать значением узлов, используя два метода и свойство, которые описаны в табл. 25.3. Вставка новых узлов Теперь, когда вы увидели, как перемещаться по документу XML и даже получать значения его элементов, давайте рассмотрим, как изменить структуру документа, добавляя узлы к документу books, с которым мы имели дело до сих пор. Чтобы вставлять новые элементы в список, нужно ознакомиться с новыми методами классов XmlDocument и XmlNode, которые перечислены в табл. 25.4.
Глава 25. XML 805 Таблица 25.3. Средства для манипулирования значениями узлов Свойство Описание innerText Получает текст всех дочерних по отношению к текущему узлов и возвращает его в виде единственной соединенной строки. Это значит, что если вы получаете значение innerText из узла book в предыдущем XML, будет возвращена строка Beginning Visual C#Karli Watson7582. Если вы получите InnerText узла title, будет возвращено только Beginning Visual C#. Вы можете устанавливать текст, используя этот метод, но будьте осторожны, делая это, поскольку если вы установите текст неверного узла, то можете переписать информацию, которую не нужно было бы изменять. innerXml Возвращает текст, как и InnerText, но также возвращает и все дескрипторы. Таким образом, если вы получите значение innerXml узла book, то результатом будет следующая строка: <title>Beginmng Visual C#</title> <author>Karli Watson</author><code>7582</code> Как видите, это может быть довольно удобно, если есть строка, содержащая XML, которую необходимо включить непосредственно в документ XML. Однако в этом случае вы полностью отвечаете за содержимое этой строки, и если вы вставите неправильный XML, то приложение сгенерирует исключение. value Обеспечивает самый "чистый" способ манипуляции информацией в документе, но, как упоминалось ранее, только несколько классов в действительности возвращают нечто полезное при получении значения. Классы, возвращающие нужный текст, таковы: XmlText XmlComment XmlAttribute Таблица 25.4. Новые методы классов XmlDocument и XmlNode Метод Описание CreateNode CreateElement CreateAttribute CreateTextNode CreateComment Создает узел любого вида. Существуют три перегрузки этого метода, две из которых позволяют создавать узлы типа из перечисления XmlNodeType, и одна, позволяющая специфицировать тип узла для использования в виде строки. Если только вы не абсолютно уверены в том, что не укажете ошибочно в виде строки тип узла, отсутствующий в перечислении, настоятельно рекомендуется отдавать предпочтение перегрузкам, принимающим аргумент типа перечисления. Метод возвращает экземпляр XmlNode, который может быть явно приведен к соответствующему типу Версия CreateNode, создающая только узлы типа XmlDocument Версия CreateNode, создающая только узлы типа XmlAttribute Создает узлы типа XmlTextNode Этот метод включен здесь для того, чтобы описать типы узлов, которые могут быть созданы. Этот метод не создает узел, являющийся частью данных, представленных документом XML, а создает комментарий, предназначенный для чтения человеком. Можно также извлекать комментарии при чтении документа приложением
806 Часть ГУ. Доступ к данным Класс XmlDocument имеет методы, позволяющие создавать новые экземпляры XmlNode и XmlElement, что замечательно, поскольку оба эти класса имеют только защищенные (protected) конструкторы, а это значит, что вы не можете создать экземпляр каждого из них непосредственно операцией new. Методы, перечисленные в табл. 25.4, используются для создания самих узлов, но после вызова любого из них необходимо что-то с ними сделать, чтобы они представляли хоть какой-нибудь интерес. Немедленно после создания узлы не несут в себе дополнительной информации и не представляют никакой ценности для документа. Чтобы изменить это, вы должны использовать методы, которые находятся в классах- наследниках XmlNode (включая XmlDocument и XmlElement). Эти методы перечислены в табл. 25.5. Таблица 25.5. Методы классов-наследников XmlNode Метод Описание AppendChild Добавляет дочерний узел к узлу типа XmlNode или типу-наследнику. Помните, что добавляемый узел появляется в конце списка дочерних узлов того узла, на котором вызван метод. Если порядок дочерних узлов безразличен, то проблемы нет; если же порядок важен, необходимо добавлять узлы в корректной последовательности insertAf ter Контролирует точно место вставки нового узла. Метод принимает два параметра: первый — новый узел, и второй — узел, после которого нужно вставить новый. insertBef ore Работает точно так же, как InsertAf ter, за исключением того, что вставляет новый узел перед узлом, переданным во втором параметре В следующем практическом занятии представлен пример, построенный на базе предыдущего, который вставляет узел book в документ books.xml. В примере нет кода, очищающего документ (пока), поэтому, запустив его несколько раз, вы получите несколько идентичных узлов. Практическое занятие СОЗДЭНИе УЗЛОВ Выполните следующие шаги, чтобы добавить узел в документ books. xml. 1. Добавьте кнопку ниже существующей кнопки в форме и назовите ее buttonCreate Node. 2. Выполните двойной щелчок на новой кнопке и введите следующий код: private void buttonCreateNode_Click(object sender, EventArgs e) { // Загрузка документа XML. XmlDocument document = new XmlDocument() ; document.Load(@"C:\Beginning Visual C#\Chapter 25\Books.xml"); // Получение корневого элемента. XmlElement root = document.DocumentElement; // Создание новых узлов. XmlElement newBook = document.CreateElement("book") ; XmlElement newTitle = document.CreateElement("title"); XmlElement newAuthor = document.CreateElement("author") ; XmlElement newCode = document.CreateElement("code"); XmlText title = document.CreateTextNode("Beginning Visual C# 3rd Edition");
Глава 25. XML 807 XmlText author = document.CreateTextNode("Karli Watson et al"); XmlText code = document.CreateTextNode(234567890") ; XmlComment comment = document. CreateComment ("This book is the book you are reading"); // Вставка новых элементов. newBook.AppendChild(comment); newBook.AppendChild(newTitle); newBook.AppendChild(newAuthor); newBook.AppendChild(newCode); newTitle.AppendChild(title); newAuthor.AppendChild(author); newCode.AppendChild(code); root.InsertAfter(newBook, root.FirstChild); document.Save(@"C:\Beginning Visual C#\Chapter 25\Books.xml"); } 3. Добавьте следующий код в конце метода RecurseXmlDocument: else if (root is XmlComment) { // Напечатать текст. string text = root.Value; listBoxXmlNodes.Items.Add(text.PadLeft(text.Length + indent)); // Затем проверить, есть ли еще дочерние узлы, и если они есть, // снова вызвать метод для их печати, if (root.HasChildNodes) RecurseXmlDocument(root.FirstChild, indent + 2); // И, наконец, проверить наличие соседних узлов, и если они есть, // вызвать этот же метод для из распечатки, if (root.NextSibling != null) RecurseXmlDocument(root.NextSibling, indent); } 4. Запустите приложение и щелкните на кнопке Create Node (Создать узел). Затем щелкните на кнопке Loop (Цикл). Должно получиться диалоговое окно, показанное на рис. 25.5. .books book UHe Begmr>g Vbuai СД author Karl Watton 7582 i book The л the book you are reading ttte BepnngUaual 0*2008 author KariWataonetal code 1234567890 book Puc. 25.5. Добавление нового узла Есть один важный тип узла, который не был создан в предыдущем примере, а именно — XmlAttribute. Оставим это для упражнения в конце главы.
808 Часть ГУ. Доступ к данным Описание полученных результатов Код в методе buttonCreateNodeClick— это место, где происходит собственно создание узлов. Он создает восемь новых узлов, четыре из которых типа XmlElement, три — типа XmlText и один последний — типа XmlComment. Все узлы создаются методом, инкапсулирующим экземпляр XmlDocument. Узлы XmlElement создаются методом CreateElement, узлы XmlText создаются методом CreateTextNode, а узел XmlComment — методом CreateComment. После создания этих методов они еще должны быть вставлены в дерево XML. Это делается методом AppendChild на элементе, по отношению к которому новый узел должен стать дочерним. Единственное исключение— узел book, который является корневым узлом для всех новых узлов. Этот узел вставляется в дерево с помощью метода InsertAfter корневого объекта. В то время как все узлы, вставленные методом AppendChild, всегда становятся последними в списке дочерних узлов, InsertAfter позволяет должным образом позиционировать узел. Удаление узлов Теперь, когда вы знаете, как создаются новые узлы, все, что остается изучить — это как их удалить. Все классы, унаследованные от XmlNode, включают методы, перечисленные в табл. 25.6, которые позволяют удалять узлы из документа. Таблица 25.6. Методы удаления узлов Метод Описание RemoveAH Удаляет все дочерние узлы того узла, на котором метод вызван. Чуть менее очевидно то, что он также удаляет все атрибуты узла, поскольку они также считаются дочерними узлами RemoveChild Удаляет единственный дочерний узел того узла, на котором вызван. Метод возвращает узел, который был удален из документа, так что его можно при необходимости вставить заново. Следующее короткое практическое занятие расширяет приложение Windows Forms, которое создавалось на протяжении двух последних примеров, добавляя возможность удаления узлов. Пока оно только ищет последний экземпляр узла book и удаляет его. практическое МНПШ Удаление узлов Следующие шаги позволяют найти и удалить последний экземпляр узла book. 1. Добавьте новую кнопку под двумя существующими и назовите ее buttonDelete Node. Установите ее свойство Text в Delete Node. 2. Выполните двойной щелчок на новой кнопке и введите следующий код: private void buttonDeleteNode_Click(object sender, EventArgs e) { // Загрузить документ XML. XmlDocument document = new XmlDocument(); document.Load(@"C:\Beginning Visual C#\Chapter 25\Books.xml"); // Получить корневой элемент. XmlElement root = document.DocumentElement;
Глава 25. XML 809 // Найти узел. Корень - дескриптор <books>, поэтому его // последний дочерний узел и будет последним узлом <book>. if (root.HasChildNodes) { XmlNode book = root.LastChild; // Удалить дочерний узел. root.RemoveChild(book); // Сохранить документ обратно на диск. document.Save(@"C:\Beginning Visual C#\Chapter 25\Books.xml"); } } 3. Запустите приложение. Когда вы щелкнете на кнопке Delete Node (Удалить узел), а затем на кнопке Loop (Цикл), последний узел в дереве исчезнет. Описание полученных результатов После начальных шагов по загрузке XML в объект XmlDocument вы проверяете корневой элемент, чтобы увидеть, есть ли какие-то дочерние элементы в загруженном XML. Если они есть, вы используете свойство LastChild класса XmlElement для получения последнего дочернего узла. После этого удаление элемента осуществляется простым вызовом RemoveChild с передачей ему экземпляра элемента, который требуется удалить. В данном случае — это последний дочерний узел корневого элемента. Выборка узлов Теперь вы знаете, как передвигаться вперед и назад по документу XML, как манипулировать значениями документа, как создавать новые узлы и как затем удалять их. Остается в этой главе изложить только одно: как выбирать узлы напрямую, не обходя всего дерева. Класс XmlNode включает два метода, часто используемых для выбора узлов из документа, не обходя их все. Это SelectSingleNode и SelectNodes (см. табл. 25.7) — оба они применяют для выборки узлов специальный язык запросов, называемый XPath. Ниже вы узнаете о нем. Таблица 25.7. Методы выбора узлов Метод Описание SelectSingleNode Выбирает единственный узел. Если вы создаете запрос, выбирающий более одного узла, будет возвращен только первый из них SelectNodes Возвращает коллекцию узлов в форме класса XmlNodeList XPath XPath — это язык запросов для документов XML, подобно тому, как SQL — язык запросов для реляционных баз данных. Он используется двумя методами, описанными в табл. 25.7, для того, чтобы позволить избежать необходимости обхода всего дерева документа XML. О нем, однако, следует поговорить особо, поскольку его синтаксис не имеет ничего общего с SQL или С#. XPath — довольно обширный язык, и мы опишем здесь лишь малую его часть, достаточную для того, чтобы можно было приступить к извлечению узлов. Если вы хотите узнать больше, загляните на www. w3. org/TR/xpa th и на страницы справочной системы Visual Studio.
810 Часть ГУ. Доступ к данным Чтобы увидеть XPath в действии, мы воспользуемся несколько расширенной версией файла books. xml. Он будет приведен в практическом занятии раздела "Выборка узлов" далее в этой главе, и его также можно найти в загружаемом коде для этой главы под названием XPathQuery.xml. В табл. 25.8 перечислены некоторые из наиболее распространенных операций, которые можно выполнять с XPath. Если не установлено иное, пример запроса XPath выполняет выборку относительно узла, к которому он применен. Когда необходимо иметь имя узла, можете предполагать, что текущим узлом является <book> из документа XML. Таблица 25.8. Наиболее распространенных операций, которые можно выполнять с XPath Назначение Пример запроса XPath Выбрать текущий узел Выбрать родительский узел по отношению к текущему Выбрать все дочерние узлы текущего узла Выбрать все дочерние узлы с указанным именем, в данном случае — title Выбрать атрибут текущего узла Выбрать все атрибуты текущего узла Выбрать дочерний узел по индексу, в данном случае — второй узел author Выбрать все текстовые узлы текущего узла Выбрать одного или более внуков текущего узла Выбрать все узлы в документе с определенным именем, в данном случае — все узлы title Выбрать все узлы в документе с определенным именем и определенным именем родителя. В данном случае — узлы title с родителем book Выбрать все узлы в документе с определенным именем и определенным именем родителя, в данном случае — книгу с автором Jacob Hammer Pedersen Выбрать узел с определенным значением атрибута pages, в данном случае pages равен 1000 title @pages author[2] text{} author/text() //title //book/title //book[author='Jacob Hammer Pedersen1] //book[@pages='1000'] В следующем практическом занятии вы создадите небольшое приложение, которое позволит выполнить и увидеть результаты предопределенных запросов, а также вводить собственные запросы. Практическое зяи Выборка узлов Как упоминалось ранее, в этом примере используется новый файл XML по имени XPathQuery .XML, входящий в состав загружаемого кода. Разумеется, код можно ввести и самостоятельно: <?xml version=.0"?> <books> <book pages="944">
Глава 25. XML 811 <title>Beginning Visual C#</title> <author>Karli Watson</author> <author>Jacob Hammer Pedersen</author> <author>Christian Nagel</author> <author>David Espinosa</author> <author>Zach Greenvoss</author> <author>Jon D. Reid</author> <author>Matthew Reynolds</author> <author>Morgan Skinner</author> <author>Eric White</author> <code>7582</code> </book> <book pages=000"> <title>Beginning Visual C# 2008</title> <author>Karli Watson</author> <author>Jacob Hammer Pedersen</author> <author>Christian Nagel</author> <author>Jon D. Reid</author> <author>Morgan Skinner</author> <author>Eric White</author> <code>1234567890</code> </book> <book pages=272"> <title>Professional C# 2nd Edition</title> <author>Simon Robinson</author> <author>Scott Allen</author> <author>011ie Cornes</author> <author>Jay Glynn</author> <author>Zach Greenvoss</author> <author>Burton Harvey</author> <author>Christian Nagel</author> <author>Morgan Skinner</author> <author>Karli Watson</author> <code>7043</code> </book> </books> Сохраните этот XML-файл под именем XPathQuery.xml. He забудьте изменить путь к файлу в следующем коде. Выполните следующие шаги для создания приложения Windows Forms с возможностью выполнения запросов. 1. Создайте новое приложение Windows Forms и назовите его XmlQueryExample. 2. Создайте диалоговое окно, показанное на рис. 25.6. Присвойте элементу TextBox имя textBoxQuery. Все прочие элементы управления должны называться именем типа, за которым следует их свойство text (т.е. именем переключателя Set book as current (Установить узел book как текущий) будет radioButtonSetBook AsCurrent, а именем кнопки Close (Закрыть) — buttonClose). 3. Выполните щелчок правой кнопкой мыши на форме и выберите в контекстном меню пункт View Code (Просмотреть код). Включите следующую директиву using: using System.Xml;
812 Часть IV. Доступ к данным о- I ©J Seiect al euttxxi S«lwt by цмсАс author SdKtribooks Set book» wcunv* Pwc. 25.6. Диалоговое окно приложения XmlQueryExample 4. Добавьте два приватных поля для хранения документа и текущего узла, и инициализируйте их в конструкторе: private XmlDocument mDocument; private XmlNode mCurrentNode; public Forml() { InitializeComponent(); mDocument = new XmlDocument(); mDocument.Load(@"C:\Beginning Visual C#\Chapter 25\XPathQuery.xml"); mCurrentNode = mDocument.DocumentElement; ClearListBox(); } 5. Вам понадобятся еще несколько вспомогательных методов для отображения результата запросов в окне списка: private void DisplayList(XmlNodeList nodeList) { foreach (XmlNode node in nodeList) { RecurseXmlDocumentNoSiblings(node, 0) ; } private void RecurseXmlDocumentNoSiblings(XmlNode root, int indent) { // Ничего не делать, если root равен null, if (root == null) return; if (root is XmlElement) // Корень имеет тип XmlElement { // Сначала напечатать имя. listBoxResult.Items.Add(root.Name.PadLeft(root.Name.Length + indent)) , // Проверить наличие дочерних узлов, и если они есть, // вызвать этот же метод для их печати, if (root.HasChildNodes) RecurseXmlDocument(root.FirstChild, indent + 2) ; }
Глава 25. XML 813 else if (root is XmlText) { // Печатать текст. string text = ( (XmlText)root) .Value; listBoxResult.Items.Add(text.PadLeft(text.Length + indent)); } else if (root is XmlComment) { // Печатать текст. string text = root.Value; listBoxResult.Items.Add(text.PadLeft(text.Length + indent)); // Проверить наличие дочерних узлов, и если они есть, // вызвать этот же метод для их печати, if (root.HasChildNodes) RecurseXmlDocument(root.FirstChild, indent + 2) ; } } private void RecurseXmlDocument(XmlNode root, int indent) { // Ничего не делать, если root равен null, if (root == null) return; if (root is XmlElement) // Корень имеет тип XmlElement { // Сначала напечатать имя. listBoxResult.Items.Add(root.Name.PadLeft(root.Name.Length + indent)); // Проверить наличие дочерних узлов, и если они есть, // вызвать этот же метод для их печати, if (root.HasChildNodes) RecurseXmlDocument(root.FirstChild, indent + 2) ; // Наконец, проверить наличие соседних узлов и если они есть, // вызвать этот же метод для их печати. if (root.NextSibling !=null) RecurseXmlDocument(root.NextSibling, indent); } else if (root is XmlText) { // Напечатать текст. string text = ( (XmlText)root).Value; listBoxResult.Items.Add(text.PadLeft(text.Length + indent)); } else if (root is XmlComment) { // Напечатать текст. string text = root.Value; listBoxResult.Items.Add(text.PadLeft(text.Length + indent)); // Проверить наличие дочерних узлов, и если они есть, // вызвать этот же метод для их печати, if (root.HasChildNodes) RecurseXmlDocument(root.FirstChild, indent + 2); // Наконец, проверить наличие соседних узлов и если они есть, // вызвать этот же метод для их печати, if (root.NextSibling != null) RecurseXmlDocument(root.NextSibling, indent); }
814 Часть IV. Доступ к данным private void ClearListBox () { listBoxResult.Items.Clear(); } private void buttonClose_Click(object sender, EventArgs e) { Application.Exit (); } 6. Вставьте код для выполнения запросов к XML в событие SelectionChange переключателей. Вставляйте каждое событие по двойному щелчку на переключателях: private void radioButtonSelectRoot_CheckedChanged(object sender, EventArgs e) { mCurrentNode = mDocument.DocumentElement.SelectSingleNode("//books"); ClearListBox (); RecurseXmlDocument(mCurrentNode, 0) ; } private void radioButtonSelectAHAuthors_CheckedChanged (object sender, EventArgs e) { if (mCurrentNode != null) { XmlNodeList nodeList = mCurrentNode.SelectNodes("//book/author") ; ClearListBox(); DisplayList(nodeList); } else ClearListBox (); } private void radioButtonSelectBySpecificAuthor_CheckedChanged(object sender, EventArgs e) { if (mCurrentNode != null) { XmlNodeList nodeList = mCurrentNode.SelectNodes("//book[author=lJacob Hammer Pedersen']"); ClearListBox(); DisplayList(nodeList); } else ClearListBox(); } private void radioButtonSelectAHBooks_CheckedChanged(object sender, EventArgs e) { if (mCurrentNode != null) { XmlNodeList nodeList = mCurrentNode.SelectNodes("//book"); ClearListBox(); DisplayList(nodeList); } else ClearListBox(); }
Глава 25. XML 815 private void radioButtonSetBookAsCurrent_CheckedChanged (object sender, EventArgs e) { if (mCurrentNode != null) { mCurrentNode = mCurrentNode.SelectSingleNode("book[title=IBeginning Visual C#']"); ClearListBoxO ; RecurseXmlDocumentNoSiblings(mCurrentNode, 0) ; } else ClearListBoxO ; } private void radioButtonSetBooksAsCurrent_CheckedChanged(object sender, EventArgs e) { if (mCurrentNode != null) { mCurrentNode = mCurrentNode.SelectSingleNode("//books"); ClearListBox(); RecurseXmlDocumentNoSiblings(mCurrentNode, 0) ; } else ClearListBoxO ; } private void radioButtonSelectAHChildren_CheckedChanged (object sender, EventArgs e) { if (mCurrentNode != null) { XmlNodeList nodeList = mCurrentNode.SelectNodes ("*"); ClearListBoxO ; DisplayList(nodeList); } else ClearListBox(); } 7. И, наконец, вставьте код, который выполнится, когда пользователь введет что- нибудь в текстовом поле, и код, который закроет приложение по щелчку на кнопке buttonClose: private void buttonExecute_Click(object sender, EventArgs e) { if (textBoxQuery.Text == "") return; try { XmlNodeList nodeList = mCurrentNode.SelectNodes(textBoxQuery.Text); ClearListBox(); DisplayList(nodeList); } catch (System.Exception err) { MessageBox.Show(err.Message);
816 Часть IV. Доступ к данным private void buttonClose_Click(object sender, EventArgs e) { Application.Exit (); } 8. Запустите приложение. Обратите внимание, как изменяется значение в результате щелчков на переключателях. Описание полученных результатов Первые два метода, RecurseXmlDocumentNoSiblings и RecurseXmlDocument, a также DisplayList используются для вывода листингов в окно списка. Причина того, что нужно более одного такого метода, связана с необходимостью наличия более тонкого контроля вывода. Код, который выполняет сам поиск в XML, вводится на шагах 6 и 7. Метод SelectNodes класса XmlNode используется для выполнения поисков, которые могут или должны возвращать множественные узлы. Этот метод возвращает объект XmlNodeList, который затем передается вспомогательному методу DisplayList. Другой метод, применяемый для поиска в XML— это SelectSingleNode. Этот метод, как следует из его названия, возвращает единственный узел, который передается вспомогательному методу RecurseXmlNodeNoSiblings для отображения текущего узла и его дочерних узлов. В табл. 25.9 описаны запросы XPath, используемые в этих методах. Таблица 25.9. Запросы XPath, используемые в SelectNodes и SelectSingleNode Запрос XPath Описание //book/author Выбрать все узлы в XML по имени author с родителем book //book [author= Выбрать все узлы book в XML, имеющие дочерний узел с •Jacob Hammer Pedersen'] именем author и значением Jacob Hammer Pedersen //book Выбрать все узлы book в XML book [title= Выбрать узел book с узлом title, эквивалентным 'Beginning Visual C#'] Beginning Visual C# //books Выбрать все узлы books * Выбрать все дочерние узлы текущего узла Резюме В этой главе вы ознакомились с расширяемым языком разметки (XML) — текстовым форматом для хранения и извлечения данных. Вы изучили правила, которые нужно соблюдать для того, чтобы гарантировать создание правильно оформленных документов XML, а также узнали, как проверить их на соответствие схемам XSD и XDR. После изучения основ XML вы видели, как XML может применяться в коде С# с использованием Visual Studio. И, наконец, вы узнали о применении XPath для формулирования запросов к XML. В этой главе рассмотрены следующие вопросы. □ Как читать и записывать код XML. □ Правила, которым должен соответствовать правильно оформленный XML. □ Как проверять документы XML на соответствие двум типам схем: XSD и XDR.
Глава 25. XML 817 □ Как использовать .NET для работы с XML в приложениях. □ Как искать в XML-документах с использованием запросов XPath. В следующей главе вы узнаете о доступе к базам данных из .NET с использованием ADO.NET. Но прежде чем приступить к этому, для закрепления материала выполните следующие упражнения. Упражнения 1. Измените пример из раздела "Создание узлов" так, чтобы в узел book вставлялся атрибут по имени Pages со значением 1000. 2. В настоящее время последний пример XPath не имеет встроенных возможностей для поиска атрибута. Добавьте переключатель для выполнения запроса всех книг, для которых атрибут pages узла node равен 1000. 3. Измените методы, пишущие в окно списка (RecurseXmlDocument и RecurseXml DocumentNoSiblings), в примере XPath таким образом, чтобы они могли отображать атрибуты.
Введение в LINQ В этой главе представлен язык интегрированных запросов LINQ (Language- Integrated Query) — новое расширение языка С#, добавленное в версию С# 3.0 — язык, поддерживаемый Visual C# 2008. Язык LINQ решает проблему работы с очень большими коллекциями объектов, когда обычно приходится выбирать подмножество коллекции для решения определенной задачи. В прошлом такого рода работа требовала написания объемного циклического кода и дополнительной обработки, связанной с сортировкой или группированием найденных объектов, которая порождала еще больший код. Язык LINQ избавляет от необходимости писать дополнительный циклический код для фильтрации и сортировки. Он позволяет сосредоточиться на объектах, которыми оперирует программа, предоставляя удобные средства формулирования запросов. В дополнение к элегантному языку запросов, который позволяет специфицировать точно искомые объекты, LINQ предлагает также множество расширяющих методов, которые облегчают задачу сортировки, группирования и вычисления статистики на результатах запросов. LINQ также позволяет запрашивать крупные базы данных или сложные документы XML, содержащие миллионы или даже миллиарды объектов, которые нужно эффективно искать и которыми необходимо манипулировать. Традиционно эта проблема обычно решалась специализированной библиотекой классов или даже посредством использования другого языка, такого как язык запросов базы данных SQL. Однако библиотеки классов не так легко расширить для множества других типов объектов, а смешивание языков вызывает проблемы несоответствия типов, а также затрудняет понимание программ разработчикам, не знакомым с обоими языками. LINQ предоставляет механизм, встроенный в сам язык С#, который решает каждую из упомянутых проблем. В этой главе мы представим различные вариации LINQ, такие как LINQ to Objects, LINQ to SQL и LINQ to XML, а также раскроем следующие темы. □ Кодирование запроса LINQ и частей оператора запроса LINQ. □ Использование синтаксиса методов LINQ вместо синтаксиса запросов LINQ.
Глава 26. Введение в LINQ 819 □ Применение лямбда-выражений в LINQ. □ Упорядочивание результатов запроса, включая многоуровневое упорядочивание. □ Когда и как следует использовать агрегатные операции LINQ. □ Использование проекций для создания новых объектов в запросах. □ Использование операций Distinct (), Any (), All (), First (), FirstOrDef ault (), Take() и Skip(). □ Групповые запросы. □ Операции множеств и соединения. Хотя эта глава сосредоточена па LINQ to Objects, основные излагаемые здесь концепции применимы ко всем вариациям LINQ и являются основой для последующих глав по LINQ to SQLuUNQtoXML. LINQ слишком велик, чтобы полностью раскрыть все его средства и методы; это вышло бы за рамки книги для начинающих. Однако вы увидите здесь примеры всех различных типов операций и операторов, которые, вероятно, понадобятся вам как пользователю LINQ, и вы получите здесь ссылки на ресурсы, необходимые для углубленного изучения темы. Вариации LINQ Среда Visual C# 2008 поставляется с тремя встроенными вариациями LINQ, которые обеспечивают запросные решения для разных типов данных: LINQ to Objects, LINQ to SQL и LINQ to XML. □ LINQ to Objects. Обеспечивает возможность запросов любых объектов С#, находящихся в памяти, таких как массивы, списки и другие типы коллекций. Во всех примерах настоящей главы используется LINQ to Objects. Тем не менее, вы сможете применять изученные здесь приемы для работы со всеми прочими вариациями LINQ. □ LINQ to SQL. Обеспечивает возможность запросов к реляционным базам данных, использующим стандартный язык запросов баз данных, таким как Microsoft SQL Server, Oracle и т.п. В прошлом доступ С# к этим базам требовал от разработчика изучения как минимум, некоторой части SQL, но теперь возможности запросов встроены в сам язык С# через LINQ, и вы можете позволить LINQ to SQL обработать трансляцию SQL за вас. LINQ to SQL детально рассматривается в главе 27. □ LINQ to XML. Обеспечивает возможность создания и манипулирования документами XML с применением того же синтаксиса, а также механизмы запросов, общие с прочими вариациями. LINQ to XML детально рассматривается в главах 27 и 29. □ Поставщики LINQ. LINQ может быть расширен для других вариаций источников данных помимо объектов, баз данных SQL и XML. Нужно просто написать собственный поставщик LINQ для этого типа источника данных. Дизайн поставщика LINQ— это отдельная тема, выходящая за рамки настоящей книги, но разработчикам С# важно знать, что новые поставщики LINQ могут быть написаны и будут написаны (как самой Microsoft, так и независимыми разработчиками), так что в будущем можно ожидать появление других вариаций LINQ.
820 Часть ГУ. Доступ к данным Первый запрос LINQ Начнем с примера. В следующем практическом занятии вы создадите запрос для поиска некоторых данных в простом, находящемся в памяти, массиве объектов с использованием LINQ и распечатаете результат на консоли. Первая программа LINQ Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новое консольное приложение по имени 2 6-1-FirstLINQquery в каталоге С: \BegVCSharp\Chapter26, затем откройте главный файл Program, cs. 2. Обратите внимание, что Visual C# 2008 по умолчанию включает пространства имен LINQ в Program, cs: using System; using System.Collections.Generic; using System.Linq; using System.Text; 3. Добавьте следующий код в метод Main () в Program.cs: static void Main(string [ ] args) { string[] names = { "Alonso", "Zheng", "Smith", "Jones", "Smythe", "Small", "Ruiz", "Hsieh", "Jorgenson", "Ilyich", "Singh", "Samba", "Fatimah" }; var gueryResults = from n in names where n.StartsWith("S") select n; Console.WriteLine("Имена, начинающиеся с S:"); foreach (var item in gueryResults) { Console.WriteLine(item); } Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Откомпилируйте и запустите программу (можно просто нажать <F5> для запуска отладки). Вы увидите имена в списке, начинающиеся с S, в том порядке, в котором они объявлены в массиве, как показано ниже: Имена, начинающиеся с S: Smith Smythe Small Singh Samba Программа завершена, нажмите Enter для продолжения: Просто нажмите <Enter> для завершения программы и закрытия экрана консоли. Если вы использовали <Ctrl+F5> (запуск без отладки), может понадобиться нажать <Enter> два раза. Это завершит выполнение программы.
Глава 26. Введение в LINQ 821 Описание полученных результатов Первый шаг— ссылка на пространство имен System.Linq, которая выполняется автоматически Visual C# 2008 при создании проекта: using System.Linq; Все базовые системные классы поддержки LINQ находятся в пространстве имен System. Linq. Если вы создаете исходный файл С# вне Visual C# 2008 или редактируете ранее существовавший проект Visual C# 2005, вам может понадобиться добавить эту директиву вручную. Следующий шаг — создание некоторых данных, что осуществляется в рассматриваемом примере объявлением и инициализацией массива names: string[] names = { "Alonso", "Zheng", "Smith", "Jones", "Smythe", "Small", "Ruiz", "Hsieh", "Jorgenson", "Ilyich", "Singh", "Samba", "Fatimah" }; Это тривиальный набор данных, тем не менее, он подходит для начального примера, в котором результат запроса очевиден. Сам оператор запроса LINQ представлен в следующей части программы: var queryResults = from n in names where n.StartsWith("S") select n; Оператор выглядит довольно странно, не правда ли? Он кажется написанным на языке, отличном от С#, и в самом деле— синтаксис from. . .where. . .select определенно напоминает язык запросов баз данных SQL. Однако это не SQL! Это действительно С#, как вы убедились, набирая этот код в Visual C# 2008 — слова from, where и select подсвечивались как ключевые, и этот странно выглядящий синтаксис вполне понравился компилятору. Оператор запроса LINQ в этой программе использует декларативный синтаксис запросов LINQ: var queryResults = from n in names where n.StartsWith("S") select n; Этот оператор состоит из четырех частей: начинающееся с var объявление переменной result, которой выполняется присваивание с использованием синтаксиса запроса, состоящего из конструкций from, where и select. Давайте рассмотрим каждую из этих частей по очереди. Объявление переменной результата с использованием ключевого слова var Запрос LINQ начинается с объявления переменной, которая будет содержать результат запроса, что обычно осуществляется объявлением с ключевым словом var: var queryResult = Как было описано в главе 14, var — новое ключевое слово в С# 3.0, предназначенное для объявления обобщенного типа переменных, что идеально для хранения результатов запросов LINQ. Ключевое слово var заставляет компилятор С# вывести тип результата на основе самого запроса. Таким образом, вам не нужно заранее объявлять тип объектов, возвращаемых запросом LINQ— компилятор позаботится об этом сам.
822 Часть IV. Доступ к данным Если запрос может вернуть множество элементов, то этот тип должен вести себя как коллекция объектов из источника данных запроса (технически — это не коллекция; она лишь выглядит так). Если вы хотите знать подробности, то скажем, что результат запроса будет типом, реализующим интерфейс lEnumerableo. В данном конкретном случае компилятор создает экземпляр System.Linq.OrderSequence<string, string>— специального типа данных LINQ который представляет упорядоченный список строк (потому что источник данных— коллекция строк). Кстати, имя queryResult произвольно — вы можете назвать результат так, как вам нравится. Это может быть names Beg inningWithS или что-нибудь другое, что имеет смысл внутри программы. Спецификация источника данных: конструкция from Следующая часть запроса LINQ— конструкция from, которая специфицирует источник запрашиваемых данных: from n in names Источником данных в рассматриваемом случае является names — массив строк, объявленный ранее. Переменная п — просто заместитель индивидуальных элементов в источнике данных, подобно переменной name, следующей за оператором f о reach. Специфицируя from, вы сообщаете о том, что собираетесь запросить подмножество коллекции, а не выполнять итерацию по всем элементам. Говоря об итерации, мы подразумеваем, что источник данных LINQ должен быть перечислимым — т.е. он должен быть массивом или коллекцией элементов, из которой их можно извлекать по одному. Технически это означает, что источник данных должен поддерживать интерфейс lEnumerableo, что присуще любому массиву или коллекции элементов С#. Источник данных не может быть единственным значением или объектом, таким как отдельная переменная int. И в самом деле, если уже имеется единственный элемент, зачем его запрашивать? Спецификация условия: конструкция where В следующей части запроса LINQ вы специфицируете условие запроса, используя конструкцию where, которая выглядит так: where n.StartWith("S") В конструкции where может быть специфицировано любое булевское выражение, которое применимо к элементам в источнике данных. На самом деле конструкция where не обязательна, и даже может быть опущена, но почти во всех случаях понадобится специфицировать условие where, чтобы ограничить результат только необходимыми данными. Конструкция where называется ограничивающей операцией в LINQ, потому что она ограничивает результат запроса. Здесь вы специфицируете, что строка name должна начинаться с буквы S, но можно было бы специфицировать что-нибудь другое вместо этого, например, что длина должна быть больше 10 (where n.Length > 10) или имя должно содержать в себе букву Q (where n. Contains ("Q")).
Глава 26. Введение в LINQ 823 Выбор элементов: конструкция select И, наконец, конструкция select специфицирует, какие элементы появятся в результирующем наборе. Выглядит она так: select n Конструкция select необходима, потому что вы должны специфицировать элементы, которые запрос поместит в результирующий набор. Для рассматриваемого набора данных это не очень интересно, поскольку в каждом объекте результирующего набора есть только один элемент — имя. Далее будут показаны некоторые примеры более сложных объектов в результирующем наборе, где полезность конструкции select будет более наглядной, а пока давайте завершим наш первый пример. Последний штрих: использование цикла f oreach Теперь вы печатаете результаты запроса. Подобно массиву, использованному в качестве источника данных, результат запроса LINQ вроде этого является перечислимым (enumerable) — в том смысле, что вы можете выполнить итерацию по результату с помощью оператора f oreach: Console.WriteLine("Имена, начинающиеся с S:"); foreach (var item in queryResults) { Console.WriteLine (item) ; } В этом случае получается совпадением с четырьмя именами — Singh, Small, Smythe и Samba, и именно они будут выведены в цикле foreach. Отложенное выполнение запроса Можете показаться, что цикл foreach не является частью самого LINQ— ведь он просто перебирает полученные результаты. Хотя это и верно, что конструкция foreach не принадлежит только LINQ, тем не менее, это часть кода, которая в действительности выполняет запрос LINQ! Присваивание результата запроса переменной лишь сохраняет план его выполнения; в LINQ сами данные не извлекаются до тех пор, пока не выполнено обращение к результату. Это называется отложенным выполнением запроса, или "ленивым" выполнением запроса. Выполнение откладывается для любого запроса, производящего последовательности, т.е. список результатов. Теперь вернемся к коду. Поскольку результаты распечатаны, давайте завершим программу: Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine() ; Эти строки лишь гарантируют, что результаты консольной программы останутся на экране до тех пор, пока вы не нажмете клавишу, даже если вы запустите ее нажатием <F5> вместо <Ctrl+F5>. Эта конструкция будет применяться и в других примерах LINQ. Использование синтаксиса методов LINQ и лямбда-выражений В LINQ существует много способов решения одной и той же задачи, как это часто бывает в программировании. Как уже отмечалось, предыдущий пример написан с использованием синтаксиса запросов LINQ. В следующем примере вы напишете ту же
824 Часть IV. Доступ к данным программу с применением синтаксиса методов LINQ (также известного, как явный синтаксис, но мы будем называть его синтаксисом методов). Расширяющие методы LINQ LINQ реализован в виде серии расширяющих методов коллекций, массивов, результатов запросов и любых других объектов, реализующих интерфейс IEnumerable. Вы можете видеть эти методы в средстве IntelliSense Visual Studio. Например, откройте файл Program, cs только что скомпилированной программы 26-1-FirstLINQquery в Visual C# 2008 и введите новую ссылку на тот же массив names, как показано ниже: string[] names = { "Alonso", "Zheng", "Smith", "Jones", "Smythe", "Small", "Ruiz", "Hsieh", "Jorgenson", "Ilyich", "Singh", "Samba", "Fatimah" }; Как только вы введете точку после names, то сразу увидите в раскрывающемся списке IntelliSense все доступные методы объекта names, как показано на рис. 26.1. names.] '^f SyncRoot var \ Takeo £ V^ TakeWhIleo w ^4 ToArrayo ^^ % ToDictlonaryO ~~ \ Tollsto ЩЩ *4 ToLookupo InQtl * ToStrIng 1 \ Union о *+Щ| A ^Izl ') , n1 \ ШШШШШШШШШШШШШШШЩШШШ T —-:: ----- -■■-,,,_ i^.l_ i^..^^ (extension) IEnumerable <TSource> IEnumerable <TSource> Where <TSource> Filters a sequence of values based on a predicate. Each element's index is used Puc. 26.1. Работа средства IntelliSense Метод Whereo и большинство прочих доступных методов являются расширяющими методами (что отмечено в документации словом extension, следующим за Whereo). Вы можете увидеть, что они являются расширениями LINQ, если закомментируете директиву using System.Linq в начале файла. Тогда Whereo, UnionO, Takeso и большинство других методов исчезнут из этого списка. Выражение запроса for. . .where. . .select, использованное в предыдущем примере, транслируется компилятором С# в серию вызовов этих методов. При использовании синтаксиса методов LINQ вы вызываете эти методы напрямую. Синтаксис запросов в сравнении с синтаксисом методов Синтаксис запросов является предпочтительным способом программирования запросов в LINQ, поскольку его намного легче читать и проще использовать в большинстве часто применяемых запросов. Однако важно иметь базовое понятие о синтаксисе методов, поскольку некоторые возможности LINQ либо недоступны в синтаксисе запросов, либо просто их легче применять в синтаксисе методов. Как рекомендует онлайновая справочная система Visual C# 2008, используйте синтаксис запросов, когда возможно, и синтаксис методов, когда необходимо. В этой главе применяется в основном синтаксис запросов, но будут показаны ситуации, когда необходим синтаксис методов, и продемонстрировано, как использовать синтаксис методов для решения проблемы.
Глава 26. Введение в LINQ 825 Лямбда-выражения Большинство методов LINQ, использующих синтаксис методов, требуют передачи метода или функции для вычисления выражения запроса. Параметр типа метода/ функции передается в форме делегата, который часто называют анонимным методом. К счастью, LINQ позволяет сделать это намного проще, чем можно предположить! Вы создаете метод/функцию, используя специальную конструкцию С# 3.0, которая называется лямбда-выражением (lambda expression). Лямбда-выражение представляет собой короткий, согласованный способ записи анонимного метода, применяемый для вычисления выражений запроса. Ниже показано простое лямбда-выражение: п => п < 1000 Операция => называется лямбда-операцией. "Лямбда" здесь происходит от лямбда-исчисления— математического понятия, лежащего в основе функциональных языков программирования. Функциональное программирование— это разновидность программирования, на котором базируется LINQ. Объяснение деталей выходит за рамки настоящей книги, но если вам интересно, можете поискать дополнительную информацию в статье Wikipedia, посвященной лямбда-исчислению. Вам не нужно понимать математические детали, чтобы использовать лямбда-выражения, хотя понимание функционального программирования полезно для профессионального программирования на LINQ. См. раздел "Ресурсы для дополнительного изучения " в конце этой главы. Лямбда-выражение — это сокращенный способ определения функции. Например, лямбда-выражение n => n < 1000 определяет функцию, которая принимает параметр по имени п и возвращает true, если п меньше 1000, и false— в противном случае. Функция представлена анонимным методом, не имеющим названия, который используется только там, где это лямбда-выражение передается лежащему в основе методу LINQ. Система онлайновой справки Visual C# 2008 советует читать это лямбда-выражение, как "п идет к п больше 1000" (n goes to n greater than 1000), хотя булевские лямбда-выражения вроде этого предпочтительнее читать как "п такое, что п меньше 1000" (n such that n is less than 1000). Лямбда-выражение для запроса в первом примере программы может быть записано, как n => n.StartsWith("S")); что следует читать, как "п такое, что начинается с S". Попробуйте применить это в действительной программе для более ясного представления. Практическое занятие ИсПОЛЬЗОВЭНИе СИНТЭКСИСа МвТОДОВ LINQ и лямбда-выражений Выполните следующие шаги для создания примера в Visual C# 2008. 1. Вы можете либо модифицировать пример 2 6-1-FirstLINQQuery, либо создать новое консольное приложение по имени 2 6-2-LINQMethodSyntax в каталоге C:\BegVCSharp\Chapter2 6. Откройте главный файл Program.cs. 2. Visual C# 2008 автоматически включает нужное пространство имен в Program, cs: using System.Linq;
826 Часть ГУ. Доступ к данным 3. Добавьте следующий код в метод Main () в Program, cs. Ниже выделен только отличающийся от первого примера код: static void Main(string[] args) { string [] names = { "Alonso", "Zheng", "Smith", "Jones", "Smythe", "Small", "Ruiz", "Hsieh", "Jorgenson", "Ilyich", "Singh", "Samba", "Fatimah" }; var queryResuits = names.Where(n => n.StartsWith("S")) ; Console.WriteLine("Имена, начинающиеся с S:"); foreach (var item in queryResults) { Console.WriteLine(item); } Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Скомпилируйте и запустите эту программу (можно просто нажать <F5>). Вы увидите тот же вывод списка имен, начинающихся с S, в порядке, в котором они объявлены в массиве, как показано ниже: Имена, начинающиеся с S: Smith Smythe Small Singh Samba Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Как и ранее, ссылка на пространство имен System.Linq вставляется автоматически Visual C# 2008: using System.Linq; Те же исходные данные, что и ранее, создаются вновь с помощью объявления и инициализации массива имен: string[] names = { "Alonso", "Zheng", "Smith", "Jones", "Smythe", "Small", "Ruiz", "Hsieh", "Jorgenson", "Ilyich", "Singh", "Samba", "Fatimah" }; Отличающаяся часть— запрос LINQ, который теперь вызывает метод Where () вместо выражения запроса: var queryResults = names.Where (n => n.StartsWith("S")); Компилятор С# превращает лямбда-выражение n => n. StartsWith ("S") в анонимный метод, выполняемый Where () на каждом элементе массива names. Если лямбда- выражение возвращает для элемента true, он включается в результат, возвращаемый Where (). Компилятор С# определяет, что Where () должен принимать string в качестве входного типа для каждого элемента входного источника (в данном случае — массива names). Как много происходит в единственной строке кода, не правда ли? Для такого простого типа запроса, как этот, синтаксис метода существенно короче, чем синтаксис запроса, потому что конструкции from и select не нужны; однако большинство запросов, с которыми вам придется иметь дело, сложнее этого. Остальная часть примера та же, что и в предыдущем случае — вы просто печатаете результаты запроса в цикле foreach и приостанавливаете вывод в конце, чтобы можно было увидеть его перед тем, как программа завершится:
Глава 26. Введение в LINQ 827 foreach (var item in queryResults) { Console.WriteLine(item); } Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); Мы не станем повторять здесь объяснения этих строк, поскольку оно уже было приведено в разделе "Описание полученных результатов" после первого примера этой главы. Теперь давайте двинемся дальше и рассмотрим другие возможности LINQ. Упорядочивание результатов запроса Как только вы нашли интересующие данные в конструкции where (или вызове метода Where ()), LINQ облегчает дальнейшую обработку, такую как сортировка результирующих данных. В следующем практическом занятии вы представите результаты первого запроса в алфавитном порядке. шшшяшт Практическое занят*] Упорядочение реЗуЛЬТЭТОВ ЗЭПрОСа Выполните следующие шаги для создания нового примера в Visual C# 2008. 1. Вы можете либо модифицировать первый пример 26-1-FirstLINQQuery, либо создать новый проект консольного приложения по имени 2 6-3-OrderQuery Results в каталоге C:\BegVCSharp\Chapter26. 2. Откройте главный файл Program, cs. Как и ранее, Visual C# 2008 автоматически включает директиву using System. Linq; в Program, cs. 3. Добавьте следующий код в метод Main () в Program, cs. Код, отличающийся от первого примера, выделен полужирным: static void Main(string[] args) { string[] names = { "Alonso", "Zheng", "Smith", "Jones", "Smythe", "Small", "Ruiz", "Hsieh", "Jorgenson", "Ilyich", "Singh", "Samba", "Fatimah" }; var queryResults = from n in names where n.StartsWith ("S") orderby n select n; Console.WriteLine("Имена, начинающиеся с S, в алфавитном порядке:"); foreach (var item in queryResults) { Console.WriteLine(item); } Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Откомпилируйте и выполните программу. Вы увидите имена, начинающиеся с S в алфавитном порядке, как показано ниже: Имена, начинающиеся с S, в алфавитном порядке: Samba Singh Small Smith Smythe Программа завершена, нажмите Enter для продолжения:
828 Часть IV. Доступ к данным Описание полученных результатов Эта программа почти идентична предыдущему примеру, за исключением одной дополнительной строки, добавленной к оператору запроса: var queryResults = from n in names where n.StartsWith ("S") orderby n select n; Конструкция orderby Конструкция orderby выглядит следующим образом: orderby n Подобно where, конструкция orderby необязательна. Добавив всего одну строку, вы можете выстроить в нужном порядке результаты любого запроса, что в противном случае потребовало бы нескольких строк дополнительного кода и, возможно, дополнительных методов или коллекций для хранения результатов, упорядоченных различным образом в зависимости от алгоритма сортировки, который вы предпочтете реализовать. Если у вас несколько типов, которые нужно упорядочить, вам пришлось бы реализовать множество методов упорядочивания для каждого из них. Благодаря LINQ, вам более не нужно беспокоиться об этом; просто добавьте дополнительную конструкцию к оператору запроса. По умолчанию orderby упорядочивает элементы результата по возрастанию (от А до Z), но вы можете специфицировать и порядок убывания (от Z до А), просто добавив ключевое слово descending: orderby n 'descending Это упорядочит результат примера следующим образом: Smythe Smith Small Singh Samba Плюс к тому можно упорядочить по произвольному выражению, не переписывая запрос. Например, чтобы упорядочить по последней букве в имени вместо нормального алфавитного порядка, вы просто изменяете конструкцию orderby следующим образом: orderby n.Substring(n.Length - 1) Результат показан ниже: Samba Smythe Smith Singh Small Обратите внимание, что последние буквы выстроены по алфавиту (а е h h l).
Глава 26. Введение в LINQ 829 Упорядочивание с использованием синтаксиса методов Чтобы добавить к запросу такие возможности, как упорядочивание результата, просто добавьте вызов метода для каждой операции LINQ, которую хотите выполнить в своем запросе LINQ на основе методов. Опять-таки, это проще, чем может показаться на первый взгляд. Практическое занятие Упорядочение С ИСПОЛЬЗОВЭНИеМ синтаксиса методов Выполните следующие шаги для создания примера в Visual C# 2008. 1. Вы можете либо модифицировать пример 26-2-LINQMethodSyntax, либо создать новый проект консольного приложения по имени 2 6-4-OrderMethodSyntax в каталоге C:\BegVCSharp\Chapter26. 2. Добавьте следующий код в метод Main () в Program, cs. Как и во всех примерах Visual C# 2008, он автоматически включит ссылку на пространство имен System. Linq. 3. Код, отличающийся от предыдущего примера синтаксиса метода, выделен полужирным: static void Main(string [ ] args) { string[] names = { "Alonso", "Zheng", "Smith", "Jones", "Smythe", "Small", "Ruiz", "Hsieh", "Jorgenson", "Ilyich", "Singh", "Samba", "Fatimah" }; var queryResults = names.OrderBy(n => n) .Where(n => n.StartsWith("S")) ; Console.WriteLine("Names beginning with S:"); foreach (var item in gueryResults) { Console.WriteLine(item); } Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console . ReadLme () ; } 4. Откомпилируйте и запустите программу. Вы увидите список имен, начинающихся с S, в алфавитном порядке — точно так де, как и в выводе предыдущего примера. Описание полученных результатов Этот пример почти идентичен предыдущему примеру синтаксиса метода, за исключением добавления вызова метода LINQ QueryBy (), предшествующего вызову метода Where (): var gueryResults = names.OrderBy(n => n).Where(n => n.StartsWith("S")); Как вы, возможно, заметили в IntelliSense, набирая код в редакторе, метод OrderBy () возвращает IOrderedEnumerable, который является надмножеством интерфейса IEnumerable, поэтому можно вызывать Where () на нем точно так же, как это делается с любым другим объектом IEnumerable. Компилятор делает вывод, что вы работаете с данными string, поэтому типы данных появляются в IntelliSense как IOrderedEnumerable<string> и IEnumerable<string>.
830 Часть ГУ. Доступ к данным Вы должны передать лямбда-выражение методу OrderBy (), чтобы сообщить ему, какую функцию следует применять для упорядочивания. Вы передаете простейшее возможное лямбда-выражение п => п, потому что не нуждаетесь в упорядочивании ничего другого, кроме самого выводимого значения. В синтаксисе запросов создавать это дополнительное лямбда-выражение не нужно. Чтобы упорядочить элементы в обратном порядке, вызывайте метод OrderByDescending(): var queryResults = names.OrderByDescending(n => n).Where(n => n.StartsWith ("S")); Это даст те же результаты, что и конструкция or derby n descending, которая применялась в версии с синтаксисом запросов. Если нужно упорядочить по чему-то отличающемуся от самого значения, вы можете изменить передаваемое OrderBy () лямбда-выражение. Например, чтобы упорядочить по последней букве каждого имени, необходимо использовать следующее лямбда-выражение: n => n. Substring (n. Length-1) и передать его OrderBy, как показано ниже: var gueryResults = names.OrderBy (n => n.Substring(n.Length-1)).Where(n => n.StartsWith("S")); Это даст тот же результат, упорядоченный по последней букве имени, как и в предыдущем примере. Упорядочивание больших наборов данных Вы можете сказать, что в синтаксисе LINQ все прекрасно, но в чем смысл всего этого? Вы можете и без него замечательно увидеть нужный результат, просто взглянув на исходный массив, так зачем же морочить голову, выстраивая запросы того, что и так очевидно? Как упоминалось ранее, иногда результаты запроса не столь очевидны. В следующем практическом занятии вы создадите огромный массив чисел и выполните запрос к нему с помощью LINQ. Выполните следующие шаги для создания примера в Visual C# 2008. 1. Создайте новое консольное приложение по имени 2 6-5-LargeNumberQuery в каталоге С: \BegVCSharp\Chapter2 6. Как и ранее, при создании этого проекта Visual C# 2008 включит необходимое пространство имен LINQ в файл Program, cs: using System; using System.Collections.Generic; using System.Linq; using System.Text; 2. Добавьте следующий код в метод Main (): static void Main(string[] args) { int[] numbers = generateLotsOfNumbersA2345678); var queryResults = from n in numbers where n < 1000 select n ; Console.WriteLine("Числа меньше 1000:");
Глава 26. Введение в LINQ 831 foreach (var item in queryResults) { Console.WriteLine(item); } Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 3. Добавьте следующий метод для генерации списка случайных чисел: private static int[] generateLotsOfNumbers(int count) { Random generator = new Random@); int[] result = new int[count]; for (int i = 0; i < count; i++) { result[i] = generator.Next (); } return result; } 4. Откомпилируйте и запустите программу. Вы увидите список чисел меньше 1000, как показано ниже: Числа меньше 1000: 714 24 677 350 257 719 584 Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Как и ранее, первый шаг— это ссылка на пространство имен System.Linq, которая выполняется автоматически Visual C# 2008 при создании проекта: using System.Linq; Следующий шаг — генерация некоторых данных, которая осуществляется в этом примере вызовом метода generateLotsOfNumbers (): int[] numbers = generateLotsOfNumbersA2345678); private static int[] generateLotsOfNumbers(int count) { Random generator = new Random@); int[] result = new int[count]; for (int i = 0; i < count; i++) { result [i] = generator.Next () ; } return result; } Это не тривиальный набор данных— в массиве 12 миллионов чисел! В одном из упражнений в конце этой главы вы измените параметр size, передаваемый методу generateLotsOfNumbers () для генерации произвольного размера наборов случайных чисел, чтобы увидеть, как это влияет на результаты запроса. Как вы увидите, выполняя упражнение, размер, указанный здесь, 12 345 678, достаточно велик для получения
832 Часть IV. Доступ к данным некоторых случайных чисел меньше 1000, чтобы получить результаты для демонстрации первого запроса. Значения должны быть равномерно распределены по диапазону целых со знаком (от нуля до более чем 2 миллиарда). Создав генератор случайных чисел с начальным значением 0, вы гарантируете генерацию одной и той же последовательности случайных чисел каждый раз, так что получите один и тот же результат запроса, показанный здесь, но каким именно будет этот результат — не знает никто, пока запрос не будет выполнен. К счастью, LINQ делает эту задачу простой! Сам оператор запроса подобен тому, что вы выполняли ранее с именами, и состоит в том, чтобы выбрать некоторые числа, отвечающие определенному условию (в данном случае — числа меньше 1000): var queryResults = from n in numbers where n < 1000 select n Конструкция or derby на этот раз не нужна и потребовала бы дополнительного времени на обработку (не слишком существенного в данном примере, но заметного, когда вы измените запрос в следующем примере). Вы печатаете результаты запроса в операторе f oreach, как и в предыдущем примере: Console.WriteLine("Числа меньше 1000:"); foreach (var item in queryResults) { Console.WriteLine (item) ; } Опять-таки, здесь присутствует вывод на консоль и чтение символа для приостановки вывода: Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); Код паузы во всех последующий примерах один и тот же и мы не станем показывать его вновь и вновь. В LINQ очень легко изменить условия запроса для представления различных характеристик набора данных. Однако в зависимости от того, насколько много результатов возвращает запрос, может оказаться бессмысленным вывод их всех каждый раз. В следующем разделе будет показано, какие агрегатные операции предлагает LINQ, чтобы справиться с этой проблемой. Агрегатные операции Часто запрос возвращает больше результатов, чем ожидалось. Например, если вы измените условие предыдущей программы запроса чисел, указав, что необходимы числа больше 1000, а не меньше 1000, результат окажется просто нескончаемым, и вам захочется прервать печать! К счастью, LINQ предоставляет набор агрегатных операций, позволяющих анализировать результаты запроса, не выполняя цикл по всем результатам. Перечисленные в табл. 26.1 агрегатные операции чаще всего используются для набора числовых результатов, таких как результаты предыдущего запроса, и могут быть знакомы, если вы имели дело с языком запросов баз данных наподобие SQL.
Глава 26. Введение в LINQ 833 Таблица 26.1. Агрегатные операции Операция Описание Count () Подсчитывает количество результатов Min () Минимальное значение результатов мах () Максимальное значение результатов Average () Среднее значение числовых результатов Sum () Сумма всех числовых результатов Есть и другие агрегатные операции, такие как Aggregate (), предназначенные для выполнения произвольного кода в манере, позволяющей самостоятельно кодировать агрегатную функцию. Однако это — тема для профессиональных разработчиков, и она выходит за рамки настоящей книги. Поскольку агрегатные операции возвращают простой скалярный тип вместо последовательности результатов, их применение вызывает немедленное выполнение всего запроса вместо обычного отложенного выполнения запроса. В следующем практическом занятии вы модифицируете большой числовой запрос из предыдущего примера и применяете агрегатные операции для исследования результирующего набора из версии "больше чем" этого запроса, используя LINQ. практическое занятие Числовые агрегатные операции Выполните следующие шаги для создания примера в Visual C# 2008. 1. Вы можете либо модифицировать пример LargeNumberQuery, либо создать новый проект консольного приложения по имени 26-6-NumericAggregates в каталоге C:\BegVCSharp\Chapter2 6. 2. Как и ранее, когда вы создаете проект, Visual C# 2008 включает в Program.cs ссылку на пространство имен LINQ. Вам нужно лишь модифицировать метод Main (), как показано в следующем коде и в остальной части этого практического занятия. Как и с предыдущим примером, в этом запросе не используется конструкция orderby. Однако условие конструкции where противоположно предыдущему примеру (числа должны быть больше 1000, а не меньше, как ранее). static void Main(string [ ] args) { int[] numbers = generateLotsOfNumbersA2345678); Console.WriteLine("Числовые агрегаты"); var queryResults = from n in numbers where n > 1000 select n Console.WriteLine("Количество чисел > 1000"); Console.WriteLine(queryResults.Count()); Console.WriteLine("Максимальное из чисел > 1000") ; Console.WriteLine(queryResults.Max()); Console.WriteLine("Минимальное из чисел > 1000"); Console.WriteLine(queryResults.Min 0);
834 Часть IV. Доступ к данным Console.WriteLine("Среднее из чисел > 1000"); Console.WriteLine(queryResults.Average()); Console.WriteLine ("Сумма чисел > 1000") ; Cons ole. WriteLine (queryRe suits. Sum (n => (long) n)) ; Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 3. Если его еще нет, добавьте тот же метод generateLotsOfNumbers(), использованный в предыдущем примере: private static mt[] generateLotsOfNumbers (int count) { Random generator = new Random@) ; irft [ ] result = new int[count]; for (int i = 0; i < count; i++) { result[i] = generator.Next (); } return result; } 4. Откомпилируйте и выполните пример. Вы увидите значения количества чисел, удовлетворяющих условию where, минимальное, максимальное и среднее их значение, как показано ниже: Числовые агрегаты Количество чисел > 1000 12345671 Максимальное из чисел > 1000 2147483591 Минимальное из чисел > 1000 1034 Среднее из чисел > 1000 1073643807.50298 Сумма чисел > 1000 13254853218619179 Программа завершена, нажмите Enter для продолжения: Этот запрос производит намного больше результатов, чем в предыдущем примере (более 12 миллиардов). Использование о г derby на таком большом результирующем наборе определенно пагубно скажется на производительности! Самое большое число в результирующем наборе превышает значение в два миллиарда, а самое маленькое — чуть больше 1000, как и ожидалось. Среднее — около 1 миллиона — как раз находится около середины диапазона возможных значений. Похоже, функция Rand () генерирует равномерное распределение чисел. Описание полученных результатов Первая часть программы точно такая же, как и в предыдущем примере, со ссылкой на пространство имен System. Linq и использованием метода generateLotsOf Numbers () для генерации исходных данных: int[] numbers = generateLotsOfNumbersA2345678); Запрос — тот же самый, что и в предыдущем примере, за исключением того, что условие where изменено с "меньше" на "больше": var queryResults = from n in numbers where n > 1000 select n;
Глава 26. Введение в LINQ 835 Как упоминалось ранее, запрос, использующий условие "больше чем", производит намного больше результатов, чем запрос с условием "меньше чем" (на этом конкретном наборе данных). Используя агрегатные операции, вы можете исследовать результаты запроса, не печатая каждый элемент и не сравнивая его в цикле f о reach. Каждая операция выступает в виде метода, вызываемого на результирующем наборе, подобно методам типа коллекции. Рассмотрим применение каждой агрегатной операции: □ Count (): Console.WriteLine("Количество чисел > 1000"); Console.WriteLine(queryResults.Count()); Count () возвращает количество строк в результате запроса, в данном случае — 12 345 671 строк. □ Мах(): Console.WriteLine("Максимальное из чисел > 1000"); Console.WriteLine(queryResults.Max()); Max () возвращает максимальное значение в результатах запроса, в данном случае — число больше 2 миллиардов: 2 147 483 591, что очень близко к максимальному значению типа int (int .MaxValue, или 2 147 483 647). □ Min(): Console.WriteLine("Максимальное из чисел > 1000"); Console.WriteLine(queryResults.Min()); Min () возвращает минимальное значение в результатах запроса, в данном случае-1034. □ Average(): Console.WriteLine("Среднее из чисел > 1000"); Console.WriteLine(queryResults.Average() ) ; Average () возвращает среднее значение из результатов запроса, которым в данном случае является 1073643807. 50298 — значение, очень близкое к середине диапазона возможных значений от 1000 до более 2 миллиардов. Это довольно бессмысленно для произвольного набора чисел, но демонстрирует возможности анализа результатов запроса. В последней части этой главы мы рассмотрим более полезное применение этих операций к некоторым бизнес-ориентированным данным. □ Sum(): Console.WriteLine("Сумма чисел > 1000"); Console.WriteLine(queryResults.Sum (n => (long) n) ) ; Обратите внимание, что вы передали лямбда-выражение n => (long) n методу Sum () для того, чтобы получить сумму всех чисел. Хотя Sum () не имеет перегрузок с другими параметрами, как Count (), Min (), Max () и т.д., использование этой версии вызова метода может привести к ошибке переполнения, потому что в результирующем наборе так много больших чисел, что их сумма может превысить значение, помещающееся в 32-битное целое, которое возвращает версия Sum () без параметров. Лямбда-выражение позволяет преобразовать результат Sum () в длинное 64-битное целое, которое нужно для хранения значения, превышающее 13 квадриллионов, не вызывая переполнения, а именно — 13 254 853 218619 179, и лямбда-выражение позволяет легко выполнить такого рода поправку.
836 Часть ГУ. Доступ к данным В дополнение к методу Count (), который возвращает 32-битное целое, LINQ также предусматривает метод LongCount (), возвращающий результат запроса в 64-битном целом. Однако это — специальный случай; все прочие операции требуют специального лямбда-выражения или вызова метода преобразования, если требуется 64-битная версия числа. Запросы сложных объектов Предыдущий пример продемонстрировал, как запросы LINQ могут работать со списками простых типов, таких как числа и строки. Теперь будет показано, как использовать запросы LINQ с более сложными объектами. Вы создадите простой класс Customer, включающий достаточно информации для испытания некоторых интересных запросов. Запрос сложных объектов Выполните следующие шаги для создания примера в Visual C# 2008. 1. Создайте новое консольное приложение по имени 26-7-QueryComplexObjects в каталоге C:\BegVCSharp\Chapter26. 2. Прежде чем запустить класс Program в Program, cs, добавьте следующее краткое определение класса Customer: class Customer { public string ID { get; set; } public string City { get; set; } public string Country { get; set; } public string Region { get; set; } public decimal Sales { get; set; } public override string ToStringO { return "ID: " + ID + " Город: " + City + " Страна: " + Country + 11 Регион: " + Region + " Продажи: " + Sales; } } 3. Добавьте следующий код к методу Main () класса Program в Program, cs: static void Main(string [ ] args) { List < Customer > customers = new List < Customer > { new Customer { ID="A", City="New York", Country="USA", Region="North America", Sales=9999}, new Customer { ID="B", City="Mumbai", Country="India", Region="Asia", Sales=8888 }, new Customer { ID="C", City="Karachi", Country="Pakistan", Region="Asia", Sales=7777 }, new Customer { ID="D", City="Delhi", Country="India", Region="Asia", Sales=6666 }, new Customer { ID="E", City="S г о Paulo", Country="Brazil", Region="South America", Sales=5555 }, new Customer { ID="F", City="Moscow", Country="Russia", Region="Europe", Sales=4444 }, new Customer { ID="G", City="Seoul"/ Country="Korea", Region="Asia", Sales=3333 },
Глава 26. Введение в LINQ 837 new Customer new Customer new Customer new Customer new Customer new Customer new Customer new Customer new Customer new Customer new Customer new Customer new Customer ID="H", Region= ID="I", Region= ID="J", Region= ID="K", Region= ID="LM, Region= ID="M"/ Region= ID="N", Region= ID=", Region= ID="P", Region= ID="Q", Region= ID="R", Region= ID="S Region= ID="T" Region: City="Istanbul", Country="Turkey", "Asia", Sales=2222 }, City="Shanghai"/ Country="China", "Asia", Sales=llll }, City="Lagos", Country="Nigeria", "Africa", Sales=1000 }, City="Mexico City", Country="Mexico", "North America", Sales=2000 }, City="Jakarta", Country="Indonesia", "Asia", Sales=3000 }, City="Tokyo", Country="Japan", "Asia", Sales=4000 }, City="Los Angeles", Country="USA", "North America", Sales=5000 }, City="Cairo", Country="Egypt", "Africa", Sales=6000 }, City="Tehran", Country="Iran", "Asia", Sales=7000 }, City="London", Country="UK", "Europe", Sales=8000 }, City="Beijing", Country="China", "Asia", Sales=9000 }, City="Bogot б ", Country="Colombia", "South America", Sales=1001 }, City="Lima", Country="Peru", "South America", Sales=2002 } var queryResults = from с in customers where c.Region == "Asia" select с Console.WriteLine("Заказчики в стране Asia:"); foreach (Customer с in queryResults) { Console.WriteLine (c); } Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Откомпилируйте и запустите программу. Ниже показан результирующий список клиентов из Азии: Заказчики в стране Asia: Mumbai Страна: India Регион: Asia Продажи: 8888 Karachi Страна: Pakistan Регион: Asia Продажи: 7777 Delhi Страна: India Регион: Asia Продажи: 6666 Seoul Страна: Korea Регион: Asia Продажи: 3333 Istanbul Страна: Turkey Регион: Asia Продажи: 2222 Shanghai Страна: China Регион: Asia Продажи: 1111 Jakarta Страна: Indonesia Регион: Asia Продажи: 3000 Tokyo Страна: Japan Регион: Asia Продажи: 4000 Tehran Страна: Iran Регион: Asia Продажи: 7000 Beijing Страна: China Регион: Asia Продажи: 9000 вершена, нажмите Enter для продолжения: ID: ID: ID: ID: ID: ID: ID: ID: ID: ID: Про В Город: С Город: D Город: G Город: Н Город: I Город: L Город: М Город: Р Город: R Город: грамма за
838 Часть IV. Доступ к данным Описание полученных результатов В определении класса Customer используется средство автоматических свойств С# 3.0 при объявлении общедоступных свойств (ID, City, Country, Region, Sales) класса Customer, не прибегая к явному кодированию приватных переменных экземпляра и кода set/get для каждого свойства: class Customer { public string ID { get; set; } public string City { get; set; } Единственный дополнительный метод, который придется закодировать в классе Customer — это переопределение метода ToString (), выдающего строковое представление для экземпляра Customer: public override string ToString () { return "ID: " + ID + " Город: " + City + " Страна: " + Country + " Регион: " + Region + " Продажи: " + Sales; } Метод ToString() используется для упрощения вывода результатов запроса, что будет показано чуть позже в этом разделе. В методе Main () класса Program создается строго типизированная коллекция типа Customer с использованием синтаксиса инициализации коллекций С# 3.0, чтобы избежать необходимости кодировать метод-конструктор и вызывать конструктор для создания каждого члена списка: List < Customer > customers = new List < Customer > { new Customer { ID="A", City="New York", Country="USA", Region="North America", Sales=9999}, new Customer { ID="B", City="Mumbai", Country="India", Region="Asia", Sales=8888 }, Ваши заказчики разбросаны по всему миру, и в данных присутствует достаточно географической информации, чтобы сформулировать интересные критерии выборки и группирования запросов. По-прежнему в методе Main () создается оператор запроса— в данном случае для выборки заказчиков из Азии: var gueryResults = from с in customers where с.Region == "Asia" select с Этот запрос должен быть хорошо знаком: это тот же LINQ-запрос from. . . where. . .select, который применялся в других примерах, но с тем отличием, что каждый элемент в результирующем списке представляет собой полноценный объект (Customer), а не простую строку или целое число. Затем в цикле foreach выводятся результаты: Console.WriteLine("Заказчики в стране Asia:"); foreach (Customer с in queryResults) { Console.WriteLine(c); }
/ Глава 26. Введение в LINQ 839 Цикл for each здесь слегка отличается от аналогичных циклов в предыдущих примерах. Поскольку известно, что запрашиваются объекты Customer, переменная итерации явно объявляется, как относящаяся к типу Customer: foreach (Customer с in queryResults) Переменную с можно было бы объявить с ключевым словом var, и компилятор вывел бы тип переменной итерации — Customer, но явное объявление делает код более понятным для читателя-человека. Внутри самого цикла вы просто пишете { Console.WriteLine (с); } вместо явной печати полей Customer, потому что в класс Customer было добавлено переопределение для метода ToStringO. Если не предоставить переопределение ToString (), метод ToString () по умолчанию просто напечатал бы имя типа, как показано ниже: Заказчики в стране Asia: BegVCSharp_2 6_7_QueryComplexObjects.Customer BegVCSharp_2 6_7_QueryComplexObj ects.Customer BegVCSharp_2 6_7_QueryComplexObj ects.Customer BegVCSharp_26_7_QueryComplexOb]ects.Customer BegVCSharp_2 6_7_QueryComplexObj ects.Customer BegVCSharp_26_7_QueryComplexObjects.Customer BegVCSharp_2 6_7_QueryComplexObjects.Customer BegVCSharp_26_7_QueryComplexObjects.Customer BegVCSharp_26_7_QueryComplexObjects.Customer BegVCSharp_26_7_QueryComplexObjects.Customer Программа завершена, нажмите Enter для продолжения: Это совсем не то, что нужно! Разумеется, можно было бы явно вывести необходимые свойства Customer: Console.WriteLine("Заказчик {0}: {1}, {2}", с.ID, с.City, с.Country); Однако если интересует только несколько свойств объекта, было бы неэффективно извлекать в запросе весь объект целиком. К счастью, LINQ позволяет просто создать результаты запроса, которые содержат только нужные элементы — через проекцию, с которой мы поэкспериментируем в следующем разделе. Проекция: создание новых объектов в запросах Проекция — это технический термин, означающий создание новых типов данных из других типов данных в запросе LINQ. Ключевое слово select представляет собой операцию проекции, которая использовалась в предшествующих примерах. Если вы знакомы с ключевым словом SELECT в языке запросов SQL, то вы уже знакомы и с операцией выбора определенного поля из объекта данных, в противоположность выбору всего объекта целиком. В LINQ также можно это делать; например, чтобы выбрать только поле City из списка Customer в предыдущем примере, просто измените конструкцию select в операторе запроса, чтобы она ссылалась только на свойство City: var queryResults = from c in customers where c.Region == "Asia" select c.City
840 Часть IV. Доступ к данным Это произведет следующий вывод: Mumbai Karachi Delhi Seoul Istanbul Shanghai Jakarta Tokyo Tehran Beijing Можно даже трансформировать данные в запросе, добавив выражение в select, как показано ниже: select n + 1 для числового типа данных, или select s.ToUpper() для запроса строкового типа данных. Однако, в отличие от SQL, LINQ не допускает множественные поля в конструкции select. Это значит, что строка select с.City, с.Country, с.Sales Вызовет ошибку компиляции (ожидается точка с запятой), потому что select принимает только один элемент в своем списке параметров. Вместо этого в LINQ "на лету" создается новый объект в конструкции select для хранения результатов, которые нужны для запроса. Сделаем это в следующем практическом занятии. Практическое занятие Проекция: СОЗДЭНИе НОВЫХ объектов в запросах Выполните следующие шаги для создания примера в Visual C# 2008. 1. Модифицируйте пример 26-7-QueryComplexObjects или создайте новое консольное приложение по имени 26-8-ProjectionCreateNewObjects в каталоге С:\BegVCSharp\Chapter2 6. 2. Если вы выберете создание нового проекта, скопируйте код для создания класса Customer и инициализацию списка заказчиков из примера 26-7-QueryComplex Objects; этот код в точности повторяет ранее использованный. 3. В методе Main () выполните инициализацию списка customers, введите (или модифицируйте) запрос и цикл обработки результатов, как показано ниже: var queryResults = from с in customers where с.Region == "North America" select new { c.City, c.Country, c.Sales } foreach (var item in queryResults) { Console.WriteLine(item); } 4. Остальной код в методе Main () такой же, как и в предыдущих примерах.
Глава 26. Введение в LINQ 841 5. Откомпилируйте и запустите программу. Вы увидите выбранные поля списка заказчиков из Северной Америки, перечисленные следующим образом: { City = New York, Country = USA, Sales = 9999 } { City = Mexico City, Country = Mexico, Sales = 2000 } { City = Los Angeles, Country = USA, Sales = 5000 } Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Класс Customer и список customers — те же, что и в предыдущем примере. В запросе вы изменили запрошенный регион на Северную Америку — просто для разнообразия. Интересное изменение, касающееся проекции, заключено в параметре конструкции select: select new { с.City, с.Country, с.Sales } Непосредственно в конструкции select используется синтаксис анонимного типа для создания нового объекта безымянного типа, имеющего свойства, City, Country и Sales. Конструкция select создает новый объект. Таким образом, только эти три свойства дублируются и обрабатываются на разных стадиях обработки запроса. При печати результатов запроса используется тот же обобщенный код цикла foreach, который применялся в предыдущих примерах, за исключением запроса Customer: foreach (var item in queryResults) { Console.WriteLine(item); } Этот код полностью обобщен; компилятор выводит тип результата запроса и вызывает правильные методы для анонимного типа без необходимости явного кодирования чего бы то ни было. Вам даже не нужно представлять переопределение ToString (), поскольку компилятор предоставляет реализацию ToString () по умолчанию, которая печатает имена свойств и значений в манере, подобной инициализации объекта. Проекция: синтаксис методов Версия в синтаксисе методов запроса проекции создается подстановкой вызова метода LINQ по имени Select () наряду с другими вызываемыми методами LINQ. Например, тот же результат запроса можно получить, если добавить вызов метода Select () к вызову Where (), как показано ниже: var queryResults = customers.Where (с => с.Region == "North America") .Select(с => new { c.City, c.Country, c.Sales }); Хотя конструкция select обязательна в синтаксисе запроса, вы еще не видели ранее вызова метода Select (), потому что он не обязателен в синтаксисе метода LINQ, если только не выполняется проекция (изменение типа результирующего набора по сравнению с исходным запрашиваемым типом). Порядок вызовов методов не фиксирован, потому что все типы возврата методов LINQ реализуют интерфейс IEnumerable — можно вызвать Select () на результате Where () и наоборот. Однако порядок может быть важен, в зависимости от специфики запроса. Например, можно было бы поменять порядок вызовов Select () и Where () следующим образом: var queryResults = customers.Select(с => new { c.City, с.Country, c.Sales }) .Where(c => c.Region == "North America");
842 Часть IV. Доступ к данным Свойство Region не включено в анонимный тип { с.City, с.Country, с.Sales }, созданный проекцией Select (), поэтому программа приведет к ошибке компиляции в методе Where (), указывающей на то, что анонимный тип не содержит определения Region. Однако если бы метод Where () был ограничен данными, основанными на поле, которое включено в анонимный тип, таким как City, то проблем бы не было. Например, следующий запрос компилируется и выполняется без проблем: var queryResults = customers.Select(с => new {с.City, с.Country, с.Sales }) .Where(с => с.City == "New York"); Запрос Select Distinct Другой тип запросов, который узнают те, кто знаком с языком запросов данных SQL — запрос SELECT DISTINCT, в котором выполняется поиск уникальных значений в данных, т.е. значений, которые не повторяются. Потребность в этом возникает очень часто при работе с запросами. Предположим, что нужно извлечь список регионов из данных заказчиков, которые применялись в предыдущих примерах. В используемых данных нет отдельного списка регионов, поэтому необходимо получить уникальный, не повторяющийся список регионов из списка заказчиков. LINQ предоставляет метод Distinct (), который облегчает выполнение задачи нахождения данных подобного рода. Этот метод используется его в следующем практическом занятии. ШШтышмшшшашшшашшшяштяяшашшшшшшшашшяшшшшшашшашшшштятшшшш практическое занятие Проекция: запрос Select Distinct Выполните следующие шаги для создания примера в Visual C# 2008. 1. Модифицируйте предыдущий пример, 26-8-ProjectionCreateNewObjects, или создайте новое консольное приложение по имени 26-9-SelectDistinctQuery в каталоге С:\BegVCSharp\Chapter26. 2. Скопируйте код создания класса Customer и инициализации списка customers (List<Customer> customers) из примера 26-7-QueryComplesObjects; код тот же самый. 3. В методе Main () вслед за инициализацией списка заказчиков введите (или модифицируйте) запрос следующим образом: var queryResults = customers.Select (с => с.Region) .Distinct (); 4. Остальной код в методе Main () оставьте без изменений, как в предыдущем примере. 5. Откомпилируйте и выполните программу. Вы увидите уникальный список регионов, в которых находятся заказчики: North America Asia South America Europe Africa Программа завершена, нажмите Enter для продолжения:
Глава 26. Введение в LINQ 843 Описание полученных результатов Класс Customer и инициализация списка customers остаются теми же, что и в предыдущем примере. В операторе запроса вызывается метод Select () с простым лямбда-выражением для выбора региона из объектов Customer, а затем вызывается Distinct (), чтобы вернуть из Select () только уникальные результаты: var queryResults = customers.Select (с => с.Region) .Distinct (); Поскольку Distinct () доступен только в синтаксисе методов, применяется вызов Select () из синтаксиса методов. Однако вызвать Distinct () для модификации запроса можно и в синтаксисе запросов: var queryResults = (from с in customers select с.Region).Distinct (); Поскольку синтаксис запросов транслируется компилятором С# 3.0 в те же серии вызовов методов LINQ, как и в случае синтаксиса методов, можно смешивать их в тех случаях, когда это оправдано для читабельности и стиля. Any и All Другой тип запроса, который часто может понадобиться, состоит в определении того, что данные удовлетворяют определенному условию, или в определении того, что все данные удовлетворяют условию. Например, может потребоваться знать, что продукта нет на складе (количество равно нулю) или что транзакция произошла. LINQ предоставляет булевские методы Any () и All (), которые могут быстро сообщить, истинно ли некоторое условие для существующих данных. Это облегчает задачу поиска данных, которую придется выполнять в следующем практическом занятии. шшншшшаяшшашшшшшашшшшаашшшшаашшашшяаяшшшяшашшшяшшшшш практическое занятие Использование Any и All Выполните следующие шаги для создания примера в Visual C# 2008. 1. Модифицируйте предыдущий пример 2 6-9-SelectDistinctQuery или создайте новое консольное приложение по имени 2 6-10-AnyAndAll в каталоге С:\BegVCSharp\Chapter2 6. 2. Скопируйте код создания класса Customer и инициализации списка customers (List<Customer> customers) из примера 26-7-QueryComplexObjects; этот код тот же самый. 3. В методе Main () после инициализации списка customers и объявления запроса удалите цикл обработки и введите код, как показано ниже: bool anyUSA = customers.Any(с => с.Country == "USA"); if (anyUSA) { Console.WriteLine("Некоторые заказчики находятся в США"); } else { Console.WriteLine("Нет заказчиков из США"); } bool allAsia = customers.All(с => с.Region == "Asia"); if (allAsia) { Console.WriteLine("Все заказчики находятся в Азии"); • }
844 Часть IV. Доступ к данным else { Console.WriteLine("He все заказчики находятся в Азии"); } 4. Остальной код в методе Main () такой же, как и в предыдущем примере. 5. Откомпилируйте и выполните программу. Вы увидите сообщения, указывающие на то, что некоторые заказчики находятся в США, но не все — в Азии: Некоторые заказчики находятся в США Не все заказчики находятся в Азии Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Класс Customer и инициализация списка customers в предыдущих примерах одинаковы. В первом операторе запроса вызывается метод Any () с простым лямбда-выражением для проверки, имеет ли поле Country в Customer значение USA: bool anyUSA = customers.Any(с => с.Country == "USA"); Метод LINQ Any () применяет переданное ему лямбда-выражение с => с. Country == "USA" ко всем данным в списке customers и возвращает true, если это лямбда-выражение истинно для любого заказчика в списке. Затем проверяется результирующая булевская переменная, возвращенная методом Any (), и печатается сообщение, отражающее результат запроса (даже несмотря на то, что метод Any () просто возвращает true или false, он выполняет запрос для получения результата true/false): if (anyUSA) { Console.WriteLine("Некоторые заказчики находятся в США"); } else { Console.WriteLine("Нет заказчиков из США"); } Хотя можно сделать это сообщение более компактным, применив некоторый хитрый код, оно более наглядно и читаемо в таком виде, как показано здесь. Как и следовало ожидать, переменная anyUSA установлена в true, потому что на самом деле в наборе данных присутствуют заказчики, находящиеся в США, поэтому вы видите сообщение Некоторые заказчики находятся в США. В следующем операторе запроса вызывается метод All () с другим простым лямбда-выражением, чтобы определить, все ли заказчики находятся в Азии: bool allAsia = customers.All (с => с.Region == "Asia"); Метод LINQ All () применяет переданное ему лямбда-выражение к набору данных и возвращает false, как и следовало ожидать, потому что некоторые из заказчиков находятся не в Азии. Затем на основе значения allAsia печатается соответствующее сообщение. Многоуровневое упорядочивание Имея дело с объектами, включающими несколько свойств, вы можете столкнуться с ситуацией, когда упорядочивания результатов запроса по единственному полю недостаточно. Что, если вы захотите запросить заказчиков и упорядочить результаты в
Глава 26. Введение в LINQ 845 алфавитном порядке по региону, а затем — по стране или названию города внутри региона? LINQ позволяет сделать это очень легко, как вы убедитесь в следующем практическом занятии. Практическое Многоуровневое упорядочивание Выполните следующие шаги для создания примера в Visual C# 2008. 1. Модифицируйте предыдущий пример, 26-8-ProjectionCreateNewObjects, или создайте новое консольное приложение по имени 26-11-MultiLevelOrdering в каталоге C:\BegVCSharp\Chapter26. 2. Создайте класс Customer и инициализацию списка customers, как показано в примере 26-7-QueryComplexObjects; этот код точно такой же, как и в предыдущих примерах. 3. В методе Main () после инициализации списка заказчиков введите следующий запрос: var queryResults = from с in customers orderby с.Region, с.Country, с.City select new { c.ID, c.Region, c.Country, c.City } 4. Цикл обработки запроса и остальной код метода Main () — точно такие же, как и в предыдущих примерах. 5. Откомпилируйте и выполните программу. Вы увидите выбранные свойства из customers, упорядоченные по алфавиту сначала по региону, затем по стране и, наконец, по городу, как показано ниже: ID = О, Region = Africa, Country = Egypt, City = Cairo } ID = J, Region = Africa, Country = Nigeria, City = Lagos } R, Region = Asia, Country = China, City = Bering } M ID ID ID ID ID ID ID ID ID ID ID ID = Q, Region = Europe, Country = UK, City = London } ID = K, Region = North America, Country = Mexico, City = Mexico City North America, Country = USA, City = Los Angeles } North America, Country = USA, City = New York } South America, Country = Brazil, City = Sao Paulo } Colombia, City = Bogot б } Peru, City = Lima } I, Region = Asia, Country D, Region = Asia, Country B, Region = Asia, Country L, Region = Asia, Country P, Region = Asia, Country Region = Asia, Country G, Region = Asia, Country C, Region = Asia, Country H, Region = Asia, Country F, Region = Europe China, City = Shanghai } India, City = Delhi } India, City = Mumbai } Indonesia, City = Jakarta Iran, City = Tehran } Japan, City = Tokyo } Korea, City = Seoul } Pakistan, City = Karachi } Turkey, City = Istanbul } Country = Russia, City = Moscow } ID = N, Region ID = A, Region ID = E, Region ID = S, Region = South America, Country ID = T, Region = South America, Country Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Класс Customer и список инициализации customers — такие же, как и в предыдущих примерах. В этом запросе нет конструкции where, потому что нужно увидеть всех
846 Часть IV. Доступ к данным заказчиков, но перечисляются только те поля, которые необходимо отсортировать по порядку, в разделенной запятыми последовательности в конструкции о г derby: orderby с.Region, с.Country, с.City Проще некуда, не правда ли? Кажется несколько не интуитивным, что простой список полей допускается в конструкции orderby, но не допускается в конструкции select, но так уж работает LINQ. Это имеет смысл, если вы поймете, что конструкция select создает новый объект, а конструкция orderby по определению оперирует полями. Можно добавить ключевое слово descending к любому из перечисленных полей, чтобы изменить порядок сортировки для этого поля. Например, чтобы упорядочить этот запрос по возрастанию региона, но по убыванию страны, просто добавьте в список descending после country: orderby с.Region, с.Country descending, с.City После этого вы увидите следующее: ID = J, Region = Africa, Country = Nigeria, City = Lagos } ID = 0, Region = Africa, Country = Egypt, City = Cairo } ID = H, Region = Asia, Country = Turkey, City = Istanbul } ID = C, Region = Asia, Country = Pakistan, City = Karachi } ID = G, Region = Asia, Country = Korea, City = Seoul } ID = M, Region = Asia, Country = Japan, City = Tokyo } ID = P, Region = Asia, Country = Iran, City = Tehran } ID = L, Region = Asia, Country = Indonesia, City = Jakarta } ID = D, Region = Asia, Country = India, City = Delhi } ID = B, Region = Asia, Country = India, City = Mumbai } ID = R, Region = Asia, Country = China, City = Beijing } ID = I, Region = Asia, Country = China, City = Shanghai } ID = Q, Region = Europe, Country = UK, City = London } ID = F, Region = Europe, Country = Russia, City = Moscow } ID = N, Region = North America, Country = USA, City = Los Angeles } ID = A, Region = North America, Country = USA, City = New York } ID = K, Region = North America, Country = Mexico, City = Mexico City } ID = T, Region = South America, Country = Peru, City = Lima } ID = S, Region = South America, Country = Colombia, City = Bogot б } ID = E, Region = South America, Country = Brazil, City = Sao Paulo } Программа завершена, нажмите Enter для продолжения: Обратите внимание, что города Индии и Китая по-прежнему расположены в порядке возрастания, в то время как порядок стран изменился на противоположный. Синтаксис методов многоуровневого упорядочивания: ThenBy Если заглянуть за кулисы многоуровневого упорядочивания, использующего синтаксис методов, ThenBy () и OrderBy (), то там все несколько сложнее. Например, вы получите тот же результат, как и в предыдущем примере, если напишете следующий код: var queryResults = customers.OrderBy(с => с.Region) .ThenBy(c => с.Country) .ThenBy(c => с.City) .Select(с => new { с ID, c.Region, c.Country, c.City });
Глава 26. Введение в LINQ 847 Теперь более очевидно, почему список из многих полей допускается в конструкции orderby в синтаксисе запросов; вы можете видеть здесь, что он транслируется в серию вызовов метода ThenBy () для каждого поля. Порядок записи вызовов важен: вы должны начать с OrderBy (), потому что метод ThenBy () допускается только на интерфейсе IOrderedEnumerable, возвращаемом OrderBy (). Однако метод ThenBy () может быть соединен с другим методом ThenBy () столько раз, сколько нужно. Это тот случай, когда синтаксис запросов явно легче написать, чем синтаксис методов. Убывающий порядок специфицируется вызовом либо OrderByDescending () на первом поле, подлежащем сортировке в убывающем порядке, либо вызовом ThenByDescending (), если любое из остальных полей должно быть отсортировано в убывающем порядке. Чтобы отсортировать страны в порядке убывания, как в этом примере, запрос в синтаксисе методов должен выглядеть следующим образом: var queryResults = customers.OrderBy(с => с.Region) .ThenByDescending (с => с.Country) .ThenBy(c => с.City) .Select (с => new { с.ID, с.Region, с.Country, с.City }); Групповые запросы Групповой запрос делит данные на группы и позволяет сортировать, вычислять агрегатные значения и сравнивать группы. Это часто наиболее интересные запросы в контексте бизнеса (те, что действительно используются для принятия решений). Например, вы можете пожелать сравнить продажи по стране и региону, чтобы решить, стоит ли открывать новый магазин или нанимать дополнительный персонал. Сделаем это в следующем практическом занятии. ПпАУтиАГКПА 1QUOTUA I практическое занятие { ГруППОВОЙ ЗЭПрОС Выполните следующие шаги для создания примера в Visual C# 2008. 1. Создайте новое консольное приложение по имени 26-12-GroupQuery в каталоге C:\BegVCSharp\Chapter26. 2. Создайте класс Customer и инициализируйте список customers (List<Customer> customers), как показано в примере 26-7-QueryComplexObjects; этот код в точности совпадает с предыдущим примером. 3. В методе Main () вслед за инициализацией списка customers введите два запроса: var queryResults = from с in customers group с by с.Region into eg select new { TotalSales = cg.Sum(c => c.Sales), Region = eg.Key } var orderedResults = from eg in queryResults orderby eg.TotalSales descending select eg 4. Продолжая метод Main (), добавьте следующий оператор print и цикл обработки foreach:
848 Часть ГУ. Доступ к данным Console.WnteLine("Bcero\t: Пo\nпpoдaж\t: региону\п \t " ) ; foreach (var item in orderedResults) { Console.WnteLine (item.TotalSales + "\t: " + item.Region); } 5. Цикл обработки результатов и остальной код метода Main () такой же, как и в предыдущих примерах. Откомпилируйте и выполните программу. Вот результат group: Всего : По продаж : региону 52997 : Asia 16999 : North America 12444 : Europe 8558 : South America 7000 : Africa Описание полученных результатов Класс Customer и инициализация списка customers такие же, как и в предыдущих примерах. Данные в групповом запросе группируются по ключевому полю — полю, для которого все члены каждой группы разделяют общее значение. В этом примере ключевым полем является Region: group с by с.Region Необходимо вычислить сумму по каждой группе, поэтому выполняется группирование в новый результирующий набор по имени eg: group с by с.Region into eg В конструкции select проектируется новый анонимный тип, чьи свойства — общая сумма продаж (вычисляемая по обращению к результирующему набору eg) и ключевое значение группы, к которому вы обращаетесь по специальному полю Key группы: select new { TotalSales = cg.Sum(c => с.Sales), Region = eg.Key } Групповой результирующий набор реализует интерфейс LINQ IGrouping, который поддерживает свойство Key. Вы почти всегда захотите каким-то образом обратиться к свойству Key при обработке групповых результатов, потому что оно представляет критерий создания каждой группы ваших данных. Вы хотите упорядочить результат в порядке убывания по полю TotalSales, чтобы увидеть, какой регион имеет максимальный уровень общих продаж, какой регион занимает по этому показателю второе место, третье и т.д. Чтобы добиться этого, вы создаете второй запрос для упорядочивания результатов группового запроса: var orderedResults = from eg in queryResults orderby eg.TotalSales descending select eg Второй запрос — стандартный запрос select с конструкцией orderby, как было показано в предыдущих примерах; он не использует никаких групповых средств LINQ, за исключением того, что источник данных для него поступает от предыдущего группового запроса.
Глава 26. Введение в LINQ 849 Затем печатаются результаты с небольшой долей форматирующего кода для отображения данных с заголовками столбцов и некоторыми разделителями между суммами и именами групп: Console.WriteLine("BceroXt: По\ппродаж\1: региону\п \t "); foreach (var item in orderedResults) { Console.WriteLine(item.TotalSales + "\t: " + item.Region); }; Вывод может быть форматирован более изощренным образом с указанием ширин полей и способа выравнивания итоговых сумм, но это — только пример, поэтому заботиться об этом не обязательно — вы и так можете достаточно ясно увидеть результирующие данные и понять, что делает код. Take и Skip Предположим, что нужно найти первые пять заказчиков по уровню продаж в существующем наборе данных. Вы не знаете заранее, какое количество продаж позволит включить заказчика в группу из пяти "передовиков", поэтому вы не можете использовать условие where для их поиска. Некоторые базы данных SQL, такие как Microsoft SQL Server, реализуют операцию ТОР, так что можно выдать команду вроде SELECT TOP 5 FROM. . ., чтобы получить первые пять заказчиков. LINQ-эквивалент этой операции представлен методом Take (), который принимает первые п результатов в выводе запроса. При практическом применении это должно быть скомбинировано с orderby для получения первых п результатов. Однако применять orderby не обязательно, поскольку могут быть ситуации, когда заранее известно, что данные уже находятся в нужном порядке, или же когда по каким-то причинам необходимо получить первые п результатов, не заботясь об их порядке. Противоположностью Take () является метод Skip (), который пропускает первые п результатов, возвращая остальные. Take () и Skip () называют в документации LINQ разбивающими операциями (partitioning operators), потому что они разбивают результирующий набор на первые п результатов (Take ()) и/или остальные (Skip ()). Практическое занятие Работа С Take И Skip Выполните следующие шаги для создания примера в Visual C# 2008. 1. Создайте новое консольное приложение по имени 26-13-TakeAndSkip в каталоге С:\BegVCSharp\Chapter2 6. 2. Скопируйте код создания класса Customer и инициализации списка customers (List<Customer> customers) из примера 2 6-7-QueryComplexObjects. 3. В методе Main () вслед за инициализацией списка customers введите следующий запрос: // Синтаксис запроса var queryResults = from с in customers orderby с.Sales descending select new { c.ID, c.City, c.Country, c.Sales }
850 Часть IV. Доступ к данным 4. Введите два цикла обработки результатов, один с использованием Take (), а другой — с применением Skip (): Console.WriteLine("Верхняя пятерка заказчиков по результатам продаж"); foreach (var item in queryResults.TakeE)) { Console.WriteLine(item); } Console.WriteLine("Заказчики, не вошедшие в верхнюю пятерку"); foreach (var item in queryResults.SkipE)) { Console.WriteLine(item); } 5. Откомпилируйте и выполните программу. Вы увидите пять ведущих заказчиков и затем остальных заказчиков, как показано ниже: Верхняя пятерка заказчиков по результатам продаж ID = A, City = New York, Country = USA, Sales = 9999 } ID = R, City = Beijing, Country = China, Sales = 9000 } ID = B, City = Mumbai, Country = India, Sales = 8888 } ID = Q, City = London, Country = UK, Sales = 8000 } ID = C, City = Karachi, Country = Pakistan, Sales = 7777 } Заказчики, не вошедшие в верхнюю пятерку ID = P, City = Tehran, Country = Iran, Sales = 7000 } ID = D, City = Delhi, Country = India, Sales = 6666 } ID = 0, City = Cairo, Country = Egypt, Sales = 6000 } ID = E, City = Sao Paulo, Country = Brazil, Sales = 5555 } ID = N, City = Los Angeles, Country = USA, Sales = 5000 } ID = F, City = Moscow, Country = Russia, Sales = 4444 } ID = M, City = Tokyo, Country = Japan, Sales = 4000 } ID = G, City = Seoul, Country = Korea, Sales = 3333 } ID = L, City = Jakarta, Country = Indonesia, Sales = 3000 } ID = H, City = Istanbul, Country = Turkey, Sales = 2222 } ID = T, City = Lima, Country = Peru, Sales = 2002 } ID = K, City = Mexico City, Country = Mexico, Sales = 2000 } ID = I, City = Shanghai, Country = China, Sales = 1111 } ID = S, City = Bogot б , Country = Colombia, Sales = 1001 } ID = J, City = Lagos, Country = Nigeria, Sales = 1000 } Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Класс Customer и инициализация списка customers — такие же, как и в предыдущих примерах. Основной запрос состоит из конструкции from. . . orderby. . . select в синтаксисе запросов, подобный ранее созданным в этой главе, но с тем отличием, что здесь отсутствует ограничивающая конструкция where, потому что необходимо получить всех заказчиков (упорядоченных по объемам продаж от большего к меньшему): var queryResults = from с in customers orderby с.Sales descending select new { c.ID, c.City, c.Country, c.Sales } Этот пример работает несколько иначе, чем предыдущие, в том смысле, что вы не применяете операцию до тех пор, пока не начнете цикл foreach на его результатах, поскольку хотите повторно использовать эти результаты.
Глава 26. Введение в LINQ 851 Сначала применяется Take E) для получения первых пяти заказчиков: foreach (var item in queryResults.TakeE)) Затем используется Skip E) для пропуска первых пяти элементов (которые уже были напечатаны) и распечатки остальных заказчиков из того же исходного набора результатов запроса: foreach (var item in queryResults.SkipE)) Код для печати результатов и приостановки вывода на экран такой же, как в предыдущих примерах, за исключением небольших изменений в сообщениях, поэтому мы не станем повторять его здесь. First и FirstOrDefault Предположим, что требуется найти пример заказчика из Африки в наборе данных. Нужны сами данные, а не значение true/false или результирующий набор соответствующих значений. LINQ предоставляет эту возможность через метод First (), который возвращает первый элемент результирующего набора, отвечающий заданным критериям. Если нет ни одного заказчика из Африки, то LINQ предоставляет метод для обработки такого случая без дополнительного кода обработки ошибок: FirstOrDef ault (). В следующем практическом занятии используется как First (), так и FirstOrDef ault () со списком данных о заказчиках. Практическое занятие Использование First и FirstOrDef ault Выполните следующие шаги для создания примера в Visual C# 2008. 1. Создайте новое консольное приложение по имени 26-14-FirstOrDefault в каталоге C:\BegVCSharp\Chapter26. 2. Скопируйте код создания класса Customer и инициализации списка customers (List<Customer> customers) из примера 26-7-QueryComplexObjects. 3. В методе Main () вслед за инициализацией списка заказчиков введите следующий запрос: var queryResults = from с in customers select new { c.City, c.Country, c.Region } 4. Введите следующие запросы, использующие First () и FirstOrDef ault (): Console.WriteLine("Заказчик из Африки"); Console.WriteLine(queryResults.First(с => c.Region == "Africa")); Console.WriteLine("Заказчик из Антарктиды"); Console.WriteLine(queryResults.FirstOrDefault(с => c.Region == "Antarctica")); 5. Откомпилируйте и выполните программу. Вот результирующий вывод: Заказчик из Африки { City = Lagos, Country = Nigeria, Region = Africa } Заказчик из Антарктиды Программа завершена, нажмите Enter для продолжения:
852 Часть IV. Доступ к данным Описание полученных результатов Класс Customer и инициализация списка customers — такие же, как в предыдущих примерах. Основной запрос состоит из конструкции from. . . orderby. . . select в синтаксисе запросов, как и те, что создавались ранее в этой главе, но без частей where и orderby. Вы проектируете поля, представляющие интерес, с помощью select, в этом случае — свойства City, Country и Region: var queryResults = from с in customers select new { c.City, c.Country, c.Region } Поскольку операция First () возвращает значение единственного объекта, а не результирующий набор, создавать цикл f oreach не нужно; вместо этого результат печатается непосредственно: Console.WriteLine(queryResults.First(с => с.Region == "Africa")); Это находит заказчика, и результат City = Lagos, Country = Nigeria, Region = Africa выводится на печать. Затем осуществляется запрос заказчика из Антарктиды с использованием FirstOrDefault (): Console.WriteLine(queryResults.FirstOrDefault(с => с.Region == "Antarctica")); Это не находит никакого реального результата, поэтому возвращается null (пустой результат), который выглядит как пустая строка. Что случилось бы, используй вы операцию First () вместо FirstOrDefault () в таком запросе? В этом случае было бы получено следующее исключение: System.InvalidOperationException: Sequence contains no matching element System. InvalidOperationException: Последовательность не содержит совпадающего элемента Вместо этого FirstOrDefault () возвращает элемент по умолчанию для списка, если критерий поиска не удовлетворяется ни одной записью, и этим элементом по умолчанию является null для данного анонимного типа. Иначе для региона Антарктиды возникло бы исключение. Код печати результатов и приостановки вывода — такой же, как и в предыдущих примерах, за исключением небольших изменений в экранных сообщениях. Операции с множествами LINQ поддерживает стандартные операции множеств, такие как Union () и Intersect (), оперирующие результатами запроса. Вы использовали одну из этих операций, когда ранее писали запрос Distinct (). В следующем практическом занятии вы добавите простой список заказов, который прислан гипотетическим заказчиком, и воспользуетесь стандартными операциями множеств для сопоставления заказов с существующими заказчиками. практическое занятие Операции множеств Выполните следующие шаги для создания примера в Visual C# 2008. 1. Создайте новое консольное приложение по имени 2 6-15-SetOperators в каталоге C:\BegVCSharp\Chapter26. 2. Скопируйте код создания класса Customer и инициализации списка customers (List<Customer> customers) из примера 26-7-QueryComplexObjects.
Глава 26. Введение в LINQ 853 3. После объявления класса Customer добавьте следующий класс Order: class Order { public string ID { get; set; } public decimal Amount { get; set; } } 4. В методе Main (), вслед за инициализацией списка customers, создайте и инициализируйте список orders данными, показанными ниже: List<0rder> orders = new List<0rder> { new Order new Order new Order new Order new Order new Order new Order new Order new Order new Order new Order ID="P", ID="Q", ID="R", ID="S", ID="T", ID=MU", ( ID="V", ID=MW", ID="X", ID="Y", ID="Z", Amount=100 }, Amount=200 }, Amount=300 }, Amount=400 }, Amount=500 }, Amount=600 }, Amount=700 }, Amount=800 }, Amount=900 }, Amount=1000 } Amount=1100 } 5. Вслед за инициализацией списка orders введите такие запросы: var customerlDs = from с in customers select с.ID var orderlDs = from о in orders select o.ID 6. Введите следующий запрос, использующий Intersect (): var customersWithOrders = customerlDs.Intersect(orderlDs); Console.WriteLine("ID заказчиков с заказами:"); foreach (var item in customersWithOrders) { Console.Write("{0} ", item); } Console.WriteLine(); 7. Затем введите следующий запрос, использующий Except (): Console.WriteLine ("ID заказов без заказчиков:"); var ordersNoCustomers = orderlDs.Except(customerlDs); foreach (var item in ordersNoCustomers) Console.Write("{0} item)j Console.WriteLine() ; 8. И, наконец, введите следующий запрос, использующий Union (): Console.WriteLine("Все ID заказчиков и заказов:"); var allCustomerOrderlDs = orderlDs.Union(customerlDs); foreach (var item in allCustomerOrderlDs) Console.Write("{0} } Console.WriteLine() ; item)
854 Часть ГУ. Доступ к данным 9. Откомпилируйте и выполните программу. Вы получите такой вывод: ID заказчиков с заказами: Р Q R S Т ID заказов без заказчиков: U V W X Y Z Все ID заказчиков и заказов: PQRSTUVWXYZABCDEFGHIJKLMNO Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Класс Customer и инициализация списка customers — те же, что и в предыдущих примерах. Новый класс Order подобен классу Customer и использует средство автоматических свойств С# 3.0 для объявления общедоступных свойств (ID, Amount): class Order { public string ID { get; set; } public decimal Amount { get; set; } } Как и класс Customer, это является упрощенным примером с количеством данных, достаточным для демонстрации запроса. Вы используете два простых запроса from. . .select для получения полей ID из классов Customer и Order, соответственно: var customerlDs = from с in customers select с. ID var orderlDs = from о in orders select o.ID Затем с помощью операции множества Intersect () находятся только ID заказчиков, которые имеют соответствующие заказы в результирующем наборе order IDs. В пересечение включаются только те ID, что появляются в обоих результирующих наборах: var customersWithOrders = customerlDs.Intersect(orderlDs); Операции множеств требуют, чтобы члены множеств имели одинаковый тип, для гарантии ожидаемого результата. Здесь используется преимущество того факта, что ID в обоих типах объектов являются строками и имеют одинаковую семантику (подобно внешним ключам в базе данных). Вывод результатов использует тот факт, что ID представлены одиночным символом, поэтому применяется вызов Console.Write () без WriteLine О до конца цикла f oreach, чтобы сделать вывод компактным и четким: Console.WriteLine("ID заказчиков с заказами:"); foreach (var item in customersWithOrders) { Console.Write("{0} ", item); } Console.WriteLine(); Ту же логику печати вы используете и в остальных циклах foreach.
Глава 26. Введение в LINQ 855 Затем с помощью операции Except () находятся ID заказов, которые не имеют соответствующего заказчика: Console.WriteLine ("ID заказов без заказчиков:"); var ordersNoCustomers = orderlDs.Except(customerlDs); И, наконец, посредством операции Union () получается объединение всех полей ID заказчиков с полями ID заказов: Console.WriteLine ("Все ID заказчиков и заказов:"); var allCustomerOrderlDs = orderlDs.Union(customerlDs); Обратите внимание, что ID выводятся в том же порядке, в каком они появляются в списках заказчиков и заказов, с исключением дубликатов. Код приостановки вывода на экран такой же, как и в предыдущих примерах. Операции множеств удобны, хотя практическая польза от их применения ограничена требованием, чтобы все объекты, которыми они манипулируют, относились к одному и тому же типу. Эти операции удобны в определенных узких ситуациях, когда нужно манипулировать наборами одинаково типизированных результатов, но в наиболее типичном случае, когда необходимо работать с разными связанными типами объектов, понадобится более практичный механизм работы с разными типами, такой как оператор соединения join. Соединения Наборы данных, подобные спискам customers и orders, которые были только что созданы с общим ключевым полем (ID) позволяют использовать запрос join, посредством которого можно запросить взаимосвязанные данные из обоих списков в единственном запросе, объединяя их по значению ключевого поля. Это подобно оператору JOIN в языке SQL. Как и можно было ожидать, LINQ предоставляет команду join в синтаксисе запросов, которой вы воспользуетесь в следующем практическом занятии. Практическое занятие Получение СОеДИНвНИЯ Выполните следующие шаги для создания примера в Visual C# 2008. 1. Создайте новое консольное приложение по имени 26-16-JoinQuery в каталоге С:\BegVCSharp\Chapter2 6. 2. Скопируйте создание классов Customer и Order, а также инициализации списков customers (List<Cistomer> customers)и orders (List<Order> orders) из предыдущего примера; этот код здесь точно такой же. 3. В методе Main () вслед за инициализацией списков customers и orders введите следующий запрос: var queryResults = from с in customers join о in orders on c.ID equals o.ID select new { c.ID, c.City, SalesBefore = c.Sales, NewOrder = o.Amount, SalesAfter = c.Sales+o.Amount }; 4. Завершите программу стандартным циклом обработки запроса f oreach, который применялся в предыдущих примерах:
856 Часть ГУ. Доступ к данным foreach (var item in queryResults) { Console.WriteLme (item) ; } 5. Откомпилируйте и выполните программу. Вы получите следующий вывод: { ID = P, City = Tehran, SalesBefore = 7000, NewOrder = 100, SalesAfter = 7100 } { ID = Q, City = London, SalesBefore = 8000, NewOrder = 200, SalesAfter = 8200 } { ID = R, City = Beijing, SalesBefore = 9000, NewOrder = 300, SalesAfter = 9300 } { ID = S, City = Bogot б , SalesBefore = 1001, NewOrder = 400, SalesAfter = 1401 } { ID = T, City = Lima, SalesBefore = 2002, NewOrder = 500, SalesAfter = 2502 } Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Код объявления и инициализации класса Customer и класса Order, а также списков customers и orders — такой же, как и в предыдущем примере. В запросе ключевое слово join служит для соединения каждого заказчика с соответствующими ему заказами, используя поле ID из классов Customer и Order, соответственно: var queryResults = from с in customers join о in orders on c.ID equals o.ID Ключевое слово on следует за именем поля ключа (ID), а ключевое слово equals указывает на соответствующее поле в другой коллекции. Результат запроса включает только данные объектов, имеющих то же значение поля ID, что и соответствующее поле ID в другой коллекции. Оператор select проектирует новый тип данных со свойствами, названными так, что вы можете ясно видеть исходную сумму продаж, новый заказ и результирующую новую сумму: select new { c.ID, с.City, SalesBefore = с.Sales, NewOrder = о.Amount, SalesAfter = с.Sales+o.Amount }; Хотя вы не увеличиваете объем продаж в объекте customer этой программы, это делается легко. Логика цикла foreach и отображение значений, возвращенных запросом, в точности такие же, как и в предыдущих программах настоящей главы. Ресурсы для дополнительного изучения Методов LINQ, доступных при использовании синтаксиса методов, слишком много, чтобы охватить их все в книге для начинающих. Дополнительные детали примеров ищите в онлайновой документации Microsoft no LINQ. Краткие примеры каждого метода LINQ вы найдете в разделе 01 LINQ Samples" в справочной системе MSDN (или по адресу http: //msdn2 .microsoft. com/en-us/vcsharp/aa33674 6.aspx). Руководство, составленное Эриком Байтом (Eric White) по функциональному программированию на http://blogs.msdn.com/ericwhite/pages/FP-Tutorial.aspx является хорошим источником информации на эту тему в контексте LINQ. Также оно представляет краткое руководство по синтаксису методов LINQ.
Глава 26. Введение в LINQ 857 Резюме В этой главе рассмотрены следующие вопросы. □ Различные вариации LINQ, включая LINQ to Objects, LINQ to SQL и LINQ to XML. □ Как кодировать запросы LINQ для LINQ to Objects и части операторов запросов LINQ, включая конструкции from, where, select и orderby. □ Как использовать цикл f о reach для выполнения итерации по результатам запроса LINQ и как отложить выполнение запроса до запуска оператора f oreach. □ LINQ построен на базе концепции расширяющих методов. Вы узнали, как использовать синтаксис методов и лямбда-выражения. □ Как использовать агрегатные операции LINQ для получения информации о крупных наборах данных, не выполняя итерации по каждому элементу результата. □ Как использовать проекцию для изменения типов данных и создания новых объектов в запросах. □ Как использовать операции Distinct (), Any (), All (), First (), FirstOrDef ault (), Take() и Skip(). □ Групповые запросы и упорядочивание на нескольких уровнях. □ Операции множеств и соединения. Как вы убедились, LINQ делает запросы, написанные на "родном" С#, довольно легкими и мощными. В следующей главе вы узнаете, как использовать LINQ to SQL для запросов реляционных баз данных и эффективной работы с наборами данных. Упражнения 1. Модифицируйте программу из первого примера B 6-1-FirstLINQquery) для упорядочивания результатов в убывающем порядке. 2. Модифицируйте число, переданное методу generateLotsOfNumbers () в примере с большими числами B 6-5-LargeNumberQuery), для создания результирующих наборов разного размера и посмотрите, как это повлияет на результаты запроса. 3. Добавьте конструкцию orderby к запросу в примере с большими числами B6-5-LargeNumberQuery), чтобы увидеть, как это влияет на производительность. 4. Модифицируйте условия запроса в примере программы с большими числами B6-5-LargeNumberQuery) для выбора больших и меньших подмножеств списка чисел. Как это влияет на производительность? 5. Модифицируйте пример синтаксиса методов B6-2-LINQMethodSyntax), чтобы полностью исключить конструкцию where. Насколько объемный вывод он сгенерирует? 6. Модифицируйте пример программы запроса сложных объектов Bб-7-Query ComplexObjects) для выбора другого подмножества полей запроса с условием, соответствующим этим полям. 7. Добавьте агрегатные операции к программе из первого примера B6-1-First LINQquery). Какие простые агрегатные операции доступны для нечисловых результирующих наборов?
27 LINQ to SQL Предыдущая глава содержала введение в LINQ и показала, как использовать LINQ to Objects — версию LINQ, работающую с объектами в памяти. В этой главе мы рассмотрим LINQ to SQL — версию LINQ, предоставляющую доступ к базам данных SQL, таким как Microsoft SQL Server и Oracle. Эти базы данных используют язык структурированных запросов SQL для манипулирования хранимыми в них данными. Традиционно работа с такими базами данных требует, как минимум, знания некоторой реализации SQL и либо включения операторов SQL в язык программирования, либо передачи содержимого SQL-операторов вызовам API или методам SQL-ориентированной библиотеки классов базы данных. Звучит замысловато, не правда ли? Но для вас есть хорошая новость, которая состоит в том, что LINQ to SQL позаботится обо всех деталях взаимодействия с SQL! Он автоматически транслирует запросы LINQ в операторы SQL и позволяет вам и вашим программам просто работать с объектами С#. Visual C# 2008 предоставляет визуальные конструкторы для создания классов LINQ to SQL из существующей базы данных и предусматривает простой способ графической связи элементов управления в формах с базой данных, так что целое приложение базы данных может быть создано быстро и легко, с очень небольшой долей написанного вручную кода. Вы увидите, как это работает, когда мы будем разбирать примеры настоящей главы. В частности, в этой главе будут рассматриваться следующие темы. □ Концепция объектнореляционного отображения (object-relational mapping — ORM) и ее реализация в LINQ to SQL. □ Как инсталлировать SQL Server Express в качестве реляционной базы данных для использования с LINQ to SQL. □ Как инсталлировать базу примеров Northwind для использования с кодом примеров. □ Как использовать O/R Designer в Visual C# 2008 для создания объектов для определенной базы данных.
Глава 27. LINQ to SQL 859 □ Как использовать запросы LINQ to SQL с объектами, созданными посредством O/R Designer. □ Как выполнять навигацию по отношениям между объектами базы данных, используя LINQ to SQL. □ Как использовать групповые запросы и другие расширенные средства LINQ с LINQ to SQL. □ Как привязать объекты LINQ to SQL к графическим элементам управления. □ Как обновлять и вставлять новые объекты в базу данных, используя LINQ to SQL. Объектно-реляционное отображение (ORM) Базы данных SQL, такие как Microsoft SQL Server, также называются реляционными базами данных. Реляционные базы данных сохраняют сходные данные в таблицах, которые концептуально представляют собой группы строк и столбцов — подобно электронным таблицам, хранящимся на жестком диске или другом постоянном хранилище. В противоположность этому С# (как и другие объектно-ориентированные языки) сохраняют данные в объектах памяти. Объекты более сложны, чем таблицы, потому что они имеют ассоциированный с ними код (методы), могут содержать в себе другие объекты и т.д. LINQ to SQL создает слой объектнареляционного отображения (object-relational mapping — ORM) между таблицами реляционной базы данных и объектами вашей программы С#, как показано на рис. 27.1. Объекты С# Реляционные таблицы в базе данных SQL Таблица Customer Слой объектно реляционного отображения (LINQ to SQL) ID A В С Name Zhang Smythe Singh City New York London Mumbai Country USA UK India Таблица Order Order # 11 22 23 Customer ID A В С Product ID 1 2 3 Quantity 2 5 1 Таблица Product ID 1 2 3 Product Widget Wax Cable Price I $2500 $3.00 $50.00 I Puc. 27.1. Слой объектно-реляционного отображения Как было показано в главе 26, для моделирования концепции заказчика на С# создается класс по имени Customer со свойствами наподобие имени заказчика, города и страны. Для хранения нескольких заказчиков создается коллекция объектов Customer. Создание кода для набора классов и^коллекций, соответствующих структуре существующей реляционной таблицы, утомительно и трудоемко, но благодаря объектно- реляционному отображению LINQ to SQL, классы, соответствующие таблице базы данных, создаются автоматически из самой базы данных, так что тратить время на это не потребуется, и можно немедленно приступить к использованию этих классов.
860 Часть IV. Доступ к данным Инсталляция SQL Server и данных примеров Northwind Чтобы запускать примеры, представленные в этой главе, необходимо инсталлировать Microsoft SQL Server Express 2005 — облегченную версию Microsoft SQL Server. Если вы знакомы с SQL Server и имеете доступ к Microsoft SQL Server 2005 Standard или Enterprise Edition с установленной на нем базой данных примеров Northwind, можете пропустить эту инсталляцию, и тогда придется изменить параметры соединения в соответствии с установленным экземпляром SQL Server. Если вы никогда не работали с SQL Server, приступайте к инсталляции SQL Server Express. Инсталляция SQL Server Express 2005 Visual Studio 2005 и Visual C# 2008 Express Edition включают в себя копию SQL Server Express 2005 — облегченную настольную версию SQL Server 2005. Чтобы инсталлировать SQL Server Express 2005, просто отметьте соответствующий флажок при установке Visual C# 2008, как показано на рис. 27.2. Рис. 27.2. Выбор инсталляции SQL Server Express 2005 Если у вас уже установлена среда Visual Studio 2008 или Visual C# 2008 Express, но не инсталлирован SQL Server 2005 Express Edition, можете перезапустить установку и добавить SQL Server 2005 Express Edition в качестве дополнительного компонента. Можно также загрузить и инсталлировать SQL Server Express, обратившись по следующему адресу: http://microsoft.com/sql/editions/express/default.mspx. Использовать версию Microsoft SQL Server Compact Edition вместе с LINQ to SQL нельзя. Вместо нее необходимо применять SQL Server 2005 Express Edition.
Глава 27. LINQ to SQL 861 Инсталляция базы данных примеров Northwind База данных Northwind для SQL Server необходима для запуска примеров этой главы. Она не включена в состав Visual C# 2008 или SQL Server 2005 Express, но доступна в виде отдельной загрузки на сайте Microsoft. Ее можно поискать в Google или на другом поисковом сайте по фразе "northwind database download" или же просто ввести следующий URL: www.microsoft.com/downloads/details.aspx?famllyld=06616212-0356-4 6a0-8da2- eebc53a68034&dlsplaylang=en Эта ссылка позволит загрузить файл SQL2000SampleDb.msi (рис. 27.3). [ Download complete ' ЕЙ^Ййв] ^ Download Complete SQl2000S«mpleDb.msi from download.mtcrosoft.com Downloaded 148MB* lOtec I Download to: С Users i \SQL2000SampieDb ma I Transfer rate 152KB/Sec Qose the duslocj box when download completes- У ft* 1 iQaanfcotdar 1 j Qoa» | У Рш. 27.3. Загрузка файла SQL2000SampleDb.msi Щелкните на кнопке Run (Выполнить) для выполнения файла .msi; это инсталлирует в систему файл базы данных примеров Northwind. Примените опции по умолчанию на всех экранах инсталляции. По завершении файлы базы данных будут установлены в C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF. Именем файла базы данных Northwind MDFявляется N0RTHWND. MDF, без буквы "I". Держите имя файла и путь к нему под рукой, поскольку к нему придется обратиться позднее при установке соединения с базой данных. Это завершает инсталляцию SQL Express и данных примера, необходимых для этой главы. Теперь можно приступить к использованию LINQ to SQL. Первый запрос LINQ to SQL В следующем практическом занятии вы создадите простой запрос для нахождения подмножества специальных объектов в примерах данных Northwind SQL Server с использованием LINQ to SQL и выведете результат на консоль. практическое занятие Первый запрос LINQ to SQL Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новый проект консольного приложения по имени BegVCSharp_27_l_ FirstLINQtoSQLQuery в каталоге C:\BegVCSharp\Chapter27, как показано на рис. 27.4. 2. Щелкните на кнопке ОК для создания проекта.
862 Часть ГУ. Доступ к данным 1«*м ; V rtu*t Stud* msUlM IrmpUtn i "Ш 2 6- «- I W ; YVtndow* ClMsLrbwy WPF WPf Browtct j Сомик • Formi Ap .. Appl««tion Appb<«t>on I Appbc«bon My Trmpfctr* \ 33 Search OnfcneTt. 3 Empty Project ■ A protect for ct«*mg • command-fa* application (NTT ft——ll 33) 0.Л e*rjVCSharp_27 J .FirrtUNQtoSQIQoery Pwc. 27.4. Создание нового проекта BegVCSharp_27_l_FirstLINQtoSQLQuery 3. Чтобы добавить класс отображения LINQ to SQL для базы данных Northwind, перейдите в панель Solution Explorer, выполните щелчок правой кнопкой мыши на проекте С# BegVCSharp_27_l_FirstLINQtoSQLQuery и выберите в контекстном меню пункт AdcHNew Item (Добавить1^ Новый элемент), как показано на рис. 27.5. aiJ*m ^3 Solution FJegVCSharp 27 1 FirstUNQtoSQlQuery A project) - ЗШВБГ~ 'S M Prop* ' _л Refer* Publish... i*a i) Progr *« » _J Neanltan... Add Reference... _J Existing Hem... ServKe Reference... ^ N«vy Folder Set as Startup Project Щ Windows Form... -• U User Control.. ^ Properties Puc. 27.5. Добавление нового элемента 4. В диалоговом окне New Item (Новый элемент) выберите LINQ to SQL Class (Класс LINQ to SQL) в качестве шаблона нового класса и измените имя класса с имени по умолчанию DataClassesl .dbml (dbml — data base mapping language (язык отображения баз данных)) на Northwind.dbml, как показано на рис. 27.6. 5. Добавьте в проект соединение с базой данных Northwind. Откройте окно Database Explorer (Проводник базы данных), выбрав пункт меню View^Other Windows1^Database Explorer (Вид^ Другие окна«=>Проводник базы данных), как показано на рис. 27.7. 6. В окне Database Explorer щелкните на пиктограмме Connect to Database (Подключиться к базе данных), как показано на рис. 27.8.
Глава 27. LINQ to SQL 863 1 Add New Item - B*gVCSh»p_27_l_FtrstUNQtoSQtQuery IempUtet | Visual Stud* installed templates m i i ^ i About Box Application Application Assembly Configurat . Manifest File Informeti... L& J =U j& UNQtoSQL Local MDI Parent Resources Classes Database Form File a d Windows XML File Form 'I UNQ to SQL classes mapped to relational objects. Name Morthwtn4dbml 1 *J Class •J Service-ba... Database a Code File J Settings File ш DataSet 1 TextF.ie ^3 Debugger Visuehzer 1 User Control U Interface v- User Control (WPF) *- 1! tjfeHBT (П3)а j d 1 Caned Puc. 27.6. Изменение имени класса на Northwind. dbml fZubetfJcSb^JT^^ 1 File Edit Ihjjljaalri f North View Project Build Ц Class View J^i Error List 3 Output [^ Properties Window 3J! Solution Explorer J|] Task List ^ Toolbox Other Windows Toolbars -4 В Debug Data CtrkW.C CtnVW.E CtrkW.O Ctrl-Л Р CtrKW, S CtH-W.T Ctr»*W. X ► » Full Screen Shift-Alt «Enter Navigate Backward Navigate Forward Property Pages CtnV- CtrK Shift* Shift-F4 Tools Window Help iaJ ^11 % Database Explorer Ctrl» W. L J 3 3 ■ Document Outline CtrUW. U Object Browser Ctrl* W.J Start Page Web Browser Ctri*W, W Find Results Find Symbol Results Ctrl* W, Q Puc. 27.7. Открытие окна Database Explorer »4 BegVCSharp_27_lJirstUNQtoSQLCJSy^ File Edit View Refactor Project : 3 4 ^ ~ .» ». ^ 2 s Database Explorer JJ Data Connections [Connect to Databasej Puc. 27.8. Пиктограмма Connect to Database.
864 Часть ГУ. Доступ к данным 7. В диалоговом окне Choose Data Source (Выберите источник данных) выберите Microsoft SQL Server Database File (Файл базы данных Microsoft SQL Server) в списке Data Source (Источник данных), как показано на рис. 27.9. Щелкните на кнопке Continue (Продолжить). Choose Deta Source Microsoft Access Database File IA%r-!M.ij.«J Description Use this selection to attach a database file to a loc el Microsoft SQL Server instance (including Microsoft SQL Express) using the .NET Framework Data Provider for SQL Server. Рис. 27.9. Выбор Microsoft SQL Server Database File в качестве источника данных 8. В диалоговом окне Add Connection (Добавить соединение) введите путь к файлу базы данных Northwind, как показано на рис. 27.10. Щелкните на кнопке Test Connection (Тестировать соединение), чтобы убедиться, что соединение с базой данных работает. Enter information to connect to the selected data source or click "Change* to choose a different data source and/or provider. Data source Microsoft SQL Server Database File (SqrCkent) Cat a base file name (new or existing): C:\SQL Server 2000 Sample DatabasesXNORTHWND MDF Log on to the server • Use Windows Authentication Use SQL Server Authentication i >roe: ! word; : Save my p «c Puc. 27.10. Ввод пути к файлу базы данных Northwind Если тестирование соединения не работает, проверьте, запущен ли SQL Server. Если исключение сообщает, что пользовательские экземпляры не включены, запустите SSMS, откройте окно запросов и запустите следующую команду базы данных: exec sp_configure 'user instances enabled1, 1 Reconfigure Это включит пользовательские экземпляры и автоматически перезапустит SQL Server. Щелкните на кнопке О К для завершения и перейдите к следующему шагу. База данных SQL Server Express NORTHWIND.MDF теперь появится в узле Data Connections (Соединения с данными) в Database Explorer, как показано на рис. 27.11.
Глава 27. LINQ to SQL 865 [ Ц$ 27-l-F»rstUNQtoSQlQuery - Microsoft Visual C* 2008 Express Edition 1 [ £•*« £<W yiew £rqject guild Debug \ Database Explorer ^ Д J 8 ;■ -^ ' *^ f • В :ji Data Connections Data Tools W.ndoJ • - - ► ! С Nort hwiod.dbml 1 1 Fmc. 27.11. База данных NOR THWIND. MDF в узле Data Connections 9. Раскройте узел NORTHWIND.MDF, чтобы просмотреть его содержимое. 10. Теперь раскройте узел Tables (Таблицы), чтобы можно было видеть список таблиц базы данных, как показано на рис. 27.12. ц4 27-1-f.rstUNQtoSQlQuery - Microsoft v-su«J С* 2006 Ьрг*м Edttwn F> £<М View Eroject fiurfd Qebug jj -i -«; у •» ^j _J Tables ♦ J3 Categories t 3 CuslemerCustomerOemo t 3 CustomerOemogf щШа i ^ Customers t Ц Employees It; 13 EmptoyeeTerntones t. 13 Order Details . J Orders . J Products » Ш R«9«n i ^Sfcppers , J Suppliers . В Territories a _J View* . ^j Stored Procedures ... _J Function* i ^ Synonyms * LI Types ♦ _J Assemblies Fmc. 27.12. Список таблиц базы данных NORTHWIND. MDF 11. Щелкните и перетащите таблицу Customers в панель Northwind. dbml (см. рис. 27.12). 12. Откроется диалоговое окно (рис. 27.13) с запросом, хотите ли вы скопировать файл локальной базы данных в проект. Щелкните на кнопке No (Нет), чтобы исключить создание дублированной копии Northwind.MDF. Microsoft Visual С* 2008 Express Edition I The connection you selected uses a local data file that is not in the ' current project. Would you like to copy the file to your project and modify the connection? If you copy the data file to your project, it will be copied to the project's output directory each time you run the application. Press Fl for information on controlling this behavior Puc. 27.13. Запрос на копирование файл локальной базы данных
866 Часть ГУ. Доступ к данным 13. После этого объект Customer появится в панели Northwind.dbml, как показано на рис. 27.14. Р P«U Connect**» > _ ч id ол»ь*н ОЬрма -. Ц т«ы« fc 3 Cetegones !tb 3 CuftomefCuitomefDemo (й 3 CustomefOemoojephio $ JD Customer? A 3 Employees Ijtr *^3 Employee Tetntones ,t. 3 Ofder DeUrls А ИЗ Ofdcn . & Э Products t 3 **9«on «£ 3«4»p«rs ф SSoppben 1$ 3 Temtones $■ Ci views & 2l Stwed Procedures .», J Functions » ca Type * £j Assembles * 3f CustomerlD J Comp#nyN*me !iP Cont*ctN»me jf ConuetTnl* rt Address a» city jf Region 3f PostoKode !SP Country 31* Phone Рггс. 27.14. Объект Customer 14. Откомпилируйте проект, чтобы объект Customer был доступен, когда вы начнете вводить код на следующем шаге. Просмотреть код классов, сгенерированных O/RDesigner, можно, заглянув в файл Northwind. designer. cs,KomopuunoneumcEnoducxodHbm0aiUoMNorthwind. dbml в Solution Explorer, подобно тому, как сгенерированный код формы помещается в <имя_формы>. designer. cs. Однако, как и в случае сгенерированного кода формы, вы не должны модифицировать сгенерированный O/R Designer код, так что лучше не открывать его в редакторе, за исключением тех случаев, когда вы хотите проверить имя класса или проверить сгенерированный тип данных. 15. Откройте главный файл Program, cs и добавьте следующий код в метод Main (): static void Main(string[] args) { NorthwindDataContext northWindDataContext = new NorthwindDataContext() ; var queryResults = from с in northWindDataContext.Customers where c.Country == "USA" select new { ID=c.CustomerID, Name=c.CompanyName, City=c.City, State=c.Region }; foreach (var item in queryResults) { Console.WriteLine(item); Console.WriteLine("Для продолжения нажмите Enter..."); Console.ReadLine();
Глава 27. LINQ to SQL 867 16. Откомпилируйте и выполните программу (можете просто нажать <F5> для запуска отладки). Вы увидите на экране информацию о заказчиках из США, как показано ниже: ID = GREAL, Name = Great Lakes Food Market, City = Eugene, State = OR } ID = HUNGC, Name = Hungry Coyote Import Store, City = Elgin, State = OR } ID = LAZYK, Name = Lazy К Kountry Store, City = Walla Walla, State = WA } ID = LETSS, Name = Let's Stop N Shop, City = San Francisco, State = CA } ID = LONEP, Name = Lonesome Pine Restaurant, Ci.ty = Portland, State = OR } ID = OLDWO, Name = Old World Delicatessen, City = Anchorage, State = AK } ID = RATTC, Name = Rattlesnake Canyon Grocery, City = Albuquerque, State = NM } ID = SAVEA, Name = Save-a-lot Markets, City = Boise, State = ID } ID = SPLIR, Name = Split Rail Beer & Ale, City = Lander, State = WY } ID = THEBI, Name = The Big Cheese, City = Portland, State = OR } ID = THECR, Name = The Cracker Box, City = Butte, State = MT } ID = TRAIH, Name = Trail's Head Gourmet Provisioners, City = Kirkland, State = WA } ID = WHITC, Name = White Clover Markets, City = Seattle, State = WA } Для продолжения нажмите Enter. . . Просто нажмите <Enter> для завершения программы и закрытия экрана консоли. Если вы использовали <Ctrl+F5> (запуск без отладки), может понадобиться нажать <Enter> два раза. Это завершит выполнение программы. Теперь давайте рассмотрим ее работу в деталях. Описание полученных результатов Код для этого и всех прочих примеров этой главы подобен примеру, описанному во введении в LINQ, который изложен в предыдущей главе, и использует расширяющие классы из пространства имен System. Linq, на которое установлена ссылка using, автоматически вставленная Visual C# 2008 при создании проекта: using System.Linq; Первый шаг использования классов LINQ to SQL состоит в создании экземпляра объекта DataContext для определенной базы данных, к которой вы обращаетесь; экземпляр представляет собой класс из файла .dbml, созданного в O/R Designer. Этот объект служит воротами к вашей базе данных, предоставляя все методы, которые вам необходимы для управления ею из программы. Он также служит фабрикой для создания бизнес-объектов, соответствующих концептуальным сущностям, хранящимся в вашей базе данных (например, заказчикам и продуктам). В проекте класс контекста данных называется NorthwindDataContext, и он скомпилирован из файла Northwind. dbml. Первый шаг в методе Main () состоит в создании экземпляра NorthwindDataContext, как показано ниже: NorthwindDataContext northWindDataContext = new NorthwindDataContext(); Когда вы перетаскиваете таблицу Customers в панель O/R Designer, объект Customer добавляется в класс LINQ to SQL в Northwind.dbml, а член Customers добавляется к объекту NorthwindDataContext, чтобы позволить запрашивать объекты Customer в базе данных Northwind. Преобразование к множественному числу в O/R Designer O/R Designer обнаруживает, что имя таблицы Customers представляет собой множественную форму существительного английского языка и автоматически создает имя Customer в единственном числе для генерируемого объекта. Вы можете управлять этим поведением через пункт меню Tools ^Options ^Database Tools^Q/R Designer (Сервис^ Параметры^Инструменты^О/Я Designer), как показано на следующем рисунке.
868 Часть ГУ. Доступ к данным Объекты С# Реляционные таблицы в базе данных SQL Таблица Customer Слой объектно реляционного отображения (LINQtoSQL) ID А В С Name Zhang Smythe Singh City New York London Mumbai Country USA UK India Таблица Order Order # 11 22 23 Customer ID A В С Product ID 1 2 3 Quantity 2 5 1 Таблица Product ID 1 2 3 Product Widget Wax Cable Price $25.00 $3.00 $50.00 ] Вы можете отключить это преобразование множественного в единственное число, установив параметр Pluralizatlon of Names Enabled (Включить преобразование к множественному числу) в False вместо значения по умолчанию True. Однако, возможно, стоит не изменять это поведение, потому что оно делает имена классов и их членов более интуитивно понятными. Сам оператор LINQ выполняет запрос, используя член Customers объекта NorthwindDataContext как источника данных: var queryResults = from с in northWindDataContext.Customers where c.Country == "USA" select new { ID=c.CustomerID, Name=c.CompanyName, City=c.City, State=c.Region }; Customers — типизированная таблица LINQ (System. Data. Linq. Table<Customer>), которая подобна типизированной коллекции объектов Customer (как List<Table>), но реализована для LINQ to SQL, и заполняется информацией из базы данных автоматически. Она реализует интерфейсы IEnumerable/IQueryable, и потому может использоваться в качестве источника данных LINQ в конструкции from, как любая другая коллекция или массив. Конструкция where ограничивает результаты только перечнем заказчиков из США. Конструкция select — это проекция, подобная примерам, которые разрабатывались в предыдущей главе; она создает новый объект с членами ID, Name, City и State. Поскольку вы знаете, что результаты относятся только к США, вы можете переименовать Region в State, чтобы более точно отображать результаты. И, наконец, вы создаете стандартный цикл f oreach, подобный тому, что разрабатывался в главе 26: foreach (var item in queryResults) { Console.WriteLine(item); Этот код использует сгенерированный по умолчанию метод ToString () для каждого элемента, чтобы форматировать вывод для Console.WriteLine (item), так что вы можете видеть значения каждого экземпляра — члена проекции в фигурных скобках: { ID=WHITC, Name=White Clover Markets, City=Seattle, State=WA }
Глава 27. LINQ to SQL 869 И, наконец, пример завершается кодом, выполняющим паузу, чтобы вы могли увидеть результат: Console.WriteLine("Для продолжения нажмите Enter..."); Console.ReadLine(); }; Таким образом, вы создали базовый запрос LINQ to SQL, который можно использовать в качестве основы для построения других, более сложных запросов. Навигация по отношениям LINQ to SQL Одним из наиболее мощных аспектов O/R Designer из Visual Studio является его способность автоматически создавать объекты LINQ to SQL, которые помогут выполнять навигацию по отношениям между связанными таблицами в базе данных. В следующем практическом занятии вы добавите связанную таблицу к классу LINQ to SQL, а также напишете код навигации по связанным объектам данных в базе и печати их значений. Навигация по отношениям LINQ to SQL Для создания примера выполните следующие шаги в Visual C# 2008. 1. Модифицируйте проект из предыдущего примера BegVCSharp_27_1_ FirstLINQtoSQLQuery в каталоге С: \BegVCSharp\Chapter27, как описано в следующих шагах. 2. Щелкните на таблице Orders в окне Database Explorer и перетащите ее в панель Northwind.dbml, как показано на рис. 17.15. Elk £<M tfew E'OjKt fimld Debug Data Joels Window £jdp 11 Data Connections i, NOfTHWNO MDf j Ct Database Diagrams _J Tables • J Categories • Zi CustomerCustomerOemo « J CustomerOemoo/aphics . ID Customers t, 3 Employees .*, 3 EmptoyeeTerntones Л- 3 Order Details . 1ЯДЯ • 13 Products • J Regton . J Stoppers • J Supplten • 3 Territories _u ■ ' ♦ _J Stored Procedures _j Functions ► _J Synonyms ♦ -J Types ♦ _i Assemblies a I uitomn = Properbe» Ж CustomeHD J CompanyName ?3P ContactName ГЭ* ContattTttJe 9 -ddreis SCity 3* Region ['f PosUlCode if Coomr, ;S? Phone - • V J '31 Order© !J5* CustomerlD If EmployeeJD 2? OrderOate ;3* ReqoiredOate ^j1 Sh.ppedOate P ShrpV.a If fre-ght 2? ShipHame .If ShipAddresi S Sb.pC.r-/ ? ShipReg.on ZSr* ShipPotuiCoee * ShipCountr/ Рис. 27.15. Перетаскивание таблицы Orders 3. Откомпилируйте проект, чтобы объект Order появлялся в IntelliSense, когда вы начинаете вводить код для следующего шага. 4. Откройте главный файл Program.cs. В методе Main() добавьте поле Orders к конструкции select запроса LINQ (не забудьте добавить запятую после с.Region, чтобы отделить дополнительные поля от остальной части списка):
870 Часть IV. Доступ к данным static void Main(string [ ] args) { NorthwindDataContext northWindDataContext = new NorthwindDataContext(); var queryResults = from с in northWindDataContext.Customers where c.Country == "USA" select new { ID=c.CustomerID/ Name=c.CompanyName, City=c.City/ State=c.Region, Orders=c.Orders }; 5. Модифицируйте следующим образом оператор f oreach для печати результатов запроса: foreach (var item in queryResults) { Console.WriteLine( "Заказчик: {0} {1}, {2}\n{3} заказов: \tID закаэа\ЗДата заказа" , item.Name, item.City, item.State, item.Orders.Count ); foreach (Order о in item.Orders) { Console.WriteLine("\t\t{0}\t{l}", o.OrderlD, o.OrderDate); > }; Console.WriteLine("Для продолжения нажмите Enter..."); Console.ReadLine(); } 6. Откомпилируйте и выполните программу (вы можете просто нажать F5 для запуска Start Debugging). Вы увидите информацию о заказчиках из США и их заказах, как показано ниже (представлена завершающая часть вывода; первая часть была прокручена вверх и исчезла из окна консоли): Заказчик: Trail's Head Gourmet Provisioners Kirkland, WA 3 заказов: ID заказа Дата заказа 10574 6/19/1997 12:00:00 AM 10577 6/23/1997 12:00:00 AM 10822 1/8/1998 12:00:00 AM Customer: White Clover Markets Seattle, WA 14 заказов: ID заказа Дата заказа 10269 7/31/1996 12:00:00 AM 10344 11/1/1996 12:00:00 AM 10469 3/10/1997 12:00:00 AM 10483 3/24/1997 12:00:00 AM 10504 4/11/1997 12:00:00 AM 10596 7/11/1997 12:00:00 AM 10693 10/6/1997 12:00:00 AM 10696 10/8/1997 12:00:00 AM 10723 10/30/1997 12:00:00 AM 10740 11/13/1997 12:00:00 AM 10861 1/30/1998 12:00:00 AM 10904 2/24/1998 12:00:00 AM 11032 4/17/1998 12:00:00 AM 11066 5/1/1998 12:00:00 AM Для продолжения нажмите Enter... Как и ранее, нажмите <Enter> для завершения программы и закрытия экрана консоли.
Глава 27. LINQ to SQL 871 Описание полученных результатов Вы модифицировали предыдущую программу вместо создания новой с нуля, так что вам не пришлось повторять все шаги по созданию исходного файла отображения LINQ to SQL по имени Northwind. dbml (обратите внимание, что пример кода содержит отдельные проекты — каждый со своим собственным экземпляром Northwind.dbml). Перетаскивая таблицу Orders из окна Database Explorer, вы добавили класс Order к исходному файлу Northwind. dbml для представления таблицы Orders в отображении базы данных Northwind. O/R Designer также обнаружил отношение в базе данных между Customers и Orders и добавил член-коллекцию Orders к классу Customer для представления отношения. Все это было сделано автоматически, в процессе добавления новых элементов управления в форму. Если вы посмотрите на то, что было до и после добавления объекта базы данных в панель 0/R Designer, то увидите определения классов для добавленных объектов в сгенерированном исходном файле Northwind, designer .cs. Как упоминалось ранее, этот сгенерированный исходный файл нельзя модифицировать — он используется только для справочных целей. Затем вы добавили новый доступный член Orders к конструкции select запроса: select new { ID=c.CustomerlD, Name=c.CompanyName, City=c.City, State=c.Region, Orders=c.Orders }; Orders— специальный типизированный набор LINQ (System. Data. Linq. EntitySet<Order>), который представляет отношение между двумя таблицами в реляционной базе данных. Он реализует интерфейс IEnumerable/IQueryable, и поэтому может использоваться в качестве источника данных LINQ сам по себе или подвергаться итерации в операторе f oreach, как любая другая коллекция или массив. Подобно объекту Table, показанному в предыдущем примере, EntitySet похож на типизированную коллекцию объектов Order (как List<Order>), но только заказы, присланные определенным заказчиком, появляются в члене EntitySet для конкретного экземпляра Customer. Объекты Order в члене Enti tySet объекта Customer соответствуют строкам заказов в базе данных, имеющих значение поля CustomerlD, равное ID этого заказчика. Навигация по отношению просто включает построение вложенного оператора f oreach для итерации по каждому заказчику, а затем по каждому его заказу: foreach (var item in queryResults) { Console.WriteLine( "Заказчик: {0} {1}, {2}\n{3} 3aKa30B:\tID заказаИДата заказа", item.Name, item.City, item.State, item.Orders.Count ); foreach (Order о in item.Orders) { Console.WriteLine("\t\t{0}\t{l}", o.OrderlD, o.OrderDate); } };
872 Часть IV. Доступ к данным Вместо того чтобы использовать форматирование ToString () по умолчанию, вы форматируете вывод для читабельности таким образом, чтобы правильно показать иерархию со списком заказов каждого заказчика. Форматная строка "Заказчик: {0} {1}, {2}\п{3} заказов :\tID заказа\1:Дата заказа" включает заполнители для имени, города и штата каждого заказчика в первой строке, а затем печатает заголовки столбцов для заказов данного заказчика, выводимых в последующих строках. Вы используете агрегатный метод LINQ Count () для печати количества заказов данного заказчика, затем печатаете ID заказа и данные заказа в каждой строке вложенного оператора f oreach: Заказчик: White Clover Markets Seattle, WA 14 заказов: ID заказа Дата заказа 10269 7/31/1996 12:00:00 AM 10344 11/1/1996 12:00:00 AM Форматирование пока еще немного корявое — в том отношении, что вы видите здесь и время заказа, тогда как интересует только дата. Однако в следующем примере будет шанс это поправить. Дальнейшее углубление в LINQ to SQL Чтобы получить интересные данные о затратах каждого заказчика, вы должны добавить еще один уровень детализации и углубиться в подробности каждого заказа, используя таблицу Order Details. Опять-таки, LINQ to SQL и О/R Designer весьма облегчают эту задачу, в чем вы убедитесь в следующем практическом занятии. практическое занятие Дальнейшее углубление в LINQ to SQL Модифицируйте в Visual C# 2008 проект предыдущего примера BegVCSharp_27_l_ FirstLINQtoSQLQuery в каталоге С: \BegVCSharp\Chapter27, как описано в следующих шагах. 1. Щелкните на таблице Order Details в окне Database Explorer и перетащите ее в панель Northwind.dbml, как показано на рис. 27.16. 2. Откомпилируйте проект, чтобы объект OrderDetail появился, когда вы начнете вводить код на следующем шаге. 3. Откройте главный файл Program, cs. В методе Main () запрос должен выглядеть так: static void Main(string [ ] args) { NorthwindDataContext northWindDataContext = new NorthwindDataContext(); var gueryResults = from с in northWindDataContext.Customers where c.Country == "USA" select new { ID=c.CustomerID, Name=c.CompanyName, City=c.City, State=c.Region, Orders=c.Orders
Глава 27. LINQ to SQL 873 £4. 1<И i*. JJ-Jli t«*Ki I-*" D<kwf Onto look A««W Ы* .--i." ia.ni» ,lD*.C<y«<to.i i Л NODTMMNOMOf •« aOiitmDiiyw) И 30 • 'Д CualomorOomotropfwct . наш i 3*4*Kto » 3M|*. * : -»v> ■« 3 МИ— . 3 Tomionoi 'ЭР СшготоНО J> (л«#о«у*от. ЗГ СописЛото * CoMoci*»te JP «оииСойо ЯГСоил*> 3T»ho«» ..Л »аго*гю ■tf С«ио«оИО •t Impto/orfD "У oo*o»»t T ftoev.'o*OiM ЗГ thpAOfrou ЭР Ib'pCo^ey 'ЗРовгЮ l"f ^ооЖоТ) ар <***•«> Рис. 27.16. Перетаскивание таблицы Order Details 4. Модифицируйте конструкцию f о reach для печати результатов запроса: foreach (var item in queryResults) { Console.WriteLine( "Заказчик: {0} {1}, {2}\n{3} заказов:\tID заказа\адата заказаМЮбщая сумма" item.Name, item.City, item.State, item.Orders.Count); foreach (Order о in item.Orders) { Console.WriteLine( "\t{0,10}\t{l,10}\t{2,10}", o.OrderlD, o.OrderDate.Value.ToShortDateString(), o.0rder_Details.Sum(od = > od.Quantity * od.UnitPrice) .ToString("C2")); } }; Console.WriteLine("Для продолжения нажмите Enter ..."); Console.ReadLine (); 5. Откомпилируйте и выполните программу (вы можете просто нажать F5 для запуска Start Debugging). Опять же, результат, показанный здесь, составляет только часть вывода: Заказчик: Trail's Head Gourmet Provisioners Kirkland, WA 3 заказов: ID заказа Дата заказа Общая сумма 10574 6/19/1997 $764.30 10577 6/23/1997 $569.00 10822 1/8/1998 $237.90 Заказчик: White Clover Markets Seattle, WA 14 заказов: ID заказа 10269 10344 10469 10483 10504 10596 10693 Дата заказа 7/31/1996 11/1/1996 3/10/1997 3/24/1997 4/11/1997 7/11/1997 10/6/1997 Общая сумма $676.00 $2,856.00 $1,125.50 $704.00 $1,388.50 $1,476.10 $2,334.00
874 Часть IV. Доступ к данным 10696 10723 10740 10861 10904 11032 11066 10/8/1997 10/30/1997 11/13/1997 1/30/1998 2/24/1998 4/17/1998 5/1/1998 $996, $468, $1,770, $3,523, $1,924, $8,902, $928, .00 .45 .00 .40 .25 .50 .75 Для продолжения нажмите Enter Как и ранее, нажмите <Enter> для завершения программы и закрытия окна консоли. Описание полученных результатов Вы модифицировали предыдущую программу вместо создания новой с нуля, так что повторять все шаги по созданию исходного файла отображения LINQ to SQL Northwind.dbml не потребуется. В результате перетаскивания таблицы Order Details из окна Database Explorer добавился класс OrderDetail к исходному файлу Northwind. dbml, а также член-коллекция OrderDetails к классу Order для представления отношения между таблицами Orders и Order Details. Поскольку имена классов и имена их членов в С# не могут содержать пробелы, O/R Designer вставляет подчеркивания, формируя имена Order_Detail и Order_Details. Подобно члену Orders самого класса Customer, член OrderDetails класса Orders является EntitySet, представляющим отношение. Модифицировать конструкции from. . .where. . .select предыдущего запроса LINQ вообще не потребовалось; вместо этого все изменения коснулись цикла обработки foreach: Console.WriteLine( "Заказчик: {0} {1}, {2}\п{3} заказов:\tID заказа\tflaTa заказа^Общая сумма" item.Name, item.City, item.State, item.Orders.Count); foreach (Order о in item.Orders) { Console.WriteLine ( "\t{0,10}\t{l,10}\t{2,10}", o.OrderlD, о. OrderDate . Value. ToShortDateStnng () , o.Order_Details.Sum(od = > od.Quantity * od.UnitPrice) .ToStnng("C2") ) ; } }; Была добавлена строка "Общая сумма" в конец заголовка для каждого заказчика в первой строке внешнего цикла foreach. Во внутреннем цикле foreach улучшено форматирование каждого вхождения за счет добавления ширины поля 10 для каждого места заполнения Console .WriteLine (), так что оно читается, как "\t{0, 10}\t{ 1,10}\t{2,10} ". В списке полей были переданы OrderlD и OrderDate, сформатированные в виде короткой строки даты, чтобы исключить ненужную часть, описывающую время в OrderDate. Итог по каждому заказу вычисляется по OrderDetails. Все элементы вы уже видели в предыдущей главе. Для получения суммы по всем строкам заказа применяется агрегатная операция LINQ Sum(), которой передается лямбда-выражение (od => od.Quantity * od.UnitPrice), вычисляющее общую сумму каждого заказа (промежуточным итогом каждой детали заказа является количество единиц цены. Вы форматируете этот итог в виде денежной величины с двумя десятичными разрядами ("С2"), и на этом пример завершается.
Глава 27. LINQ to SQL 875 Группировка, упорядочивание и другие расширенные запросы в LINQ to SQL Теперь, когда вы знаете общие затраты каждого заказчика, можно приступить к выяснению вопросов более высокого уровня. Какие заказчики приобрели больше всего продуктов? В каких регионах или странах они находятся? Какие продукты продаются лучше всего? Чтобы ответить на подобные вопросы, поддерживающие принятие решений, потребуется группировка и упорядочивание. Операции group и о г derby в коде LINQ to SQL те же, что и в LINQ to Objects, но использование источников данных SQL для каждого LINQ может опираться на возможности group и orderby самой базы данных SQL, в зависимости от способа построения запроса. В следующем примере вы попробуете это, конструируя запрос для нахождения стран, в которых были наибольшие объемы продаж. занятие Группировка, упорядочивание и другие расширенные запросы Модифицируйте в Visual C# 2008 проект предыдущего примера BegVCSharp_27_l_ FirstLINQtoSQLQuery в каталоге С: \BegVCSharp\Chapter27, как описано в следующих шагах. 1. Откройте главный файл Program, cs. Замените запрос LINQ в методе Main () следующими запросами: static void Main(string[] args) { NorthwindDataContext northWindDataContext = new NorthwindDataContext(); var totalResults = from с in northWindDataContext.Customers select new { Country = c.Country, Sales = c.Orders.Sum(o = > o.Order_Details.Sum(od = > od.Quantity * od.UnitPrice) ) }; var groupResults = from с in totalResults group с by c. Country into eg select new { TotalSales = cg.Sum(c = > c.Sales), Country = eg.Key } var orderedResults = from eg in groupResults orderby eg.TotalSales descending select eg 2. Продолжите модификацию метода Main () в Program. cs заменой цикла f oreach, следующего за запросом LINQ, таким кодом: Console.WriteLine ("CTpaHa\t\tBcero продаж\п \t ") ; foreach (var item in orderedResults) { Console.WriteLine("{0,-15}{1,12}", item.Country, item.TotalSales.ToString("C2"));
876 Часть IV. Доступ к данным Console.WriteLine("Для продолжения нажмите Enter..."); Console.ReadLine(); } 3. Откомпилируйте и выполните программу (можно просто нажать <F5> для запус- ка отладки). Страна USA Germany Austria Brazil France Venezuela UK Sweden Ireland Canada Belgium Denmark Switzerland Mexico Finland Spain Italy Portugal Argentina Norway Poland Вы Для продолжения увидите общие объемы продаж по странам в порядке убывания: Всего продаж $263,566.98 $244,640.63 $139,496.63 $114,968.48 $85,498.76 $60,814.89 $60,616.51 $59,523.70 $57,317.39 $55,334.10 $35,134.98 $34,782.25 $32,919.50 $24,073.45 $19,778.45 $19,431.89 $16,705.15 $12,468.65 $8,119.10 $5,735.15 $3,531.95 нажмите Enter... Как и ранее, нажмите <Enter> для завершения программы и закрытия экрана консоли. Описание полученных результатов Вы опять модифицировали предыдущую программу для повторного использования файла отображения LINQ to SQL Northwind.dbml. Для этого примера не пришлось добавлять новые классы к Northwind.dbml, поскольку использовались имеющиеся классы отображения Customer, Order и Order_Detail. Вы заменили в этой главе предыдущий запрос LINQ тремя новыми запросами, используя тот же источник данных, что и ранее, но при этом обрабатывая результат совершенно иначе. Первый запрос LINQ подобен предыдущим запросам этой главы, и он использовал в качестве источника данных northWindDataContext. Customers: var totalResults = from с in northWindDataContext.Customers select new { Country = c.Country, Sales = c.Orders.Sum(o = > o.Order_Details.Sum(od = > od.Quantity * od.UnitPrice) ) }; Для этого запроса нет необходимости ограничивать результат только заказчиками из США, поэтому здесь конструкция where отсутствует. Вместо свойства Region вы
Глава 27. LINQ to SQL 877 выбираете Country, потому что этот запрос группирует и упорядочивает результаты по странам. Вы не выбираете ID, Name или City, потому что они не участвуют в результате и бессмысленно запрашивать больше данных из базы, чем вы собираетесь использовать. Операция Sum (), использующая лямбда-выражение od => od. Quantity * od.UnitPrice для получения суммы всех единиц заказов, теперь перемещена в сам запрос LINQ. Затем вы специфицируете группирующий запрос для группировки общих сумм продаж по странам, используя результаты предыдущего запроса в качестве источника данных: var groupResults = from с in totalResults group с by с.Country into eg select new { TotalSales = cg.Sum(c = > c.Sales), Country = eg.Key } Этот запрос очень похож на группирующий запрос, который создавался для LINQ to Objects в предыдущей главе; он использует страну в качестве ключа для группировки результатов и суммирования общих продаж (в данном случае Sum () суммирует общие продажи по всем заказчикам в пределах страны), и операция вложена внутрь другого вызова Sum () для суммирования продаж по всем Orders для каждого Customer. И в последнем запросе вы выполняете некоторое форматирование, печатая заголовки для столбцов "Страна" и "Всего продаж" в результате, а затем в цикле обработки f oreach печатаете два столбца в выровненном формате (выровненном влево в пределах поля шириной 15 символов для "Страна", и выровненном вправо в поле шириной 12 символов для "Всего продаж": Console.WriteLine ("CTpaHa\t\tBcero продаж\п \t ") ; foreach (var item in orderedResults) { Console.WriteLine("{0,-15}{1,12}", item.Country, item.TotalSales.ToString("C2")); } Отображение сгенерированного SQL Если вы знакомы с базами данных SQL, может вызвать удивление то, какие команды SQL для базы данных генерируются LINQ to SQL. Даже если вы не знакомы с SQL, иногда полезно взглянуть на сгенерированный код SQL, чтобы понять, как LINQ to SQL взаимодействует с базой данных (и, возможно, испытать благодарность к LINQ to SQL за то, что он выполняет за вас такую сложную работу). В следующем практическом занятии вы модифицируете предыдущий пример группирующего запроса для отображения сгенерированного кода SQL, а затем посмотрите, как он выполняется. j/тшташяятвштвшвшвшшшшштттвтшшшяваттттшштшштшттяшшт практическое занятие Отображение сгенерированного SQL Выполните следующие шаги для создания примера в Visual C# 2008. 1. Модифицируйте проект из предыдущего примера BegVCSharp_27_1_ FirstLINQtoSQLQuery в каталоге C:\BegVCSharp\Chapter27, как описано в последующих шагах. 2. Откройте главный файл Program.cs. Добавьте выделенные строки к методу Main ():
878 Часть IV. Доступ к данным static void Main(string[] args) { NorthwindDataContext northWindDataContext = new NorthwindDataContext() ; var totalResults = from с in northWindDataContext.Customers select new { Country = c.Country, Sales = c. Orders. Sum (o = > o.Order_Details.Sum(od = > od.Quantity * od.UnitPrice) ) }; Console.WriteLine(" SQL для запроса totalResults ") ;; Console.WriteLine(totalResults); Console.WriteLine("Для продолжения нажмите Enter. ..") ; Console.ReadLine(); var groupResults = from с in totalResults group с by c.Country into eg select new { TotalSales = cg.Sum(c = > c.Sales), Country = eg.Key } Console.WriteLine(" SQL для запроса groupResults ");; Console.WriteLine(groupResults); Console. WriteLine ("Для продолжения нажмите Enter. . .") ; Console.ReadLine(); var orderedResults = from eg in groupResults orderby eg.TotalSales descending select eg Console.WriteLine(" SQL для запроса orderedResults ") ;; Console.WriteLine(groupResults); Console. WriteLine ("Для продолжения нажмите Enter. . .") ; Console.ReadLine(); Console.WriteLine ("CTpaHa\t\tBcero продаж\п \t ") ; foreach (var item in orderedResults) { Console.WriteLine("{0,-15}{1,12}", item.Country, item.TotalSales.ToString("C2")); } Console.WriteLine("Для продолжения нажмите Enter..."); Console.ReadLine(); } 3. Откомпилируйте и выполните программу (можно просто нажать <F5> для запуска отладки). Вы увидите следующий вывод SQL для первого запроса на экране консоли: SQL для запроса totalResults SELECT [t0].[Country], ( SELECT SUM([t4].[value]) FROM ( SELECT SUM([t3].[value])( SELECT SUM((CONVERT(DecimalB9,4),[t2].[Quantity])) * [t2].[UnitPrice]) FROM [dbo].[Order Details] AS [t2] WHERE [t2].[OrderlD] = [tl].[OrderlD] ) AS [value], [tl].[CustomerlD] FROM [dbo].[Orders] AS [tl] ) AS [t3] WHERE [t3].[CustomerlD] = [tO].[CustomerlD] ) AS [value] FROM [dbo].[Customers] AS [tO] Для продолжения нажмите Enter...
Глава 27. LINQ to SQL 879 Пока не нажимайте <Enter>; оставьте экран консоли на месте и прочтите следующий раздел "Описание полученных результатов", чтобы разобраться в сгенерированном выводе. Описание полученных результатов Метод ToString () по умолчанию для запроса LINQ to SQL печатает сгенерированный SQL, поэтому то, что вы видите— это вывод вызова Console .WriteLine ( totalResults). Не правда ли, можно порадоваться, что вам не пришлось писать это самостоятельно? Эта команда SQL будет передана SQL Server для возврата результатов в LINQ to SQL, который транслирует конструкции LINQ select, from и where в соответствующие конструкции SQL — SELECT, FROM и WHERE. Кроме того, агрегатная операция LINQ Sum () транслируется в вызов агрегатной операции SQL SUM (). Действие этих функций SQL достаточно схоже с эквивалентными функциями LINQ, поэтому мы не станем их объяснять подробно; однако имейте в виду, что не все конструкции или методы LINQ транслируются однозначно в эквиваленты SQL. Просто так случилось, что этот запрос почти точно переводится на SQL, с небольшими поправками вроде вызова функции SQL CONVERT () для преобразования корректного типа данных decimal для столбца Sales результата и добавления конструкций SQL WHERE для соединения результатов таблиц Order Details, Orders и Customers по значениям полей OrderlD и CustomerlD; обо всех этих запутанных деталях SQL позаботится LINQ to SQL. Обратите также внимание, что этот запрос SQL еще не выполнился — благодаря отложенному выполнению запросов LINQ— и эту отображенную команду SQL каркас LINQ to SQL только тогда отправит на SQL Server, когда запрос действительно нужно будет выполнить (при запуске цикла foreach по результатам). Теперь нажмите <Enter>, чтобы увидеть следующий экран сгенерированного кода SQL для группового запроса: SQL для запроса groupResults SELECT SUM([t4].[value]) AS [value], [t4].[Country] FROM ( SELECT [tO].[Country], ( SELECT SUM([t4].[value]) FROM ( SELECT SUM([t3].[value]) ( SELECT SUM((CONVERT(DecimalB9,4),[t2].[Quantity])) * [t2].[UnitPrice]) FROM [dbo].[Order Details] AS [t2] WHERE [t2] . [OrderlD] = [tl] . [OrderlD] ) AS [value], [tl].[CustomerlD] FROM [dbo].[Orders] AS [tl] ) AS [t3] WHERE [t3].[CustomerlD] = [tO].[CustomerlD] ) AS [value] FROM [dbo].[Customers] AS [tO] ) AS [t4] GROUP BY [t4] .[Country] Для продолжения нажмите Enter... Обратите внимание, что SQL для группового запроса содержит тот же SQL, который вы только что видели в запросе суммарных результатов внутри него; и поскольку его источник данных— totalResults, как указано в конструкции from (from с in totalResults), он помещает SQL для запроса total Re suits во внешний запрос SQL с конструкцией GROUP BY:
880 Часть ГУ. Доступ к данным SELECT SUM([t4].[value]) AS [value], [t4].[Country] FROM ( . . . SQL для totaResults . . . ) AS [t4] GROUP BY [t4].[Country] Опять-таки, это еще не посылается на сервер; поскольку вы передаете эти результаты еще одному вложенному запросу то тем самым по существу строите единственную сложную команду SQL со множеством запросов LINQto SQL. Теперь нажмите <Enter>, чтобы увидеть следующий экран сгенерированного SQL: SQL для запроса orderedResults SELECT [t5].[value] AS [TotalSales], [t5].[Country] FROM ( SELECT SUM([t4].[value]) AS [value], [t4].[Country] FROM ( SELECT [tO].[Country], ( SELECT SUM([t3].[value]) FROM ( SELECT ( SELECT SUM((CONVERT(DecimalB9, 4) , [t2].[Quantity])) * [t2].[UnitPrice] ) FROM [dbo].[Order Details] AS [t2] WHERE [t2].[OrderlD] = [tl].[OrderlD] ) AS [value], [tl].[CustomerlD] FROM [dbo].[Orders] AS [tl] ) AS [t3] WHERE [t3] . [CustomerlD] = [tO] . [CustomerlD] ) AS [value] FROM [dbo].[Customers] AS [tO] ) AS [t4] GROUP BY [t4] . [Country] ) AS [t5] ORDER BY [t5].[value] DESC Для продолжения нажмите Enter Как и ранее, этот запрос SQL содержит SQL для предшествующего запроса внутри него. Он помещает результат GROUP BY в запрос QUERY BY: SELECT [t5].[value] AS [TotalSales], [t5].[Country] FROM ( . . . SQL for groupResults . . . ) AS [t5] ORDER BY [t5].[value] DESC И это все еще не посылается на сервер; однако когда вы нажмете следующий раз <Enter>, то запустите цикл f oreach по результату запроса queryby, инициируя непосредственное выполнение этого запроса SQL. Из-за отложенного выполнения только один запрос SQL в действительности отправляется на сервер базы данных, что почти всегда более эффективно, чем обработка результатов множества запросов. Эффективность выполнения запроса SQL зависит от оптимизатора запросов, встроенного в большинство современных SQL-серверов баз данных — он проанализирует вложенные подзапросы и построит план выполнения, который выполнит его наиболее оптимальным образом. Хотя одни оптимизаторы запросов SQL лучше других, все же наиболее современные из них справляются с этой задачей лучше среднего программиста, поэтому вполне оправдано доверять LINQ to SQL и базе SQL выполнение наиболее трудной работы.
Глава 27. LINQ to SQL 881 Хотя и неплохо все-таки знать, какой SQL генерируется, и иметь возможность сообщить его экспертам по SQL в вашей организации, но благодаря LINQ to SQL вам не придется создавать SQL самостоятельно. Если возникает потребность в выполнении некоторых специфичных запросов SQL к базе данных, взгляните на классы ADO.NET, описанные в следующей главе, которые предназначены для того, чтобы позволить специфицировать любой SQL, который будет отправляться серверу. LINQ to SQL также предусматривает метод ExecuteQuery () в классе DataContext для базы данных — специально для тех ситуаций, когда вы, прежде всего, хотите использовать сгенерированный LINQ to SQL код SQL, но время от времени посылать специфические команды SQL в базу данных. Далее вы узнаете о другом способе, которым Visual C# 2008 и LINQ to SQL облегчает работу, когда речь пойдет о том, как можно генерировать пользовательский интерфейс, привязанный к запросам базы данных, с очень небольшими усилиями с вашей стороны. Привязка данных с LINQ to SQL Для того чтобы конечные пользователи могли взаимодействовать с приложением базы данных, вы должны спроектировать набор форм для ввода данных, а также отчетов для их вывода. Традиционно это требует большого объема утомительного кодирования для обновления бизнес-объектов из форм GUI с последующим вызовом методов на бизнес-уровне для запросов и обновления базы данных в результате просмотра пользователем данных, их ввода и обновления. Ранее вы видели, как O/R Designer генерирует классы LINQ to SQL для обработки чтения базы данных и навигации по отношениям. Но для вас есть и другие хорошие новости! Visual C# 2008 включает привязку данных в визуальных конструкторах форм, которая работает с LINQ to SQL, так что писать детализированный код для получения данных от форм конечного пользователя и помещения их в классы базы данных не придется. Вы можете лишь указывать и щелкать в визуальном конструкторе форм Visual C# 2008, чтобы сгенерировать код для изощренных графических приложений баз данных, которые прекрасно работают, практически не требуя ручного кодирования. В следующем практическом занятии вы увидите, как это работает. Практическое занятие Привязка ДЭННЫХ С LINQ to SQL Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новый проект Windows Forms по имени BegVCSharp_2 7_2_ LINQtoSQLDataBinding в каталоге C:\BegVCSharp\Chapter27. 2. Добавьте класс LINQ to SQL Northwind.dbml и подключитесь к базе данных Northwind, как это делалось в первом примере запроса LINQ to SQL в самом начале главы. Если вам нужно напоминание о том, как это делается, вернитесь к практическому занятию "Первый запрос LINQ to SQL' в начале этой главы и повторите шаги с 3 по 13. 3. Перетащите таблицы Customers и Orders в Northwind.dbml в O/R Designer. Экран проекта должен выглядеть, как показано на рис. 27.17.
882 Часть IV. Доступ к данным Рис. 27.17. Перетаскивание таблиц Customers и Orders 4. Откомпилируйте проект, чтобы классы, определенные в объектах Northwind. dbml стали доступны для следующих шагов. 5. Добавьте к проекту новый источник данных, выбрав пункт меню Data^Add New Data Source (Данные1^Добавить новый источник данных), как показано на рис. 27.18. Lj BggVCSh^.^J.llWCt&'lBbat^diny^ И icrojoft Visual С* 20 File Edit View Project Build Debug t QaU Tools Window Help Д ллА» Q8 И m 11ЖА1 j^^j ^ S>wwD«t»Sources ShaVAft*D ^ 9'0п~ J Add New Date Source... Puc. 27.18. Добавление нового источника данных 6. В диалоговом окне мастера Data Source Configuration Wizard (Мастер конфигурации источника данных) щелкните на элементе Object (Объект) в качестве типа источника данных, как показано на рис. 27.19. Затем щелкните на кнопке Next (Далее). Date Source Configuration Wizard it С So ом a Data Source Тура Иаага вШ the ajgl Ulrm eajl date fro»? 14* D«t*bese Service Lets you choose »n object that can later be used to generate data-bound controls. Ца*> [ frwa Caned Puc. 27.19. Выбор типа источника данных
Глава 27. LINQ to SQL 883 7. Выберите объект Customer (рис. 27.20), затем щелкните на кнопке Finish (Готово). Data Source Configuration Wizard ft St l.ct the Object You Wish to Bind to pratort tint contain* your ЯрМЫ Ш B^VCSh«^^XLI^oSQL0alaBm*n7 В (> BeqVCSh*rp.27.2_UNQtoSQLDetaBmd.ng Щ% Forml 4% Northw.ndDataContext «| Order ^ Program i {) BegVCSharpJTJ.UNQtoSQLOataeind^.Propcrties | Hioc •* %€fr> Ьнсх tnit MQffl with Mk r osoft Of Syit ttx\ Рис. 27.20. Выбор объекта Customer 8. Щелкните в панели дизайна Forml. cs, чтобы она появилась перед окном кода вашего проекта. Отобразите вновь добавленный источник данных, выбрав пункт меню Data^Show Data Sources (Данные^Показать источники данных), как показано на рис. 27.18, и раскройте узел Customer в источнике данных (рис. 27.21). шшщшшяшшшщ «ы Address ' 'та* £3 CompanyName i*i ContactName 4b» ComactTitle ; Ш, Country jtei CustomerlO ita (|j .jiJ Orders ■ы Phone «ы PostarCode -ReoK>n 4|| Database Explorer ^p Data Sources 1 ■W Fermi v rormLcilDeiignl l Рис. 27.21. Узел Customer 9. Немного увеличьте размер Forml. cs, поскольку нужно будет добавить сюда ряд элементов управления. 10. Щелкните на узле Customer и в раскрывающемся списке (рис. 27.22) выберите Details (Подробности) в качестве типа элемента управления по умолчанию для создания этой таблицы. Щелкните на узле Customer и перетащите его в окно визуального конструктора Forml.cs (рис. 27.23). На форме появится набор полей ввода данных (рис. 27.23) наряду с двумя новыми объектами: customerBindingSource и customerBindingNavigator. 11
884 Часть ГУ. Доступ к данным Data Sources •» # X I ! уЛ DataGndView вг»» "i Г5 [None] £ustomue... Я ■■■■■J ' ;«bt CustomerTO •ы Раж ♦ ,J Orders [«ы( Phone •ы PostalCode •ы Region Рггс. 27.22. Выбор типа элемента управления для таблицы Рис. 27.23. Перетаскивание узла Customer А что это за странные псевдо-элементы управления внизу формы? Их не видно на экране при запуске приложения. На самом деле один из этих новых объектов появляется в форме. Этот объект — customerBindingNavigator — панель навигации в верхней части формы, предназначенная для перемещения по строкам базы данных, как показано на рис. 27.23. Объект customerBindingSource контролирует привязку элементов управления формы к источнику Customers LINQ to SQL. Хотя он невидим в форме, на самом деле он взаимодействует с элементами управления и вашими классами LINQ to SQL, обеспечивая их синхронизацию с базой данных. 12. Щелкните на узле Orders и перетащите его в окно визуального конструктора Forml. cs справа от полей ввода данных Customer (рис. 27.24). На форме появится элемент управления типа сетки данных (см. рис. 27.24) вместе с новой привязкой источника orderBindingSource. Никакой панели навигации добавлено не будет, потому что Orders — зависимый член объекта Customer, и его навигация управляется позицией в родительской таблице Customers.
Глава 27. LINQ to SQL 885 TT7 •ы City •£• '.Omptr '.• " f «м CuilomtrtO t(PTc*S*»jTy — PoiuKotf* '•VcirtW»^i««*nflSeurt« ,/*cu»»o»n«i«.«%*«flN»vigitof *V o*«nl.n*«9Sou» ^Pi<»b«i«f.pW». 4-J]D«Uiou4« Pwc. 27.24. Перетаскивание узла Orders 13. Выполните двойной щелчок в панели заголовка в окне визуального конструктора Forml. cs для создания обработчика события FormLoad. Появится представление кода Forml. cs, которое включает шаблон обработчика событий, готовый для редактирования. 14. Добавьте следующие строки к классу Forml в редакторе кода: public partial class Forml : Form { NorthwindDataContext northwindDataContext = new NorthwindDataContext () ; public Forml () { InitializeComponent (); } private void Forml_Load(object sender, EventArgs e) { cus tome rBindingS our ce. DataSource = northwindDataContext. Cus toilers; 15. Откомпилируйте и выполните программу (можно просто нажать <F5> для запуска отладки). После этого вы увидите информацию о заказчиках из США и их заказах — подобно тому, как это показано на рис. 27.25 для заказчиков из Германии. t ''CM Н « :i X o>U > N v > .4 ALFM OXVOOTCMS 0KW074J21 1220» [• s •■*■ WM2 10702 1M» 10S52 ПОП а—в ■1 - -■ 4fW НПО "Хняа"" .. ИЙ! u ...: hfegO * i ;i ■■■ !»'«• I0.VI»7 10/11ИГ i плш ink m «? »e Ц.*г«:*е vanm 10/11-1»? ПДОМ7 212/1M» А17ЛЯ1 i7 1«i ■ Ц1ЮИ1 »2.1JJ7 ia-i3i»7 10/21 1»7 1<21/1JM 1 24.19» VJVHM ^ • 2 i 3 i ии 1 Рис. 27.25. Детальная информация о заказчиках из Германии
886 Часть IV. Доступ к данным Воспользуйтесь кнопками навигации в левом верхнем углу для перемещения по записям о заказчиках. Вы увидите, как изменяются значения полей заказчика, и как заказы каждого заказчика отображаются в сетке данных по мере перемещения по данным. Довольно неплохая функциональность, учитывая, что для этого пришлось написать всего пару строк кода. Описание полученных результатов Процесс создания и использования отображения LINQ to SQL Northwind.dbml точно такой же, как в предыдущих примерах. Как и в предыдущих примерах консольных приложений, вы должны создать экземпляр класса NorthwindDataContext, через который обеспечивается доступ ко всем прочим объектам LINQ to SQL. Здесь вы сделали данный экземпляр частью класса Forml вместо добавления его в метод Main (): public partial class Forml : Form { NorthwindDataContext northwindDataContext = new NorthwindDataContext (); Ключевая строка кода, который подключает привязанные к данным поля к классу NorthwindDataContext, заключена в коде, добавленном в метод Form_Load(): private void Forml_Load(object sender, EventArgs e) { customerBindingSource.DataSource = northwindDataContext.Customers; Хотя элементы управления данными были сгенерированы для вашего класса LINQ to SQL, когда был специфицирован объект в качестве источника данных, присваивание члена DataSource — вот что сообщает С# о том, из какого экземпляра объекта следует наполнять элементы управления. Однако, как только начальный экземпляр найден, остальное происходит автоматически "за кулисами", когда пользователь взаимодействует с элементами управления на экране, и вы не должны ничего кодировать самостоятельно! Что может быть проще? Обновление связанных данных с помощью LINQ to SQL Одна часть функциональности, отсутствие которой вы, возможно, заметили в предыдущем примере — это внесение изменений в базу данных. Кнопка Save (Сохранить), помеченная пиктограммой с изображением дискеты, справа от линейки навигации (рис. 27.26) по умолчанию недоступна до тех пор, пока вы не включите ее в своем коде. Рис. 27.26. Кнопка Save Это значит, что любые изменения в данных, которые вносятся в программе, не будут сохранены в базе данных. К счастью, включить функциональность сохранения очень легко, в чем вы убедитесь в следующем практическом занятии.
Глава 27. LINQ to SQL 887 Практическое занятие I Обновление привязанных данных с LINQ to SQL Для создания примера выполните следующие шаги в Visual C# 2008. 1. Модифицируйте существующий проект Windows Forms BegVCSharp_2 7_2_ LINQtoSQLDataBinding в каталоге С: \BegVCSharp\Chapter27. 2. В окне визуального конструктора Forml.cs выполните двойной щелчок на кнопке Save в панели навигации. 3. В окне кода Forml.cs появится пустой обработчик события customerBinding NavigatorSaveltemClick. Добавьте следующую строку к этому методу-обработчику: private void customerBindingNavigatorSaveItem_Click(object sender, EventArgs e) northwindDataContext.SubmitChanges(), } 4. Вернитесь в окно визуального конструктора Forml.cs и установите свойство Enabled для элемента управления customerBindingNavigatorSaveltem в True. 5. Если вы выбрали No (Нет), чтобы не копировать файл базы данных Northwnd. MDF, как рекомендовано в предыдущих примерах, перейдите к шагу 6. Если вы выберете Yes (Да), щелкните на Northwnd.MDF в Solution Explorer. В окне Properties (Свойства) установите свойство Copy to Output Directory (Копировать в выходной каталог) в Copy if Newer (Копировать, если новее), как показано на рис. 27.27. Это гарантирует, что Visual C# 2008 не перепишет измененную локальную копию, когда вы запустите проект. 6. Откомпилируйте и выполните программу (можно нажать <F5> для запуска отладки). Soiut.on bplorer Solution B«rVCSb«rp.:7.2.UWQtoSQlD*t«Bin>-ling' fl project' » 3 x ' tti-arc— - 3 Solution BegVCSh»fp.27.2.UNQtoSQlDM*8ir>dto9 A project) 3 B«9VCSturp.27.2_llMQtoSQlD*teBfaMfin9 || Properties .-: ^ DataSources * Customer datasource jj Assembtylnfc.es V _2 Resources res* > J Settingssettings I Л References j} app.config Ъ Я Forml.cs *^J F0rml.Desr9ner.cs <fjj Forml.resx 3 J| Normwmd.dbml Ц Northwmd.dtoml.la/out **Й North*"rMt«l«»9ft«'.cs « Щ NORTHWN0M)F4! Я Pr09ram.cs NOKTlIwTiU.MDf File Pfopert»c$ ш Biuld Action Content Copy always Custom Too* Custom Too» Namespace FrleName fulif-eth [Do not copy • с • к«('л ащг"•' щшп'bijwьлягр..'!.:_i Рис. 27.27. Установка свойства Copy to Output Directory в Copy if Newer
888 Часть IV. Доступ к данным 7. Модифицируйте поле (например, введите значение ALKFI для Region в первой записи заказчика). После проведения изменения перейдите к следующей записи, щелкнув на стрелке вправо, и затем щелкните на кнопке Save. Вернитесь обратно к первой записи, и вы обнаружите, что изменения сохранены. Они сохранятся, даже если вы выйдете из программы и перезапустите ее. Описание полученных результатов Метод SubmitChanges (), который вызывается в обработчике события кнопки Save, сохранит все имеющиеся в памяти изменения в экземпляре DataContext в базу данных: northwindDataContext.SubmitChanges() / Это включает все объекты базы данных, загруженные в память, и связанные с ними записи. Это не включает данные, введенные в поле, но еще не сохраненные в памяти; вот почему нужно перейти к следующей записи перед щелчком на Save. Соответствующие операторы SQL вставки и обновления выполняются в базе данных на основе изменений, которые были внесены в объекты. Резюме На этом наш тур по LINQ to SQL завершается. В главе рассматривались следующие основные темы. □ Концепция объектно-реляционного отображения (ORM) и способ ее реализации LINQ to SQL в С#. □ Использование O/R Designer в Visual C# 2008 с целью создания объектов для определенной базы данных. □ Использование запросов LINQ to SQL с объектами, созданными O/R Designer. □ Навигация по связям между объектами базы данных с использованием как кода явных запросов, так и сгенерированного кода. □ Группировка и упорядочивание для выполнения запросов поддержки принятия решений с LINQ to SQL. □ Привязка объектов LINQ to SQL к графическим элементам управления. □ Обновление базы данных с использованием метода LINQ to SQL SubmitChanges (). Следующая глава посвящена применению LINQ для нереляционных данных XML. Упражнения Для выполнения описанных ниже упражнений добавьте в решение класс отображения LINQ to SQL Northwind.dbml, как описано ранее в этой главе. Перетащите таблицы Customers, Employees, Order Details, Orders и Products в панель O/R Designer для Northwind.dbml, как показано на рис. 27.28. 1. Воспользуйтесь LINQ to SQL для отображения детальной информации из таблиц Products и Employees базы данных Northwind. 2. Создайте запрос LINQ to SQL для отображения наиболее продаваемых продуктов в базе данных Northwind. 3. Создайте групповой запрос для показа наиболее продаваемых продуктов по странам.
Глава 27. LINQ to SQL 889 4. Создайте набор привязанных к данным элементов управления для редактирования информации о продуктах из базы данных Northwind. шшптг- тг I % IJ Date Connection* . t, NORTHWNO MOF | \U DjUbM* Duo/trm > 3 Cetegonei '* 3 CustomefCurtomefOemo ♦ 3 CmtomerOemoerepbici fa ":5jCZ , 3 EmptoyeeTerntorie» t 3*«9*« fa 3»4>p«« » 3 Su«*«»* 4; 3 T«n»on« 4. ^ Vnw» . _J Stored Procedures t _J functionf ■ JtType fc—r U Proper 1 2f CustomeHD 2f Comp*nyN»me 3P Cor»bxtN«m« 3? ConbxtTitte 9Po% 3* Ледюп ^PoiUlCode 31 Country 3" Phon* 3^„ ^ 1**. -Ptopert*» 1 * £тр1суееЮ 3f U«N»me 3* fimNeme afTrtle ■3* TipeOKourtesy АРМАМ 3* rt^eOete 31 Addnn« !tf City Л? Region 2? PotalCode 31 Country 3* Mome**one 31 I »renvon 31 Photo 3* Notet 3" RepomTo 3* PhctoPet* '.' A «« "I -Propertm I »3^ OfdtrtD I ^? Customer© "?!* {mptoyeelD 1 2f OiderOete 3* РеолиомОке 31 Sn'PpedOete I 3* Sh«pAe 31 freight 3* Sh.pN»me 3* ShipAddress 1 "tfSbpOty 3* Sh.pRegon 3^ ShioPoiulCode j 31 ShipCountry H •f Order© 3* Product© 3* Un.tPnce 3* Quantity 3" Otcount 1 PwTt ■ Properties ' jf Product© 3* ProductName 3* Supplier© j 3* Category© 31 QuenbtyPerUnit * OtPnce jf UnioinStocli 31 UnruOnCVrJer 3* Reordertevet ЗГ CNKontmuerJ ~Tk r 1—-/ Рис. 27.28. Перетаскивание таблиц Customers, Employees, Order Details, Orders и Products в панель О/R Designer
28 ADO.NET и LINQ поверх DataSet В предыдущих главах был представлен язык LINQ, и было показано, как он работает с объектами. В этой главе речь пойдет об ADO.NET — традиционном способе доступа к базам данных в предыдущих версиях С# и .NET. Кроме того, мы представим LINQ поверх DataSet — версию LINQ, взаимодействующую с ADO.NET. Все примеры этой главы используют базу данных примеров Northwind (за исключением явно отмеченных). Инструкции по инсталляции базы данных примеров SQL Server Northwind можно найти в предыдущей главе. В частности, в этой главе будут рассматриваться следующие темы. □ Обзор ADO.NET и структура его главных классов. □ Чтение данных посредством DataReader и DataSet. □ Обновление базы данных, добавление и удаление записей. □ Работа с отношениями в ADO.NET. □ Чтение и запись XML-документов в ADO.NET. □ Выполнение команд SQL напрямую из ADO.NET □ Выполнение хранимых процедур из ADO.NET □ Опрос объектов ADO.NET средствами LINQ поверх DataSet. После обзора ADO.NET вы изучите концепции, положенные в основу этой технологии. Затем вы сможете создать несколько простых проектов примеров и приступить к изучению классов ADO.NET.
Глава 28. ADO.NET и LINQ поверх DataSet 891 Что такое ADO.NET? ADO.NET — это наименование набора классов, которые используются с С# и .NET Framework для доступа к данным в реляционном, таблично-ориентированном формате. Это включает реляционные базы данных, такие как Microsoft SQL Server и Microsoft Access, наряду с другими базами данных и даже не реляционными источниками данных. Технология ADO.NET интегрирована в .NET Framework и спроектирована для использования с любым языком .NET, в особенности — С#. ADO.NET включает пространство имен System.Data и его вложенные пространства имен, такие как System. Data. SqlClient и System. Data.Linq, плюс некоторые специфические связанные с данными классы из пространства имен System.Xml. Вы познакомились с XML в главе 25; в этой главе будет описана поддержка XML со стороны ADO.NET. Физически классы ADO.NET содержатся в сборке System.Data.dll и связаны со сборками System.Data.xxx.dll, опять с некоторыми исключениями, такими как XML. Почему это называется ADO.NET? Вы можете недоумевать, почему эта часть .NET Framework получила собственное загадочное прозвище— ADO.NET? Почему бы просто не назвать ее System.Data и все? ADO.NET происходит от ActiveX Data Objects (ADO) — широко распространенного набора классов, которые использовались для доступа к данным в предыдущих поколениях технологий Microsoft. Имя ADO.NET применяется потому, что Microsoft хотела ясно указать на то, что этот набор классов призван заменить ADO, и что он является предпочтительным интерфейсом доступа к данным в среде программирования .NET — по крайней мере, до LINQ to SQL. ADO.NET служит тем же целям, что и ADO, предоставляя легкие в использовании классы для доступа к данным, обновленные и расширенные для среды программирования .NET. Но хотя он играет ту же роль, что и ADO, классы, свойства и методы ADO.NET довольно значительно отличаются от их аналогов в ADO. Если вы заинтересованы в подробностях истории разработки этих технологий и их отношением с другими интерфейсами работы с базами данных Microsoft, такими как ODBC или OLE DB, загляните в краткую историю, приведенную ниже. В противном случае перейдите к следующему разделу, чтобы приступить к изучению ADO.NET. Очень краткая история доступа к данным Когда появились первые системы управления данными, такие как Oracle и DB2 от IBM, любой разработчик, которому был необходим доступ к данным, нуждался в наборе функций, специфичных для каждой системы управления данными. Каждая система имела собственную библиотеку функций, подобную Oracle Call Interface (OCI) для Oracle, или DBLib для Sybase SQL Server (позднее купленного Microsoft). Это позволяло программам иметь быстрый доступ к данным, поскольку они напрямую взаимодействовали с базами данных. Однако необходимость программистам быть знакомыми с разными библиотеками для каждой базы данных, с которой им приходилось работать, приводила к тому, что задача написания управляемых данными приложений чрезвычайно усложнялась. Это также означало, что если компания заменяла используемую базу данных, все ее приложения нужно было полностью переписывать.
892 Часть IV. Доступ к данным Эта проблема была решена с появлением ODBC (Open Database Connectivity — открытый интерфейс взаимодействия с базами данных). ODBC был разработан Microsoft в кооперации с другими компаниями в начале 90-х годов. ODBC предоставил полный набор функций, которые разработчики могли применять с любыми системами баз данных. Эти функции транслировались в специфичные для базы данных вызовы драйверами конкретной системы управления базами данных. Это решило множество проблем, связанных с патентованными библиотеками баз данных — разработчикам теперь достаточно было знать, как пользоваться одним набором функций (функциями ODBC); и если компания заменяла свою систему управления базами данных, все, что приходилось изменить — это код подключения к базе данных. Однако оставалась одна проблема. Хотя безбумажный офис по большей части остается мечтой, компании обзавелись огромными объемами данных, хранимых в электронном виде— электронная почта, Web-страницы, файлы Project 2000 и т.д. ODBC] был хорош для доступа к данным в традиционных базах, но не позволял обращаться к данным других типов, которые не размещались в строгом порядке строк и столбцов, а порой вообще не имели регулярной структуры. Ответом на эту проблему стало появление OLE DB. Технология OLE DB работает способом, аналогичным ODBC, предоставляя слой абстракции между базой данных и приложениями, которым нужен доступ к данным. Клиентское приложение взаимодействует с источником данных, которым может быть традиционная база данных или любое другое место хранения данных, через поставщика OLE DB для данного источника данных. Данные из любого источника предоставляются приложению в табличном формате — как будто они поступили из базы данных. Вдобавок, поскольку OLE DB обеспечивает доступ к данным, предоставляемым существующими драйверами ODBC, он может быть использован и для доступа к базам, поддерживаемым ODBC. Как вы вскоре убедитесь, ADO.NET поддерживает как OLE DB, так и ODBC очень похожим образом. И последней унаследованной технологией доступа к данным, появившейся до эпохи .NET, была ActiveX Data Objects (ADO). ADO — это просто тонкий слой, располагающийся поверх OLE DB, который позволяет программам, написанным на таких высокоуровневых языках, как Visual Basic, обращаться к данным OLE DB. Технология ADO.NET появилась вместе с .NET Framework 1.0, и эволюционировала вместе с различными версиями .NET с добавлением поставщиков данных, постепенно при этом совершенствуясь. LINQ to SQL представляет собой первый серьезный "сдвиг парадигмы" в доступе к данным с момента появления .NET Framework. ADO.NET продолжит развитие вместе с LINQ, с появлением таких новых средств, как ожидаемый ADO.NET for Entities, который должен выйти после Visual Studio 2008. ADO.NETfor Entities— расширенный интерфейс, нацеленный на разработчиков-экспертов в AD0.NET, и его описание выходит за рамки настоящей книги. Продолжая свое развитие, он не является официальной составной частью Visual Studio 2008. Цели проектирования AD0.NET Ниже кратко перечислены основные цели проектирования ADO.NET. □ Простой доступ к реляционным и нереляционным данным. □ Расширяемость для поддержки еще более широкого круга источников данных, чем предшествующие технологии. □ Поддержка многоуровневых4 приложений в Internet. □ Унификация доступа к данным XML и реляционным данным.
Глава 28. ADO.NET и LINQ поверх DataSet 893 Простой доступ к реляционным данным Первичная цель ADO.NET — обеспечение простого доступа к реляционным данным. Прямолинейные, легкие в использовании классы представляют таблицы, столбцы и строки в реляционной базе данных. Вдобавок ADO.NET предлагает класс DataSet, представляющий набор данных из реляционных таблиц, инкапсулированных в единственном элементе, с сохранением целостности отношений между ними. Это новая концепция в ADO.NET, которая значительно расширила возможности интерфейса доступа к данным. Расширяемость ADO.NET является расширяемой технологией. Она предлагает каркас для подключаемых поставщиков данных .NET (также называемых управляемыми поставщиками), которые могут быть построены так, чтобы читать и записывать данные из любого источника. ADO.NET поставляется с несколькими встроенными поставщиками данных, включая один для базы данных Microsoft SQL Server, один — для Oracle и по одному для каждого обобщенного интерфейса баз данных: Microsoft Open DataBase Connectivity (ODBC) API и API, основанный на COM Object Linking and Embedding DataBase (OLE DB). Почти каждая база данных и формат файла данных имеет доступных поставщиков ODBC или OLE DB, включая Microsoft Access, базы от независимых поставщиков и нереляционные источники данных; таким образом, ADO.NET может быть использован в качестве интерфейса для работы почти с любой базой данных или форматом данных — через одного из встроенных поставщиков данных. Многие поставщики систем управления базами данных, такие как MySQL и Oracle, также предлагают "родные" поставщики данных .NET для своих продуктов. Поддержка многоуровневых приложений ADO.NET спроектирован для работы в многоуровневых приложениях. Сегодня это — наиболее распространенная архитектура для бизнес-приложений и приложений электронной коммерции. В многоуровневой архитектуре разные части прикладной логики распределены по разным уровням или слоям, и взаимодействуют только с соседними слоями. Одним из наиболее распространенных подходов является трехуровневая модель, которая состоит из следующего. □ Слой данных. Содержит базу данных и код доступа к данным. □ Бизнес-слой. Содержит бизнес-логику, который определяет уникальную функциональность приложения, и абстрагирует ее от других слоев. Этот слой иногда называют средним слоем. □ Слой представления. Обеспечивает пользовательский интерфейс и контроль потока управления приложения, наряду с такими вещами, как проверка пользовательского ввода. ADO.NET использует открытый Internet-стандарт XML для взаимодействия между слоями, позволяя передавать данные через Internet-брандмауэры и обеспечивая возможность He-Microsoft-реализаций одного или более слоев. Унификация доступа к реляционным данным и XML Другая важная цель, которой служит ADO.NET состоит в построении моста между реляционными данными в строках и столбцах и документами XML, имеющими иерархическую структуру данных. Технология .NET строится вокруг XML, a ADO.NET интенсивно использует его.
894 Часть IV. Доступ к данным Теперь, когда вы познакомились с целями ADO.NET, давайте взглянем на сами классы ADO.NET. Обзор классов и объектов ADO.NET Диаграмма на рис. 28.1 демонстрирует базовые классы ADO.NET Обратите внимание, что это не диаграмма наследования. Она показывает отношения между наиболее часто используемыми классами. Объекты-потребители ADO.NET Объекты-поставщики ADO.NET 1 DataSet —Н DataTabl< -Н DataRow L->| DataColumn 1—►! DataRelation Connection Command CommandBuilder DataReader DataAdapter Рис. 28.1. Базовые классы ADO.NET Класс подразделяются на классы объектов-поставщиков данных .NET и классы объектов потребителей. □ Объекты-поставщики специфичны для каждого типа источников данных — действительное чтение и запись информации в источники данных выполняется специфичными для поставщика объектами. □ Объекты-потребители — это те, что применяются для доступа и манипуляции данными после того, как они прочитаны в память. Объекты-поставщики требуют наличия активного соединения. Вы используете их сначала для чтения данных, а затем, в зависимости от потребностей, вы можете работать с данными в памяти через объекты-потребители. Вы можете также обновлять данные в источнике данных, используя объекты поставщика для записи изменений обратно в источник данных. Таким образом, объекты-потребители оперируют в автономном (отключенном) режиме; вы можете работать с данными в память даже тогда, когда соединение с базой данных закрыто. Объекты поставщика Объекты поставщика (provider objects) определены в каждом поставщике данных .NET. Имена их включают уникальное имя поставщика; например, действительный объект соединения с поставщиком OLE DB называется OleDbConnection, а класс поставщика .NET SQL Server— SqlConnection. Объект Connection Объект Connection — обычно первый объект, который вы используете, еще до применения большинства прочих объектов ADO.NET; он предоставляет базовое соединение с вашим источником данных. Если вы используете базу данных, требующую указания имени и пароля пользователя на удаленном сетевом сервере, то объект Connection заботится о таких деталях, устанавливая соединение и регистрируя пользователя.
Глава 28. ADO.NET и LINQ поверх DataSet 895 Если вы знакомы с классическим ADO, обратите внимание, что Connection и прочие объекты, выполняющие аналогичные функции в классическом ADO, имеют сходные имена в ADO.NET. Объект Command Вы используете объект Command для передачи команды источнику данных, такой как SELECT * FROM Customers для запроса данных из таблицы Customers. Специфичные для поставщика имена включают SqlCommand для SQL Server, OdbcCommand— для ODBC и OleCommand - для OLE DB. Объект CommandBuilder Объект CommandBuilder используется для построения команд SQL для модификации данных из объектов на основе однотабличного запроса. Мы рассмотрим этот объект более подробно, когда будем изучать способы обновления данных. Специфичные для поставщика имена включают SqlCommandBuilder для SQL Server, OdbcCommandBuilder для ODBC и OleCommandBuilder для OLE DB. Объект DataReader Это быстрый, простой в использовании объект, который читает однонаправленные, доступные только для чтения потоки данных, такие как множество заказчиков, полученное из источника данных. Этот объект обеспечивает максимальную производительность для простого чтения данных; первый пример продемонстрирует использование этого объекта. Специфичные для поставщика объекты включают: SqlDataReader для SQL Server, OdbcDataReader для ODBC и OleDbDataReader для OLE DB. Объект DataAdapter Это класс общего назначения, выполняющий различные операции, специфичные для источника данных, включая обновление измененных данных, наполнение объектов DataSet (см. раздел "Объект DataSet") и другие операции, показанные в следующих примерах. Специфичные для поставщика имена включают SqlDataAdapter для SQL Server, OdbcDataAdapter для ODBC и OleDbAdapter для OLE DB. Объекты-потребители Это объекты, определенные для отключенной, потребляющей стороны ADO.NET. Они не связаны ни с каким определенным поставщиком данных .NET и находятся в пространстве имен System. Data. Объект DataSet DataSet — разновидность потребляющих объектов. DataSet представляет набор связанных таблиц, воспринимаемых как единое целое в рамках приложения. Например, Customers, Orders и Products могут быть таблицами в одном DataSet, представляющем каждого заказчика и продукты, которые он заказал у вашей компании. С помощью этого объекта можно быстро получать все необходимые данные из каждой таблицы, рассматривать и изменять их, будучи отключенным от сервера, а затем обновлять сервер изменениями в одной эффективной операции. DataSet имеет средства, которые позволяют обращаться к объектам более низкого уровня, представляющим индивидуальные таблицы и отношения. Эти объекты — DataTable и DataRelation — будут рассмотрены в следующих разделах.
896 Часть IV. Доступ к данным Объект DataTable Этот объект представляет одну из таблиц в DataSet, такую как Customers, Orders или Products. Объект DataTable имеет средства, позволяющие вам обращаться к его строкам и столбцам. □ Объект DataColumn. Представляет один столбец в таблице вроде Order ID или CustomerName. □ Объект DataRow. Представляет одну строку или связанные данные из таблицы наподобие СиstomerID, name, address определенного заказчика или т.д. Объект DataRelation Объект DataRelation представляет отношение между двумя таблицами, установленное через общий столбец. Например, таблица Orders может иметь столбец СиstomerID, идентифицирующий заказчика, разместившего заказ. Объект DataRelation может быть создан как представляющий отношение между Customers и Orders через разделенный столбец Customer ID. Все это должно дать представление об общей структуре объектов в ADO.NET. Существуют и другие объекты, помимо перечисленных, но давайте пока пропустим детали и рассмотрим некоторые примеры, демонстрирующие как все это работает. Использование пространства имен System.Data Первый шаг при использовании ADO.NET внутри кода С# состоит в установке ссылки на пространство имен System. Data, в котором находятся все классы ADO. NET. Поместите следующую директиву using в начало каждой программы, использующей ADO.NET: using System.Data Затем вы должны сослаться на поставщика данных .NET для конкретного используемого вами источника данных. Поставщик данных .NET SQL Server Если вы используете базу данных SQL Server (версии 7 или выше), включая настольные ее версии (SQL Express или MSDE), то наилучшая производительность и наиболее прямой доступ к ее средствам обеспечивается "родным" (специфичным для SQL) поставщиком данных .NET, на который ссылается следующая директива using: using System.Data.SqlClient; Сам Oracle предлагает собственный поставщик данных .NET, который называется System. DataAccess .Client. Вы можете получить его в отдельной загрузке, которую предлагает Oracle. Применять поставщик данных .NET от разработчика базы данных или же поставщик, предоставленный в .NET Framework — выбор за вами. Обычно поставщик от разработчика базы может использовать больше специальных средств конкретного продукта, но в качестве базового применения или применения начинающими разработчиками такой выбор может оказаться не особенно хорошим. Любой поставщик будет работать; используйте тот, который вы предпочитаете. Поставщик данных OLE DB Для источников данных, отличных от SQL Server или Oracle (такого как Microsoft Access), можно применять поставщик данных .NET OLE DB, на который ссылается следующая директива using: using System.Data.OleDB;
Глава 28. ADO.NET и LINQ поверх DataSet 897 Этот поставщик использует DLL-библиотеку поставщика OLE DB для определенной базы данных, к которой вы обращаетесь; поставщики OLE DB для многих распространенных баз данных инсталлируются вместе с Windows, и среди них — Microsoft Access, который вы используете в следующих примерах. Поставщик данных ODBC .NET Если вы используете источник данных, для которого не существует доступного "родного" поставщика OLE DB, то поставщик данных ODBC .NET является хорошей альтернативой, потому что большинство поставщиков поддерживают интерфейс ODBC. Для ссылки на него включите в программу следующую директиву using: using System.Data.Odbc; Другие "родные" поставщики данных .NET Если доступен "родной" поставщик данных .NET, предназначенный специально для вашей базы данных, то, скорее всего, его использовать предпочтительней. Многие другие производители баз данных, а также независимые компании предлагают "родные" поставщики данных .NET для конкретных баз; выбор между применением "родного" поставщика и некоторого обобщенного поставщика, вроде поставщика ODBC, зависит от конкретных условий. Если для вас важнее переносимость, чем производительность, выбирайте обобщенный поставщик. Если же нужна максимальная производительность и наиболее полное использование средств конкретной базы данных, применяйте "родной" поставщик. Чтение данных с помощью DataReader В следующем практическом занятии вы просто получите некоторую информацию из таблицы Customers базы данных Northwind примеров SQL Server/MSDE (она была описана в главе 25). Таблица Customers содержит строки и столбцы с данными о заказчиках — клиентах торговой компании Northwind. В первом примере используется DataReader для извлечения столбцов CustomerlD и CompanyName из этой таблицы. актическое занятие Чтение данных посредством DataReader Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новое консольное приложение по имени DataReading в каталоге C:\BegVCSharp\Chapter28. 2. Начните с добавления директив using для классов ADO.NET, которые будут использоваться здесь: #region Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика SQL Server using System.Collections.Generic; using System.Text; #endregion
898 Часть IV. Доступ к данным 3. Добавьте следующий код в метод Main (): static void Main(string[] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF';" + (^"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Открыть соединение thisConnection.Open(); // Создать команду для этого соединения SqlCommand thisCommand = thisConnection.CreateCommand(); // Специфицировать запрос SQL для этой команды thisCommand.CommandText = "SELECT CustomerlD, CompanyName from Customers"; // Выполнить DataReader для указанной команды SqlDataReader thisReader = thisCommand.ExecuteReader() ; // Пока есть строки для чтения while (thisReader.Read()) { // Вывести столбцы ID и name Console.WriteLine("\t{0}\t{1}", thisReader["CustomerlD"], thisReader["CompanyName"]); } // Закрыть читатель thisReader.Close (); // Закрыть соединение thisConnection.Close (); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Откомпилируйте и запустите эту программу. Вы должны увидеть список идентификаторов заказчиков и названия компаний, как показано в следующем коде. Если вы не видите этого вывода, не волнуйтесь; очень скоро вы решите эту проблему. RICAR Ricardo Adocicados RICSU Richter Supermarkt ROMEY Romero у tomillo SANTG Sante Gourmet SAVEA Save-a-lot Markets SEVES Seven Seas Imports SIMOB Simons bistro SPECD Specialites du monde SPLIR Split Rail Beer & Ale SUPRD Supremes delices THEBI The Big Cheese THECR The Cracker Box TOMSP Toms Spezialitaten TORTU Tortuga Restaurante TRADH Tradigao Hipermercados TRAIH Trail's Head Gourmet Provisioners VAFFE Vaffeljernet
Глава 28. ADO.NET и LINQ поверх DataSet 899 VICTE Victuailles en stock VINET Vins et alcools Chevalier WARTH Wartian Herkku WELLI Wellington Importadora WHITC White Clover Markets WILMK Wilman Kala WOLZA Wolski Zajazd Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Первый шаг— ссылка на пространство имен System. Data и вашего поставщика, как описано ранее. Вы собираетесь использовать в этих примерах поставщика SQL Server .NET, так что понадобятся следующие строки в начале программы: using System.Data; using System.Data.SqlClient; Для извлечения данных программе требуется выполнить пять шагов. 1. Подключиться к источнику данных. 2. Открыть соединение. 3. Издать SQL-запрос. 4. Прочесть и отобразить данные с помощью DataReader. 5. Закрыть DataReader и соединение. Далее мы опишем все эти шаги по очереди. Сначала необходимо подключиться к источнику данных. Это делается посредством создания объекта соединения с использованием строки соединения. Строка соединения — просто символьная строка, содержащая имя поставщика для базы данных, к которой хотите подключиться, регистрационной (login) информации (пользователь базы данных, пароль, и т.п.), а также имени конкретной базы данных, которую необходимо использовать. Давайте посмотрим на определенные элементы этой строки соединения; однако при этом имейте в виду, что эти строки существенно отличаются у разных поставщиков данных, так что следует изучить специфическую информацию соединения для вашего поставщика данных, если она отличается от настоящего примера (информация соединения для Access будет показана в этой главе чуть позже). Строка, в которой вы создаете объект соединения, выглядит следующим образом: SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\NORTHWND.MDF';" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); SqlConnection — имя объекта соединения для поставщика данных SQL .NET; в случае применения OLE DB вы создавался бы OleDbConnection. Строка соединения состоит из именованных элементов, разделенных точками с запятой. Вот первый элемент: @"Data Source=.\SQLEXPRESS;" Это просто имя экземпляра SQL Server, к которому вы обращаетесь, в форме имя_компьютера\имя_экземпляра. Точка— удобное сокращение, принятое в SQL Server для ссылки на экземпляр сервера, запущенный на текущей машине. Вы могли бы также подставить вместо нее имя (local) или действительное сетевое имя вашего компьютера; например, если настольный компьютер называется Wroadrunner, можно указать в качестве имени сервера roadrunner\sqlexpress.
900 Часть ГУ. Доступ к данным SQLEXPRESS — имя жземпляра§ОУ Server. Может существовать несколько копий SQL Server, установленных на одной машине, и имя экземпляра, заданное во время инсталляции, сообщает SQL Server, какой именно из них нужен. SQLEXPRESS — имя по умолчанию, используемое при инсталляции SQL Express. Если бы уже была установлена другая версия SQL Server, то это имя было бы другим — имени экземпляра даже вообще могло бы не быть (и тогда потребовалось бы использовать в качестве имени (local) либо имя самой машины), или же это было бы другое имя, такое как local\NetSDK, применяемое для инсталляций MSDE в предыдущих версиях .NET Framework. Обратите внимание на символ @, предшествующий строке соединения, который обозначает строковый литерал, что обеспечивает правильную работу обратных слэшей в этой строке; в противном случае понадобились бы двойные слэши (\\) для защиты символа обратного слэша внутри строки С#. Если вы инсталлировали SQL Server, с которым собираетесь работать, то имя этого экземпляра SQL Server должно быть известно. Иначе нужно узнать это имя у самого SQL Server или сетевого администратора. Следующий элемент строки соединения выглядит так: AttachDbFilename='C:\SQL Server 2000 Sample Databases\NORTHWND.MDF'; Это тот же файл . MDF, который вы выгрузили в главе 27, содержащий данные примеров Northwind. Если вы поместите этот файл в другой каталог, измените путь, чтобы он соответствовал установке на компьютере. Следующая часть строки соединения специфицирует способ входа в базу данных. Здесь вы используете интегрированную безопасность регистрационной записи Windows, поэтому никакого отдельного имени пользователя и пароля указывать не нужно: Integrated Security=True; Эта конструкция специфицирует стандартную встроенную систему безопасности SQL Server и Windows. В противоположность ей, вместо Integrated Security вы могли бы специфицировать конструкции имени пользователя и пароля, как в случае User=sa;PWD=secret. Применение встроенной системы безопасности регистрационной записи Windows более предпочтительно, чем использование жестко закодированных имени и пароля в строке соединения. Следующая часть строки соединения специфицирует таймаут соединения, т.е. насколько долго программа будет ожидать установки подключения к базе данных SQL Server, прежде чем вернет ошибку. Здесь показано значение в 30 секунд; для реального приложения это вполне нормальное время установки соединения по сети, если сервер достаточно загружен. Connect Timeout=30; И последняя часть строки соединения указывает, используется ли пользовательский экземпляр для вашего соединения: User Instance=true; Пользовательский экземпляр — это локальный экземпляр SQL Server Express, работающий от имени вашей регистрационной записи Windows на локальном компьютере. Пользовательский экземпляр позволяет запускать локальную копию SQL Server для разработки программ, даже если ваша регистрационная запись Windows не имеет административных привилегий в Windows или базе данных. За дополнительной информацией о пользовательских экземплярах обращайтесь по следующим адресам: http://technet.microsoft.com/en-us/library/bb264 564.aspx http://technet.microsoft.com/en-us/library/msl4 3684.aspx
Глава 28. ADO.NET и LINQ поверх DataSet 901 Если вы получили исключение, указывающее па недоступность экземпляра, откройте окно запроса и запустите следующую команду базы данных: exec sp_configure 'user instance enabled', 1 Reconfigure Это включит пользовательские экземпляры и автоматически перезапустит SQL Server. Теперь у вас должен быть объект соединения, полностью сконфигурированный для вашей машины и базы данных (соединение еще пока не активно; для активизации его необходимо открыть). После получения объекта соединения можно перейти ко второму шагу — открыть его, что установит соединение с базой данных: thsConnection.Open(); Если метод Open () потерпит неудачу, например, если не будет найден SQL Server, будет сгенерировано исключение SqlException, и вы увидите сообщение, подобное представленному на рис. 28.2. An error has occurred whte estabbhing a connection to the server. When *J connecting to SQL Server 2005, this falure m*y be caused by the fact that ZJ under the default settings SQL Server does not alow remote connections. ^| Troubleshooting tips: Get general help for th»s exception. Search for more Help Onlne... Actions: ViewDetai... Copy exception detad to the clpboard Puc. 28.2. Исключение SqlException Это конкретное сообщение указывает, что программа не может найти базу данных SQL Server или сам сервер. Проверьте правильность имени сервера в строке соединения, и удостоверьтесь, запущен ли сервер. Третий шаг — создание объекта команды и передачи ему собственно команды SQL для выполнения операции базы данных (такой как извлечение некоторых данных). Код выглядит следующим образом: SqlCommand thisCommand = thisConnection.CreateCommand(); thisCommand.CommandText = "SELECT CustomerlD, CompanyName from Customers"; Объект соединения имеет метод по имени CreateCommand () для создания команды, ассоциированной с соединением, поэтому вы используете его для создания объекта команды. Сама команда присваивается свойству CommandText объекта команды. Вы хотите получить список идентификаторов заказчиков и имен компаний из базы данных Northwind, так что это — основа вашей команды запроса SQL: SELECT CustomerlD, CompanyName from Customers SELECT — это команда SQL для получения данных из одной или более таблиц. Распространенная ошибка — опечатка в имени одной или более таблиц, приводящая к другому исключению: thisCommand.CommandText = "SELECT CustomerlD, CompanyName from Customer"; Пропущена завершающая буква "s" в Customers, поэтому возникло исключение, показанное на рис. 28.3.
902 Часть ГУ. Доступ к данным InvaW object name 'Customer'. Troubleshooting tips: ; Qet aenerelhelpfor ^exception,: Search f<x more Help Onlne... Actions: ViewDetal... Copy exception detaJ to the dpboard J$ Рис. 28.3. Исключение, гласящее о неверпом имени объекта Исправим эту ошибку, перестроим приложение и двинемся дальше. Четвертый шаг состоит в чтении и отображении данных. Сначала данные читаются с помощью DataReader, который представляет собой легковесный, быстрый объект для быстрого получения результатов запроса. Он обеспечивает доступ только для чтения, поэтому вы не сможете использовать обновление данных; к этому мы приступим после завершения данного примера. Как было показано в предыдущем разделе, с помощью метода из последнего созданного объекта — объекта команды создается ассоциированный экземпляр объекта, который понадобится дальше, в данном случае — DataReader: SqlDataReader thisReader = thisCommand.ExecuteReader(); ExecuteReader () выполняет команду SQL в базе данных, поэтому любые ошибки базы данных генерируются здесь; он также создает объект-читатель для чтения сгенерированных результатов — здесь он присваивается thisReader. Существует несколько методов для получения результатов от читателя, но следующий процесс общепринят. Метод Read() объекта DataReader читает единственную строку данных, полученную в результате запроса, и возвращает true, пока еще есть строки для чтения, и false— в противном случае. Таким образом, вы устанавливаете цикл while для чтения данных методом Read () и печати результатов по мере получения их в каждой итерации: while (thisReader.Read()) { Console.WriteLine("\t{0}\t{1}", thisReader["CustomerlD"], thisReader["CompanyName"]) ; } Пока Read () возвращает true, Console. WriteLine ("\t{ 0} \t{ 1} ", ...) пишет строку, состоящую из двух частей, разделенных символами табуляции (\t). Объект DataReader представляет свойство indexer (см. в главе 11 обсуждение индексаторов). Индексатор indexer перегружен, и позволяет рассматривать столбцы как массив, в котором индексом служит имя столбца: thisReader [ "CustomerlD" ], thisReader [ "CompanyName" ], или же по целочисленному индексу: thisReader [ 0 ], thisReader [ 1 ]. Когда Read () возвращает false, достигнув конца результатов, цикл while завершится. Пятый и последний шаг состоит в закрытии всех открытых вами объектов, к которым относится объект-читатель и объект соединения. Каждый из них имеет метод Close (), который вы вызываете перед выходом из программы: thisReader.Close() ; thisConnection.Close() ; Вот и все, что нужно для обращения к одной таблице.
Глава 28. ADO.NET и LINQ поверх DataSet 903 Ту же программу можно переписать с несколькими небольшими изменениями, чтобы использовать версию этой же базы данных для Microsoft Access (nwind.mdb). Вы найдете ее в каталоге С: \Program Files\Microsoft Of fice\0f f iceXX\Samples (если инсталляция Microsoft Office включает Microsoft Access). Фрагмент XX в OfficeXX— это заполнитель для номера, такого как Officell для Office 2003 или Off ice 12 для Office 2007. Сделайте копию файла (во временном каталоге, например, С: \Northwind\nwind.mdb), чтобы всегда можно было вернуться к оригиналу. Если у вас еще нет Office, загрузите Nwind.exe из следующего источника: http://microsoft.com/downloads/details.aspx?familyid=c6661372-8dbe-422b- 8676-c632d66c529c & displaylang=en Запустите Nwind.exe для инсталляции базы данных примеров; эта программа запросит каталог, так что можно будет поместить базу туда, куда необходимо, например, в с: \Northwind. За исключением деталей строки соединения, внесенные изменения в следующем практическом занятии, будут работать с любым источником данных OLE DB. п?актыческое занятмв Чтение данных из базы Access 1. Создайте новое консольное приложение по имени ReadingAccessData в каталоге С:\BegVCSharp\Chapter28. 2. Начните с добавления директив using для классов поставщика OLE DB, которые будут использоваться: #region Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.OleDb; // Использовать пространство имен для // поставщика OLE DB .NET using System.Collections.Generic; using System.Text; #endregion 3. Добавьте следующий код в метод Main (): static void Main(string[] args) { // Создать объект соединения для поставщика Microsoft Access OLE DB; // обратите внимание на символ @, предшествующий строковому литералу, // чтобы правильно воспринимались обратные слэши в пути файла. OleDbConnection thisConnection = new OleDbConnection( @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Northwind\nwind.mdb"); // Открыть объект соединения thisConnection.Open (); // Создать объект команды SQL на этом соединении OleDbCommand thisCommand = thisConnection.CreateCommand (); // Инициализировать команду SQL SELECT для извлечения нужных данных thisCommand.CommandText = "SELECT CustomerlD, CompanyName FROM Customers"; // Создать объект DataReader на основе ранее определенного объекта команды OleDbDataReader thisReader = thisCommand.ExecuteReader ();
904 Часть ГУ. Доступ к данным while (thisReader.Read()) { Console. WnteLine("\t{0}\t{l}", thisReader["CustomerlD"], thisReader["CompanyName"]); } thisReader.Close (); thisConnection.Close (); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine (); } Описание полученных результатов Вместо объектов SqlConnection, SqlCommand и SqlDataReader вы создаете объекты OleDbConnection, OleDbCommand и OleDbDataReader. Эти объекты работают по существу таким же образом, как их аналоги из SQL Server. Соответственно вы изменяете директиву using, специфицирующую поставщика данных с using System.Data.SqlClient; на using System.Data.OleDb; Единственное другое отличие — строка соединения, которую вы должны изменить полностью. Первая часть строки соединения OLE DB — конструкция Provider — специфицирует имя поставщика OLE DB для базы данных этого типа. Для открытия баз Microsoft Access (файл . mdb) необходимо сделать так: Data Source=C:\Northwind\nwind.mdb Опять-таки, символ @, предшествующий строке соединения, специфицирует строковый литерал, чтобы правильно работали обратные слэши в путевом имени; в противном случае понадобились бы двойные слэши для правильного восприятия полного имени файла в С#. Чтение данных с помощью DataSet Вы только что узнали, как читаются данные посредством DataReader, а теперь посмотрим, как достичь тех же целей с помощью DataSet. Для начала подробно рассмотрим его структуру. DataSet — центральный объект ADO.NET; все операции любой сложности используют его. DataSet содержит набор объектов DataTable, представляющих таблицы базы данных, с которыми вы работаете. Объект Da taTable может быть всего один. Каждый объект DataTable имеет дочерние объекты DataRow и DataColumn, представляющие строки и столбцы таблицы базы данных. Вы можете получить все индивидуальные элементы таблиц, строк и столбцов через эти объекты, как будет описано ниже. Наполнение DataSet данными Основной операцией, которую придется выполнять с DataSet, вероятно, будет заполнение его данными методом Fill () объекта адаптера данных. Fill () — метод адаптера данных, а не метод DataSet, потому что DataSet — абстрактное представление данных в памяти, в то время как адаптер данных — это объект, привязывающий DataSet к определенной базе данных. Fill () имеет множество перегрузок, но используемая в этой главе принимает два параметра — первый специ-
Глава 28. ADO.NET и LINQ поверх DataSet 905 фицирует DataSet, который вы хотите наполнить, а второй — имя DataTable внутри DataSet, содержащий данные, которые вы хотите загрузить. Доступ к таблицам, строкам и столбцам в DataSet Объект DataSet включает в себя свойство по имени Tables, которое является коллекцией всех объектов DataTable внутри DataSet. Свойство Tables имеет тип DataTableCollection и включает в себя перегруженный индексатор, а это означает, что вы можете обращаться к каждому индивидуальному объекту DataTable одним из двух следующих способов. □ По имени таблицы. thisDataSet .Tables ["Customers"] специфицирует DataTable по имени Customers. □ По индексу (начинающемуся с нуля), this DataSet. Tables [ 0 ] специфицирует первую DataTable в DataSet. Внутри каждого DataTable есть свойство Rows, представляющее собой коллекцию индивидуальных объектов DataRow. Свойство Rows имеет тип DataRowCollection и представляет собой упорядоченный список, индексированный номерами строк. То есть myDataSet.Tables["Customers"].Rows[n] специфицирует строку номер n - 1 (нумерация начинается с нуля) в объекте DataTable по имени Customers внутри thisDataSet. (Разумеется, для спецификации DataTable также можно использовать альтернативный синтаксис индексов.) Можно было бы ожидать, что DataRow имеет свойство типа DataColumnCollection, но на самом деле все не так просто, поскольку вы, естественно, хотели бы воспользоваться преимуществами типизации данных индивидуальных столбцов в каждой строке, чтобы столбец, хранящий символьные данные, был представлен строкой, а столбец, содержащий целые числа — целочисленным объектом и т.д. Объект DataRow сам имеет перегруженное свойство indexer, позволяющее обращаться к индивидуальным столбцам по имени и по номеру. Таким образом, thisDataSet.Tables["Customers"].Rows[n]["CompanyName"] специфицирует столбец ColumnName строки номер n - 1 в DataTable по имени Customers объекта thisDataSet. Объект DataRow здесь представлен как thisDataSet .Tables ["Customers"] .Rows [n]. Следующее практическое занятие демонстрирует все описанные приемы на примере программы, использующей DataSet и его дочерние классы. Начнем с программы, которая просто читает данные. Ее функциональность подобна программе-примеру DataReader, но выполняет свою задачу посредством DataSet. J2E!!!]^ Чтение данных с помощью DataSet Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новое консольное приложение по имени DataSetRead в каталоге С: \ BegVCSharp\Chapter28. 2. Добавьте директивы using для необходимых классов ADO.NET: tregion Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server
906 Часть IV. Доступ к данным using System.Collections.Generic; using System.Text; #endregion 3. Добавьте следующий код в метод Main (): static void Main(string [ ] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF1;" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Создать объект DataAdapter SqlDataAdapter thisAdapter = new SqlDataAdapter ( "SELECT CustomerlD, ContactName FROM Customers", thisConnection); // Создать DataSet для хранения таблиц данных, строк, и столбцов DataSet thisDataSet = new DataSet (); // Заполнить DataSet, используя запрос, определенный ранее для DataAdapter thisAdapter.Fill(thisDataSet, "Customers"); foreach (DataRow theRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine(theRow["CustomerlD"] + "\t" + theRow["ContactName"]); } thisConnection.Close() ; Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Откомпилируйте и выполните программу. Вы должны увидеть список идентификаторов заказчиков и имен компаний, как показано ниже: RICSU Michael Holz ROMEY Alejandra Camino SANTG Jonas Bergulfsen SAVEA Jose Pavarotti SEVES Hari Kumar SIMOB Jytte Petersen SPECD Dominique Perrier SPLIR Art Braunschweiger SUPRD Pascale Cartrain THEBI Liz Nixon THECR Liu Wong TOMSP Karin Josephs TORTU Miguel Angel Paolino TRADH Anabela Domingues TRAIH Helvetius Nagy VAFFE Palle Ibsen VICTE Mary Saveley VINET Paul Henriot WANDK Rita M ь Her WARTH Pirkko Koskitalo WELLI Paula Parente WHITC Karl Jablonski WILMK Matti Karttunen WOLZA Zbyszek Piestrzeniewicz Программа завершена, нажмите Enter для продолжения:
Глава 28. ADO.NET и LINQ поверх DataSet 907 Описание полученных результатов Сначала создается соединение, которое затем используется для создания объекта SqlDataAdapter: SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF';" + @" Integrated Security=Tme; Connect Timeout=30;User Instance=true" ) ; SqlDataAdapter thisAdapter = new SqlDataAdapter ( "SELECT CustomerlD, ContactName FROM Customers", thisConnection); Следующий шаг— создание DataSet, который должен быть наполнен данными: DataSet thisDataSet = new DataSet (); Теперь есть объекты DataSet и адаптера данных (здесь— SqlAdapter, поскольку применяется поставщик SQL Server), и можно приступить к наполнению DataTable в DataSet: thisAdapter.Fill(thisDataSet, "Customers"); Объект DataTable по имени Customers будет создан в DataSet. Обратите внимание, что здесь упоминание слова Customers не ссылается на таблицу Customers в базе данных Northwind. Оно специфицирует имя объекта DataTable в DataSet, который должен быть создан и наполнен данными. Когда DataSet заполнен данными, можно обращаться к индивидуальным строкам и столбцам. Процесс, необходимый для этого, прост— проход циклом по объектам DataRow в коллекции Rows объекта DataTable по имени Customers. Для каждого DataRow извлекаются значения столбцов CustomerlD и ContactName: foreach (DataRow theRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine(theRow["CustomerlD"] + "\t" + theRow["ContactName"]); } Мы уже упоминали ранее, что объект DataRow имеет свойство-индексатор, позволяющее обращаться к его индивидуальным столбцам по имени и по номеру. Таким образом, theRow ["CustomerlD"] специфицирует столбец CustomerlD объекта theRow типа DataRow, a theRow ["ContactName"] специфицирует столбец ContactName объекта DataRow по имени theRow. Альтернативно можно было бы ссылаться на столбцы по номеру— CustomerlD был бы theRow[0] (если это первый столбец, извлеченный из базы данных), a ContactName стал бы theRow [ 1 ]. Однако обычно лучше ссылаться на столбец по имени, поскольку запрос может измениться в будущем, или же столбцы могут изменить порядок. Возможно, вы заметили, что в примере не выполняется явного открытия или закрытия соединения — об этом заботится адаптер. Объект-адаптер данных открывает соединение при необходимости и снова закрывает его по завершении работы. Адаптер данных сохраняет состояние соединения неизменным, поэтому если соединение было открыто до того, как адаптер начал работу, оно останется открытым и после того, как адаптер закончит ее. Итак, теперь вы знаете, как читать данные из базы. Вы использовали DaraReader, который требует поддержки соединения с базой данных на протяжении всей его работы. Вы также применяли адаптер данных для наполнения DataSet — при таком методе адаптер данных имеет дело с соединением, открывая и закрывая его по мере необходимости. DataReader также читает в одном направлении — он может осуществлять
908 Часть ГУ. Доступ к данным навигацию по записям последовательно или перепрыгивать к определенной записи напрямую. Как следует из его имени, DataReader только читает данные, в то время как DataSet обеспечивает невероятную гибкость, как для чтения, так и записи данных, а также работы с данными из разных источников. Вы оцените всю мощь DataSet по мере чтения настоящей главы. Чтение данных — только половина того, что требуется. Обычно необходимо и модифицировать данные, добавлять новые данные или удалять их, так что этим мы и займемся. Обновление базы данных Теперь, когда есть возможность читать данные из базы, возникает вопрос: как их изменять? В этом разделе предлагается очень простой пример, опять использующий только одну таблицу, и в то же время представляющий несколько новых объектов, которые применяются далее в этой главе. Все действия, которые вы обычно хотите выполнять в базе данных (обновление, вставка и удаление записей), могут быть осуществлены по одному и тому же шаблону. 1. Заполнить DataSet информацией из базы данных, с которой вы собираетесь работать. 2. Модифицировать данные, содержащиеся в DataSet (например, обновить, вставить или удалить записи). 3. После проведения всех модификаций сохранить изменения DataSet в базе данных. Вы не раз встретитесь с этой последовательностью, рассматривая примеры, и нет необходимости беспокоиться о точном синтаксисе SQL для обновления базы данных, и все модификации данных в базе могут быть выполнены в одно время. В следующем практическом занятии вы начнете с рассмотрения обновления данных в базе перед тем, как перейти к добавлению и удалению записей. практическое занятие обновление базы данных Предположим, что один из ваших заказчиков, Bottom-Dollar Markets, изменил название на Acme, Inc. Вам нужно изменить наименование компании в базе данных. При этом будет использоваться версия SQL Server/MSDE базы данных Northwind. 1. Создайте новое консольное приложение по имени UpdatingData в каталоге С:\BegVCSharp\Chapter28. 2. Начните с добавления директив using для классов ADO.NET, которые будут использоваться: #region Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server using System.Collections .Generic- using System.Text; #endregion
Глава 28. ADO.NET и LINQ поверх DataSet 909 3. Добавить следующий код в Main (): static void Main(string [ ] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF';" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Создать объект DataAdapter для обновления и прочих операций SqlDataAdapter thisAdapter = new SqlDataAdapter( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); // Создать объект CommandBuilder для построения команд SQL SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter); // Создать DataSet для хранения таблиц данных, чтрок и столбцов DataSet thisDataSet = new DataSet (); // Заполнить DataSet, используя запрос, определенный ранее в DataAdapter thisAdapter.Fill(thisDataSet, "Customers"); // Показать данные перед изменением Console.WriteLine("название до изменения: {0}", thisDataSet.Tables["Customers"].Rows[9]["CompanyName"]); // Изменить данные в таблице Customers, строке 9, столбце CompanyName thisDataSet.Tables["Customers"].Rows[9]["CompanyName"] = "Acme, Inc."; // Вызвать команду Update для фиксации изменений в таблице thisAdapter.Update(thisDataSet, "Customers"); Console.WriteLine("название после изменения: {0}", thisDataSet.Tables["Customers"].Rows[9]["CompanyName"]); thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine() ; } 4. Запуск программы сгенерирует вывод, показанный здесь: название до изменения: Bottom-Dollar Markets название после изменения: Acme, Inc. Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Первая часть программы подобна предыдущему примеру SQL Server; здесь создается объект соединения с использованием строки соединения: SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\NORTHWND.MDF';" + (^"Integrated Security=True;Connect Timeout=30;User Instance=true" ); Затем создается объект SqlAdapter с помощью следующего оператора: SqlDataAdapter thisAdapter = new SqlDataAdapter( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); Далее необходимо создать корректные операторы SQL для обновления базы данных. Это не нужно делать самостоятельно — о них позаботится SqlCommandBuilder: SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter);
910 Часть IV. Доступ к данным Обратите внимание, что thisAdapter передается конструктору SqlCommandBuilder в качестве аргумента. Корректные команды SQL генерируются и ассоциируются конструктором с переданным адаптером данных при создании объекта SqlCommandBuilder. Чуть позже в главе вы увидите разные операторы SQL, а пока программа позаботится о правильном SQL сама. Теперь вы создадите демонстрационный объект Data Set и заполните его данными: DataSet thisDataSet = new DataSetO; thisAdapter.Fill(thisDataSet, "Customers"); В данном случае нужна таблица Customers, поэтому вы вызываете ассоциированный объект DataTable в DataSet с тем же именем. В заполненном DataSet можно обращаться к индивидуальным строкам и столбцам. Перед изменением данных выводится их снимок в том виде, в котором они были до изменения: Console.WriteLine("название до изменения: {0}", thisDataSet.Tables["Customers"].Rows[9]["CompanyName"]); Здесь печатается значение столбца CompanyName в строке с индексом номер 9 в таблице Customers. Полная строка вывода выглядит так: название до изменения: Bottom-Dollar Markets Здесь присутствует одна небольшая хитрость, поскольку известно, что интересует строка с индексом номер 9 (который на самом деле представляет десятую строку, потому что индексация начинается с нуля, и первая строка — это Rows [ 0 ]). В реальной программе, в отличие от примера, вероятно, понадобится поместить квалификатор в запрос SQL для выбора интересующих строк вместо обращения к строке с номером 9. Следующий пример демонстрирует, как найти только те строки, которые вас интересуют. Другой способ понять, что здесь происходит, состоит в рассмотрении эквивалентного примера, который разбивает каждый отдельный объект в выражении: // Пример использования множества объектов DataTable customerTable = thisDataSet.Tables["Customers"]; DataRow rowTen = customerTable.Rows[9]; object CompanyName = rowTen["CompanyName"]; Console.WriteLine("название до изменения: {0}", CompanyName); В этом примере customerTable объявлена как DataTable, a Customers присвоена таблица из свойства Tables объекта thisDataSet. Затем rowTen объявлена как DataRow и ей присвоен десятый элемент свойства Rows объекта customerTable. И, наконец, CompanyName объявлена как object, после чего свойство indexer объекта rowTen используется для присваивания ему поля CompanyName. Этот пример помогает понять процесс, проследив цепочку связанных объектов, но часто бывает проще применить однострочное выражение, которое дает тот же результат: Console.WriteLine("название до изменения: {0}", thisDataSet.Tables["Customers"].Rows[9]["CompanyName"]); Если код, использующий несколько объектов, более понятен, без сомнений используйте этот метод. Для однострочной ссылки вроде этой может быть неэффективно создавать переменные для каждого объекта и присваивать их каждый раз; однако если объект должен быть повторно использован, то много-объектный метод может оказаться более эффективным. Оптимизатор компилятора может компенсировать любую неэффективность, связанную с кодированием, так что обычно лучше просто применять код, более читабельный для вас.
Глава 28. ADO.NET и LINQ поверх DataSet 911 Вернемся к примеру. Вы отобразили значение столбца перед внесением изменения, так что теперь давайте внесем это изменение в столбец. Чтобы изменить значение DataColumn, просто присвойте ему новое значение, как показано в следующей строке примера: thisDataSet.Tables["Customers"].Rows[9]["CompanyName"] = "Acme, Inc."; Эта строка изменяет значение столбца CompanyName в строке с номером индекса 9 таблицы Customers на "Acme, Inc. ". Это изменение касается только значения столбца DataSet в памяти, а не в самой базе данных. Объекты DataSet, DataTable, DataRow и DataColumn являются представлениями данных таблицы, находящимися в памяти. Чтобы обновить базу данных, следует вызвать метод Update (). Update () — это метод объекта адаптера данных. Для его вызова укажите DataSet, на основе которого должно произойти обновление, и имя DataTable в DataSet для обновления. Важно чтобы имя DataTable ("Customers") соответствовало тому, что использовалось в предшествующем вызове метода Fill (): thisAdapter.Update(thisDataSet, "Customers"); Метод Update () автоматически проходит по строкам DataTable, проверяя необходимые изменения, которые нужно провести в базе данных. Каждый объект DataRow в коллекции Rows имеет свойство RowState, которое отслеживает, когда строка была удалена, добавлена, модифицирована или осталась неизменной. Любые проведенные в ней изменения затем отражаются в базе данных. Теперь вы подтверждаете изменение, печатая состояние данных после его проведения: Console.WriteLine("название после изменения: {0}", thisDataSet.Tables["Customers"].Rows[9]["CompanyName"]); Вот и все! Прежде чем двинуться дальше, подведем краткий итог новым понятиям, которые встречались здесь. □ SqlCommandBuilder. Объект SqlCommandBuilder заботится о построении корректных операторов SQL для обновления базы данных — их не нужно строить самостоятельно. □ SqlDataAdapter. Update (). Этот метод проходит по строкам в DataTable, проверяя изменения, которые должны быть проведены в базе данных. Каждый объект DataRow в коллекции Rows имеет свойство RowState, отслеживающее, когда строка удалена, добавлена, модифицирована или оставлена без изменений. Любые проведенные изменения отражаются в базе данных. Все это, конечно, версии поставщика источника данных SQL Server, однако существуют соответствующие версии поставщика OLE DB, которые работают аналогичным образом. Добавление строк в базу данных В предыдущем примере вы обновляли значения в существующих строках; следующий шаг — добавление совершенно новой строки. Процедура, которую вы должны выполнить для добавления новой строки в базу данных, включает, как и пример с обновлением, добавление новой строки к существующему DataSet (и здесь сосредоточена
912 Часть ГУ. Доступ* к данным большая часть работы), с последующим сохранением этого изменения обратно в базу данных. 1. Создать новый объект DataRow. 2. Заполнить его некоторыми данными. 3. Добавить его в коллекцию Rows объекта Data Set. 4. Сохранить изменения в базе данных вызовом метода Update () адаптера данных. Схема выглядит вполне разумно. В следующем практическом занятии вы увидите, как именно это делается. практическое занятие Добавление строк Выполните следующие шаги для создания примера AddingData в Visual C# 2008. 1. Создайте новое консольное приложение по имени AddingData в каталоге С:\BegVCSharp\Chapter28. 2. Начните с добавления директив using для классов ADO.NET, которые будут использованы: #region Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server using System.Collections .Generic- using System.Text; #endregion 3. Добавьте следующий код к методу Main (): static void Main(string[ ] args) { // Указать специфичную для SQL Server строку соединения SglConnection thisConnection = new SglConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\NORTHWND.MDF'; " + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Создать объект DataAdapter для обновления и прочих операций SglDataAdapter thisAdapter = new SqlDataAdapter( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); // Создать объект CommandBuilder для построения команд SQL SqlCommandBuilder thisBuilder = new SglCommandBuilder(thisAdapter); // Создать DataSet для хранения таблиц данных, строк и столбцов DataSet thisDataSet = new DataSet (); // Заполнить DataSet, используя запрос, определенный ранее для DataAdapter thisAdapter.Fill(thisDataSet, "Customers"); Console.WriteLine("количество строк до изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count); DataRow thisRow = thisDataSet.Tables["Customers"].NewRow(); thisRow["CustomerID"] = "ZACZI"; thisRow["CompanyName"] = "Zachary Zithers Ltd."; thisDataSet.Tables["Customers"].Rows.Add(thisRow);
Глава 28. ADO.NET и LINQ поверх DataSet 913 Console.WriteLine("количество строк после изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count); // Вызвать команду Update для фиксации изменений в таблице thisAdapter.Update(thisDataSet, "Customers"); thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. При выполнении этот пример выдаст следующее: количество строк до изменения: 91 количество строк после изменения: 92 Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Интересующие нас строки находятся между вызовами методов thisAdapter. Fill () и thisAdapter.Update(). Сначала, чтобы увидеть картину до изменений, вы вводите в программу новое свойство коллекции Rows — Count. Оно дает количество строк в таблице: Console.WriteLine("количество строк до изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count); Затем создается новый объект строки, используя метод NewRow () объекта DataSet: DataRow thisRow = thisDataSet.Tables["Customers"].NewRow(); Обратите внимание, что это создает новый объект строки, использующий те же столбцы, что и в таблице Customers, но не добавляет их в DataSet; потребуется добавить некоторые значения столбцов, прежде чем это может быть сделано: thisRow["CustomerID"] = "ZACZI"; thisRow["CompanyName"] = "Zachary Zithers Ltd."; После этого вы можете в действительности добавить строку, используя метод Add () коллекции Rows: thisDataSet.Tables["Customers"].Rows.Add(thisRow); Если вы снова проверите значение свойства Count после вызова Add (), то увидите, что оно увеличилось на единицу: Console.WriteLine("количество строк после изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count); Вывод показывает 92 строки — на одну больше, чем перед проведением изменения. Как и в предыдущем примере, необходим вызов Update () для того, чтобы действительно добавить строку в базу данных на диске: thisAdapter.Update (thisDataSet, "Customers"); Напомним, что DataSet представляет собой находящуюся в памяти автономную копию данных; к базе данных на диске подключен объект DataAdapter. Поэтому должен быть вызван метод Update (), который синхронизирует данные в памяти из DataSet с данными в базе на диске. Заполнены только столбцы Customer ID и Company Name, поскольку это все, что использовалось в программе. Остальные столбцы остаются пустыми (на самом деле они содержат значение NULL в терминах SQL). Вы можете предположить, что заполнение, скажем, Contact Name — это просто вопрос добавления следующей строки кода: thisRow["ContactName"] = "Zylla Zithers";
914 Часть IV. Доступ к данным Однако это не единственный шаг, который вы должны выполнить. Напомним, что когда выполняется исходный запрос, строится Data Set, состоящий только из двух столбцов — CustomerID и CompanyName: SqlDataAdapter thisAdapter = new SqlDataAdapter( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); Ссылка на ContactName вызовет ошибку, поскольку в построенном DataSet нет такого столбца. Вы могли бы поправить это, добавив столбец ContactName в исходный запрос SQL: SqlDataAdapter thisAdapter = new SqlDataAdapter( "SELECT CustomerlD, ContactName, CompanyName FROM Customers", thisConnection); Альтернативно можно было бы выбрать все столбцы из Customers, используя следующую команду: SqlDataAdapter thisAdapter = new SqlDataAdapter("SELECT * FROM Customers", thisConnection); Здесь звездочка (*) в команде SQL SELECT является сокращением для всех столбцов таблицы; с этим изменением вы сможете добавить значения любых столбцов таблицы в базу данных. Однако получение всех столбцов при работе только с двумя или тремя неэффективно, и обычно вы должны его избегать. Поиск строк Если вы попытаетесь запустить предшествующий пример еще раз, то увидите сообщение, подобное показанному на рис. 28.4. Violation of PRIMARY KEY constraint «.Customers'. Cera* insert duptcate key hi object 'dbo.Customers'. The statement has been terminated. Troubleshooting dp* KB general help for this ESSE! Search for more Hep On*»... Actions: ViewDetal... Copy exception detal to the dpboard Рис. 28.4. Сообщение о нарушении уникальности первичного ключа Это говорит о том, что вызов Add () завершился неудачей, поскольку предпринята попытка добавить дублированный первичный ключ. Определение таблицы Customers требует, чтобы поле CustomerlD содержало уникальные значения, что необходимо, когда столбец служит первичным ключом. Значение "ZACZI" уже присутствовало в базе, когда вы попытались запустить код второй раз, потому что было помещено в таблицу при первом его запуске. Давайте изменим логику примера таким образом, чтобы он сначала искал строку, прежде чем пытаться ее добавить. Коллекция Rows типа DataTable представляет метод по имени Find (), который очень удобен для этой цели. В следующем практическом занятии вы перепишете логику, окружающую добавление строки, с использованием Fill () вместо подсчета строк. 1
Глава 28. ADO.NET и LINQ поверх DataSet 915 Практическое занятие Нахождение СТрОК Выполните следующие шаги для создания примера FindingData в Visual C# 2008. 1. Создайте новое консольное приложение по имени FindingData в каталоге С:\BegVCSharp\Chapter28. 2. Начните с добавления обычных директив using для классов ADO.NET, которые вы собираетесь использовать: ^region Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server using System.Collections.Generic; using System.Text; #endregion 3. Добавьте следующий код в метод Main (): static void Main(string [ ] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename=fC:\SQL Server 2000 Sample Databases\N0RTHWND.MDF';" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Создать объект DataAdapter для обновления и прочих операций SqlDataAdapter thisAdapter = new SqlDataAdapter( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); // Создать объект CommandBuilder для построения команд SQL SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter); // Создать DataSet для хранения таблиц данных, строк и столбцов DataSet thisDataSet = new DataSet(); // Заполнить DataSet, используя запрос, определенный ранее для DataAdapter thisAdapter.Fill(thisDataSet, "Customers"); Console.WriteLine("количество строк до изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count); // Установить ключевой объект для определения первичного ключа DataColumn[] keys = new DataColumn[1]; keys[0] = thisDataSet.Tables["Customers"].Columns["CustomerlD"]; thisDataSet.Tables["Customers"].PrimaryKey = keys; DataRow findRow = thisDataSet.Tables["Customers"].Rows.Find("ZACZI"); if (findRow == null) { Console.WriteLine("ZACZI не найдена и будет добавлена в таблицу Customers"); DataRow thisRow = thisDataSet.Tables["Customers"].NewRow(); thisRow["CustomerlD"] = "ZACZI"; thisRow["CompanyName"] = "Zachary Zithers Ltd."; thisDataSet.Tables["Customers"].Rows.Add(thisRow); if ((findRow = thisDataSet.Tables["Customers"].Rows.Find("Z&CZI")) != null) {
916 Часть ГУ. Доступ к данным Console.WriteLine("ZACZI успешно добавлена в таблицу Customers"); } } else { Console.WriteLine("ZACZI уже существует в базе данных"); } thisAdapter.Update(thisDataSet, "Customers"); Console.WriteLine("количество строк после изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count); thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } Описание полученных результатов В начале программы вызывается метод Fill (), как и в предыдущих примерах. Свойство Count применяется для вывода количество существующих строк, а затем с помощью Find () проверяется, не существует ли уже строка, которую планируется добавить. Прежде чем можно будет использовать Find (), необходимо установить первичный ключ. Первичный ключ — это то, что применяется при поиске; он может состоять из одного или более столбцов таблицы и содержит значение или набор значений, уникально идентифицирующих определенную строку таблицы, так что когда вы ищете по ключу, то находите одну и только одну строку. Таблица Customers в базе данных Northwind использует столбец Customer ID в качестве первичного ключа: DataColumn[] keys = new DataColumn[1]; keys[0] = thisDataSet.Tables["Customers"].Columns["CustomerlD"]; thisDataSet.Tables["Customers"].PrimaryKey = keys; Сначала создается массив DataColumn. Поскольку ключ может состоять из одного или более столбцов (это известно как составной ключ), массив является естественной структурой для использования здесь; массив называется keys и хранит значения DataColumn. Затем первому элементу массива, keys [0], присваивается столбец CustomerlD таблицы Customers. И, наконец, keys присваиваете свойству PrimaryKey объекта DataTable по имени Customers. Альтернативно можно загрузить информацию первичного ключа непосредственно из базы данных, что по умолчанию не делается. Можно явно сообщить ADO.NET о необходимости загрузки информации первичного ключа, установив свойство MissingSchemaAction из DataAdapter перед заполнением DataSet, как показано ниже: thisAdapter.MissingSchemaAction = MissingSchemaAction.AddWithKey; thisAdapter.Fill(thisDataSet, "Customers"); Это выполняет ту же установку первичного ключа посредством инициализации свойства PrimaryKey объекта DataTable неявным образом. В любом случае теперь вы готовы найти строку: DataRow findRow = thisDataSet.Tables["Customers"].Rows.Find("ZACZI") ; Find () возвращает DataRow, поэтому вы устанавливаете объект DataRow по имени findRow для получения результата. Find() принимает параметр, которым является искомое значение; это может быть массив объектов для составного первичного ключа, но в данном случае первичный ключ состоит только из одного столбца, поэтому нужно передать только одно значение, которое вы и передаете в виде строки, содержащей значение ZACZI — это CustomerlD, который необходимо найти.
Глава 28. ADO.NET и LINQ поверх DataSet 917 Если Find () находит соответствующую строку, то возвращает объект DataRow, соответствующий условию поиска; если же не находит, то возвращает null-ссылку, и это можно проверить: if (findRow == null) { Console.WriteLine("ZACZI не найдена и будет добавлена в таблицу Customers"); DataRow thisRow = thisDataSet.Tables["Customers"].NewRow(); thisRow["CustomerID"] = "ZACZI"; thisRow["CompanyName"] = "Zachary Zithers Ltd."; thisDataSet.Tables["Customers"].Rows.Add(thisRow); if ((findRow = thisDataSet.Tables["Customers"].Rows.Find("ZACZI")) != null) { Console.WriteLine("ZACZI успешно добавлена в таблицу Customers"); else { Console.WriteLine("ZACZI уже существует в базе данных"); } Если findRow равно null, то вы идете дальше и добавляете новую строку, как в предыдущем примере. Просто для того, чтобы убедиться, что метод Add () выполнился успешно, вы снова вызываете Find () немедленно после операции добавления, чтобы доказать, что она сработала. Как упоминалось в начале раздела, эта версия использует Find () повторно; этот пример можно запускать многократно без каких-либо ошибок. Однако он уже не выполняет код Add () после того, как строка "ZACZI" окажется в базе данных. Давайте посмотрим, как все-таки заставить повториться эту часть программы. Удаление строк После того, как вы научились добавлять строки к DataSet и базе данных, логично изучить противоположное действие — удаление строк. Объект DataRow имеет метод Delete (), удаляющий текущую строку. В следующем практическом занятии изменяется смысл оператора if на findRow, проверяя неравенство findRow значению null (другими словами, наличие искомой строки). Затем вызовом Delete () на findRow эта строка удаляется. Практическое занятие Удаление строк Выполните следующие шаги для создания примера DeleteData в Visual C# 2008. 1. Создайте новое консольное приложение по имени DeleteData в каталоге С: \ BegVCSharp\Chapter28. 2. Как обычно, начните с добавления директив using для используемых классов ADO.NET: #region Using Directives using System; using System.Data; // Использовать пространство имен ADO.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server using System.Collections .Generic- using System.Text; #endregion
918 Часть IV. Доступ к данным 3. Добавьте следующий код в метод Main (): static void Main(string [] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF'; " + 0"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Создать объект DataAdapter для обновления и прочих операций SqlDataAdapter thisAdapter = new SqlDataAdapter( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); // Создать объект CommandBuilder для построения команд SQL SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter) ; // Создать DataSet для хранения таблиц данных, строк и столбцов DataSet thisDataSet = new DataSet (); // Заполнить DataSet, используя запрос, определенный ранее для DataAdapter thisAdapter.Fill(thisDataSet, "Customers"); Console.WriteLine("количество строк до изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count); // Установить ключевой объект для определения первичного ключа DataColumn[] keys = new DataColumn[1]; keys[0] = thisDataSet.Tables["Customers"].Columns["CustomerlD"] ; thisDataSet.Tables["Customers"].PrimaryKey = keys; DataRow findRow = thisDataSet.Tables["Customers"].Rows.Find("ZACZI"); if (findRow != null) { Console.WriteLine ("ZACZI уже присутствует в таблице Customers"); Console.WriteLine("Удаление ZACZI . . ."); findRow.Delete (); thisAdapter.Update(thisDataSet, "Customers"); } Console.WriteLine("количество строк после изменения: {0}", thisDataSet.Tables["Customers"].Rows.Count) ; thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine() ; } Описание полученных результатов Код создания DataSet и объект адаптера данных стандартны — вы их уже видели ранее в этой главе несколько раз, так что не станем повторяться. Различие между этим кодом и кодом предыдущего примера состоит в том, что если строка найдена, она удаляется! Обратите внимание, что при вызове Delete () строка из базы данных не удаляется до тех пор, пока не будет вызван метод Update () для фиксации изменений. Метод Delete () не удаляет строку — он лишь помечает ее для последующего удаления. Каждый объект DataRow в коллекции Rows имеет свойство RowState, которое отслеживает, когда строка удалена, добавлена, модифицирована или осталась неизменной. Метод Delete () устанавливает RowState строки в Deleted, и Update () затем удаляет из базы данных все строки, которые обнаружит в коллекции Rows с пометкой Deleted.
Глава 28. ADO.NET и LINQ поверх DataSet 919 То же касается метода Remove (); вызывайте его для удаления строк из коллекции Rows объекта DataSet, но не из самой базы данных. Доступ к нескольким таблицам в DataSet Одним из значительных преимуществ модели ADO.NET перед прежними моделями доступа к данным является то, что объект DataSet внутри себя отслеживает множество таблиц и отношений между ними. Это значит, что можно передавать целые наборы связанных данных между частями программы в одной операции, и архитектура самостоятельно поддерживает целостность отношений между данными. Отношения в ADO.NET Объект DataRelation используется для описания отношений между множеством объектов DataTable в DataSet. Каждый DataSet имеет коллекцию Relations объектов DataRelation, которая позволяет находить и манипулировать связанными данными. Давайте начнем с таблиц Customers и Orders. Каждый заказчик может иметь несколько заказов; как же увидеть заказы, размещенные каждым заказчиком? Каждая строка таблицы Orders содержит Customer ID заказчика, разместившего его; вы сопоставляете все строки заказов, имеющие такое же значение CustomerlD, как у заказчика из таблицы Customers. Соответствующие поля CustomerlD в двух таблицах определяют отношение "один ко многим" между таблицами Customers и Orders. Вы можете использовать это отношение в ADO.NET, создав объект DataRelation для его представления. Создание объекта DataRelation DataSet имеет свойство Relations, которое является коллекцией всех объектов DataRelation, представляющих отношения между таблицами в DataSet. Чтобы создать новый DataRelation, используйте метод Add () объекта Relations, который принимает строковое имя отношения и два объекта DataColumn — родительский столбец, за которым следует дочерний столбец. Таким образом, чтобы создать описанное ранее отношение между столбцом CustomerlD таблицы Customers и столбцом CustomerlD таблицы Orders, необходимо использовать следующий синтаксис, присвоив отношению имя CustOrders: DataRelation custOrderRel = thisDataSet.Relations.Add("CustOrders", thisDataSet.Tables["Customers"].Columns["CustomerlD"], thisDataSet.Tables["Orders"].Columns["CustomerlD"]); Вы увидите этот синтаксис снова в следующем примере. Навигация по отношениям Чтобы использовать отношение, нужно перейти от строки в одной таблице к связанным с ней строкам в другой таблице. В базе данных Northwind строка таблицы Customers может считаться родительской, а каждая из связанных строк таблицы Orders — дочерней. Навигация также возможна и в обратном направлении. Извлечение дочерних строк Имея строку родительской таблицы, как можно получить все строки дочерней таблицы, связанные с нею? Вы можете извлечь набор дочерних строк методом GetChildRows () объекта DataRow. Объект DataRelation, который вы создали между родительской и дочерней таблицами, передается методу, а возвращается объект
920 Часть ГУ. Доступ к данным DataRowCollection, который представляет коллекцию связанных объектов DataRow в дочерней DataTable. Например, имея ранее созданный объект DataRelation, если заданная строка DataRow в родительской таблице DataTable (Customers) представлена объектом customerRow, то customerRow.GetChildRows(custOrderRel) ; возвращает коллекцию связанных объектов DataRow из таблицы Orders. Вы увидите, как обрабатывать этот набор объектов в следующем практическом занятии. П, рактическое занятие Получение СВЯЗЭННЫХ СТрОК Выполните следующие шаги для создания примера DataRelationExample в Visual С# 2008. 1. Создайте новое консольное приложение по имени DeleteData в каталоге С:\BegVCSharp\Chapter28. 2. Начните с добавления директив using для классов ADO.NET, которые вы будете использовать: #region Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server using System.Collections.Generic; using System.Text; #endregion 3. Добавьте следующий код в метод Main (): static void Main(string[] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\NORTHWND.MDF';" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Создать объект DataAdapter для обновления и прочих операций SqlDataAdapter thisAdapter = new SqlDataAdapter ( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); // Создать объект CommandBuilder для построения команд SQL SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter) ; // Создать DataSet для хранения данных связанных таблиц, строк и столбцов DataSet thisDataSet = new DataSet (); // Установить объекты DataAdapter для каждой таблицы и заполнить их SqlDataAdapter custAdapter = new SqlDataAdapter( "SELECT * FROM Customers", thisConnection); SqlDataAdapter orderAdapter = new SqlDataAdapter( "SELECT * FROM Orders", thisConnection); custAdapter.Fill(thisDataSet, "Customers"); orderAdapter.Fill(thisDataSet, "Orders");
Глава 28. ADO.NET и LINQ поверх DataSet 921 // Установить связь DataRelation между заказчиками и заказами DataRelation custOrderRel = thisDataSet.Relations.Add("CustOrders", thisDataSet.Tables["Customers"].Columns["CustomerlD"], thisDataSet.Tables["Orders"].Columns["CustomerlD"]); // Распечатать заказчиков и идентификаторы их заказов foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine("ID заказчика: " + custRow["CustomerlD"] + " Название: " + custRow["CompanyName"]); foreach (DataRow orderRow in custRow.GetChildRows(custOrderRel)) { Console.WriteLine(" ID заказа: " + orderRow["OrderlD"]); } } thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Выполните приложение. Ниже показан фрагмент вывода: ID заказа: 10696 ID заказа: 10723 ID заказа: 10740 ID заказа: 10861 ID заказа: 10904 ID заказа: 11032 ID заказа: 11066 ID заказчика: WILMK Название: Wilman Kala ID заказа: 10615 ID заказа: 10673 ID заказа: 10695 ID заказа: 10873 ID заказа: 10879 ID заказа: 10910 ID заказа: 11005 ID заказчика: WOLZA Название: Wolski Zajazd ID заказа: 10374 ID заказа: 10611 ID заказа: 10792 ID заказа: 10870 ID заказа: 10906 ID заказа: 10998 ID заказа: 11044 ID заказчика: ZACZI Название: Zachary Zithers Ltd. Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Прежде чем сконструировать DataRelation, вы должны создать объект DataSet и связать таблицы базы данных, которые собираетесь использовать с ним, как показано ниже: DataSet thisDataSet = new DataSet (); SqlDataAdapter custAdapter = new SqlDataAdapter ( "SELECT * FROM Customers", thisConnection); SqlDataAdapter orderAdapter = new SqlDataAdapter ( "SELECT * FROM Orders", thisConnection); custAdapter.Fill(thisDataSet, "Customers"); orderAdapter.Fill(thisDataSet, "Orders");
922 Часть IV. Доступ к данным Для каждой таблицы, на которую осуществляется ссылка, создается объект DataAdapter. Затем DataSet заполняется данными из необходимых столбцов; в рассматриваемом случае беспокоиться об эффективности не нужно, поэтому просто берутся все доступные столбцы (SELECT * FROM <table>). Затем создается объект DataRelation, который привязывается к DataSet: DataRelation custOrderRel = thisDataSet.Relations.Add("CustOrders", thisDataSet.Tables["Customers"].Columns["CustomerlD"] , thisDataSet.Tables["Orders"].Columns["CustomerlD"]); Теперь вы готовы искать заказчиков и их заказы. Установите цикл f oreach для отображения информации о каждом заказчике: foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine("ID заказчика: " + custRow["CustomerlD"] + " Название: " + custRow["CompanyName"]); Здесь осуществляется просто проход циклом по коллекции Rows таблицы Customers, печатая CustomerlD и CompanyName для каждого заказчика. После отображения заказчика необходимо показать связанные с ним заказы. Для этого добавляется вложенный цикл foreach, инициализированный вызовом метода GetChildRows () объекта DataRow. Объект DataRelation передается методу GetChildRows (), который возвращает DataRowCollection, содержащую только связанные строки таблицы Orders для заданного заказчика. Чтобы отобразить эти связанные строки, просто выполняется цикл по каждой DataRow в коллекции вложенным оператором foreach: foreach (DataRow orderRow in custRow.GetChildRows(custOrderRel) ) { Console.WriteLine(" ID заказа: " + orderRow["OrderlD"]); } } Этот процесс повторяется для каждого заказчика. При отображении OrderlD добавляется ведущий пробел, чтобы все заказы каждого заказчика отображались со сдвигом по отношению к информации о заказчике. Благодаря этому сдвигу можно наглядно видеть отношение "родительский-дочерний" между каждым заказчиком и его заказами. Заказчик "Zachary Zithers Ltd. " не имеет заказов, поскольку в предыдущем примере вы добавили только его, но не его заказы. Это одно отношение между двумя таблицами, а теперь рассмотрим отношения между большим количеством таблиц, расширив программу, чтобы увидеть, какие специфические элементы каждый заказчик включил в каждый заказ и какие наименования продуктов. Эта информация доступна через другие таблицы базы данных Northwind. Давайте рассмотрим эти отношения, взглянув на диаграмму базы данных Northwind, показанную на рис. 28.5. Линии между таблицами представляют отношения, причем конец каждой линии указывает на столбец, идентифицирующий отношение. Отношение "первичный ключ — внешний ключ" показано с символом ключа у родительского столбца и символом бесконечности у дочернего столбца. В следующем практическом занятии будут отображены подробности каждого заказа, включая имена продуктов, с помощью отношений между четырьмя таблицами, показанными на рис. 28.5: Customers, Orders, Order Details и Products.
Глава 28. ADO.NET и LINQ поверх DataSet 923 si SuppherlD CompanyName ContactName Contact TWe Addre« C»y Regan Postcode Country Phone Fax HomePage CategorylD CategoryName [I I Descrpton Picture ProductID Pr oductName *erID Category ID QuanbtyPerUr* UntPrice UnrtsInStock UrttsOnOder Reordertevel Deconhnued Employee Terr I tones ||_£l Employee ID ]~Щ ТеггЛогуГО 1 j ?|Territory ID TerritoryOescnption RegtonID 1 Fmptoyees s - з = EmpioyeelD LastName FrstName Ше TltteOfCourtesy BrthOate HreDate Address aty Regen PostaKode Country i z\ l|"y|RegoriID j j RegcnOescrtptcn И. CustomerlD CompanyName ContactName ContactTMe Address I aty Region PostaJCode Country Рис. 28.5. Диаграмма базы данных Northwind практическое занял* Работа с множественными отношениями Выполните следующие шаги для создания примера Man у Relations в Visual C# 2008. 1. Создайте новое консольное приложение по имени ManyRelations в каталоге C:\BegVCSharp\Chapter28. 2. Добавьте директивы using для классов ADO.NET, которые вы будете использовать: // Использовать пространство имен ADO.NET // Использовать пространство имен // поставщика данных SQL Server #region Using Directives using System; using System.Data; using System. Data. SqlClient ; using System.Collections.Generic; using System.Text; #endregion 3. Добавьте следующий код в метод Main (): static void Main(string[] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename=lC:\SQL Server 2000 Sample Databases\ N0RTHWND.MDF' @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); DataSet thisDataSet = new DataSet();
924 Часть ГУ. Доступ к данным SqlDataAdapter custAdapter = new SqlDataAdapter( "SELECT * FROM Customers", thisConnection); custAdapter.Fill(thisDataSet, "Customers"); SqlDataAdapter orderAdapter = new SqlDataAdapter( "SELECT * FROM Orders", thisConnection); orderAdapter.Fill(thisDataSet, "Orders"); SqlDataAdapter detailAdapter = new SqlDataAdapter ( "SELECT * FROM [Order Details]", thisConnection); detailAdapter.Fill(thisDataSet, "Order Details"); SqlDataAdapter prodAdapter = new SqlDataAdapter( "SELECT * FROM Products", thisConnection); prodAdapter.Fill(thisDataSet, "Products"); DataRelation custOrderRel = thisDataSet.Relations.Add("CustOrders", thisDataSet.Tables["Customers"].Columns["CustomerlD"] , thisDataSet.Tables["Orders"].Columns["CustomerlD"]); DataRelation orderDetailRel = thisDataSet.Relations.Add("OrderDetail", thisDataSet.Tables["Orders"].Columns["OrderlD"] , thisDataSet.Tables["Order Details"].Columns["OrderlD"]); DataRelation orderProductRel = thisDataSet.Relations.Add( "OrderProducts",thisDataSet.Tables["Products"].Columns["ProductID"], thisDataSet.Tables["Order Details"].Columns["ProductID"]); foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine("ID заказчика: " + custRow["CustomerlD"]); foreach (DataRow orderRow in custRow.GetChildRows(custOrderRel)) { Console.WriteLine("\tID заказа: " + orderRow["OrderlD"]); Console.WriteLine("\t\tflaTa заказа: " + orderRow["OrderDate"] ) ; foreach (DataRow detailRow in orderRow.GetChildRows(orderDetailRel)) { Console.WriteLine("\t^Продукт: " + detailRow.GetParentRow(orderProductRel)["ProductName"]); Console.WriteLine ("Х^Количество: " + detailRow ["Quantity" ]) ; } } } thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 4. Выполните приложение. Вы увидите вывод, подобный приведенному ниже (здесь показана сокращенная версия, включающая только завершающую часть вывода): ID заказчика: WOLZA ID заказа: 10998 Дата заказа: 4/3/1998 12:00:00 AM Продукт: Guarana Fantastica Количество: 12 Продукт: Sirop d'erable Количество: 7 Продукт: Longlife Tofu
Глава 28. ADQ.NET и LINQ поверх DataSet 925 Количество: 20 Продукт: Rhbnbrau Klosterbier Количество: 30 ID заказа: 11044 Дата заказа: 4/23/1998 12:00:00 AM Продукт: Tarte au sucre Количество: 12 ID заказчика: ZACZI Описание полученных результатов Как обычно, все начинается с инициализации соединения и создания нового DataSet. Затем создается адаптер данных для каждой из четырех таблиц, которые будут использованы: SqlDataAdapter custAdapter = new SqlDataAdapter ( "SELECT * FROM Customers", thisConnection); custAdapter.Fill(thisDataSet, "Customers"); SqlDataAdapter orderAdapter = new SqlDataAdapter( "SELECT * FROM Orders", thisConnection); orderAdapter.Fill(thisDataSet, "Orders"); SqlDataAdapter detailAdapter = new SqlDataAdapter ( "SELECT * FROM [Order Details]", thisConnection); detailAdapter.Fill(thisDataSet, "Order Details"); SqlDataAdapter prodAdapter = new SqlDataAdapter( "SELECT * FROM Products", thisConnection); prodAdapter.Fill(thisDataSet, "Products"); Далее строятся объекты DataRelation для каждого отношения между таблицами: DataRelation custOrderRel = thisDataSet.Relations.Add("CustOrders", thisDataSet.Tables["Customers"].Columns["CustomerlD"], thisDataSet.Tables["Orders"].Columns["CustomerlD"]); DataRelation orderDetailRel = thisDataSet.Relations.Add("OrderDetail", thisDataSet.Tables["Orders"].Columns["OrderlD"], thisDataSet.Tables["Order Details"].Columns["OrderlD"]); DataRelation orderProductRel = thisDataSet.Relations.Add( "OrderProducts",thisDataSet.Tables["Products"].Columns["ProductID"], thisDataSet.Tables["Order Details"].Columns["ProductID"]); Первое отношение — точно такое, как в предыдущем примере. Следующее отношение строится между Orders и Order Details с использованием OrderlD в качестве столбца связи. Последнее отношение— между Order Details и Products с применением ProductID в качестве столбца связи. Обратите внимание, что в этом отношении Products является родительской таблицей (второй из трех параметров). Это потому, что он представляет сторону "один" в отношении "один ко многим" (один продукт может входить во множество заказов). Установив отношения, можно приступить к работе с ними. Опять-таки, базовая структура— вложенный цикл foreach, но на этот раз с тремя уровнями вложенности: foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine("ID заказчика: " + custRow["CustomerlD"]); foreach (DataRow orderRow in custRow.GetChildRows(custOrderRel))
926 Часть IV. Доступ к данным Console.WriteLine("\tID заказа: " + orderRow["OrderlD"]); Console.WriteLine("\t\tflaTa заказа: " + orderRow["OrderDate"]); foreach (DataRow detailRow in orderRow.GetChildRows(orderDetailRel)) { Console.WriteLine("\t^Продукт: " + detailRow.GetParentRow(orderProductRel)["ProductName"]); Console.WriteLine(м\t\tKoличecтвo: " + detailRow["Quantity"] ) ; } } } Как и ранее, выводятся данные родительской строки, а затем с помощью GetChildRows () получаются дочерние строки, связанные с родительской. Внешний цикл такой же, как и в предыдущем примере. Вы печатаете дополнительные детали OrderDate в OrderlD, а затем получаете OrderDetails для этого OrderlD. Наиболее глубоко вложенный цикл отличается; чтобы получить строку Product, вызывается GetParentRow (), который получает родительский объект, двигаясь по отношению от стороны "многие" к стороне "один". Иногда эта навигация от дочернего к родительскому объекту называется навигацией вверх, в противоположность нормальной навигации вниз — от родителя к ребенку. Навигация вверх требует вызова GetParentRow(). Вывод этой программы показывает детали заказов, принятых от каждого заказчика, со смещением, чтобы показать иерархию родителей и детей. Опять-таки, заказчик "ZACZI" не имеет заказов, поскольку он был только что добавлен в предыдущих примерах. XML и ADO.NET Как было отмечено во введении, поддержка XML — одна из основных проектных целей ADO.NET, и она также является центральной во внутренней реализации ADO. NET. Поэтому имело смысл, чтобы ADO.NET обеспечивал поддержку XML, встроенную в объектную модель. Вы ознакомились с основами XML в главе 23, и теперь можно приступать к изучению его поддержки со стороны ADO.NET. Поддержка XML в объектах DataSet из ADO.NET Поддержка ADO.NET языка XML сосредоточена вокруг объекта DataSet, поскольку XML— это отношения и иерархически структурированные данные. DataSet имеет несколько методов, обрабатывающих XML, и один из наиболее простых в применении — это WriteXml (), который записывает содержимое DataSet в документ XML. Чтобы использовать WriteXml (), просто сконструируйте DataSet из имеющихся данных, используя тот же код, что и в предыдущих примерах, примените метод Fill () адаптера данных для загрузки данных, определите объекты DataRelation для отношений и т.д. Затем просто вызовите WriteXml () на построенном DataSet: thisDataSet.WriteXml("nwinddata.xml"); WriteXml () может записывать в различные места; эта версия метода просто пишет XML в файл. Внешняя программа, принимающая XML в качестве входного формата, может легко прочесть и обработать этот XML. Доступен также метод ReadXml () для чтения содержимого файла XML в DataSet. Следующий пример берет код из DataRelationExample и просто пишет данные DataSet в файл XML; вложенные циклы foreach заменены единственным вызовом WriteXml (). Перед запуском программы необходимо обеспечить наличие каталога C:\Northwind.
Глава 28. ADO.NET и LINQ поверх DataSet 927 Практическое занятие Запись XML ИЗ DataSet Выполните следующие шаги по модификации программы DataRelationExample для записи XML. 1. Откройте проект DataRelationExample и замените цикл foreach // Печать наших вложенных заказчиков и их ID заказов foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) } следующим кодом: custOrderRel.Nested = true; thisDataSet.WriteXml(@"c:\northwind\nwinddata.xml"); Console.WriteLine( @"Successfully wrote XML output to file c:\Northwind\nwinddata.xml"); 2. Запустите модифицированный пример. 3. Откройте Internet Explorer и загрузите файл С: \Northwind\nwinddata. xml, как показано на рис. 28.6. 9. Ь» р» Ч**** I** 0* М .*•» f - . - Лфпт\ • С \North»«xftn«rt<fcJ*a xirf l-lnlxl * Mi ^fl У То Мр protect you/ Mcvty, Internet Explorer bet restarted the Me from «howno. «cove content that соиИ «сем* you computer Ckk here tor option» 1 <»xml version=*l.O* standalone="yes" ?> I • <Ne*DaUSet> 1 - <Custome'«.> <CustometlD>ALFrU</Customer!D> <CompanvName>AltTedsFutt«rtilst#</CofopanyName> ^ContactNan»e>Mede And*r»</Corn act Name-» <ContactTitle>iales Repre««ntetlwe</ContactTttle> <£ddress>Obere Mr. 57</Addrass> <Gty >B#fHn</Ot / > <Po$talCode>12209</PoitalCode> <rountry>Q#nn«nv</Country> <Phone >030-0074321< /Phone > <Fax>030-0076S43</Fax> - <Ord#rs> <OrderlD> 10e43</0rderl0> <Custom»rlD>ALFKIv/Cw^ 'pu.IL-. <EmployeeID>e<./EmpioyeeID> <OrdetDare>1997-0e-23T00:00:00-04:OO</OrderDate> <toqutredDare>1997-09-22T00:00:00-04:00(/l<eq<j.red0ate> <ShippedDate>1997-09-02T00:00:00-04:00</Shipped0at«> <ShipVia>l</ShipVia> <Fre.ght>1.2100</Freight> <3rpf;ame> Alfreds Futt«rklst*</5h>pNarne> <£hipAddre*s>Ob«re 8tr. 57</ShrpAddre$«> <Sh.pC i!y>B*Hln</ShipCity> <5hipPo<-.talv:ode>12209</ShipPof.talCode> <ShipCouritr^Oennenv</5hipCountrv> </Orders> ;e<-s> I»*» T"T i г i fg^ce-cut-r x 1 d 4 A Рис. 28.6. Файл С: \Northwind\nwinddata. xml в Internet Explorer Описание полученных результатов Свойство Nested объекта DataRelation сообщает методу WriteXml () о том, что в зыводе XML нужно вложить детали заказов и сами заказы внутрь каждого заказчика. Файл nwinddata. xml содержит все данные в таблицах (включая все столбцы, посколь- су при наполнении DataSet специфицировано SELECT * FROM). Информация выводит- :я в читабельном для человека, легко понимаемом формате XML, и этот файл может 5ыть просмотрен непосредственно в Microsoft Internet Explorer.
928 Часть ГУ. Доступ к данным Наряду с WriteXml (), в DataSet также имеется метод ReadXml (). Метод ReadXml () создает DataTable в DataSet и наполняет его данными из файла XML. Более того, созданный DataTable получает имя корневого элемента в документе XML. Имея файл XML, только что созданный в предыдущем примере, в следующем практическом занятии вы прочитаете его и отобразите на экране. практическое занятие Чтение XML в DataSet Выполните следующие шаги для создания примера ReadingXML в Visual C# 2008. 1. Создайте новое консольное приложение по имени ReadingXML в каталоге С:\BegVCSharp\Chapter28. 2. Добавьте в начало кода директиву using для пространства имен System. Data: using System.Data; 3. Добавьте следующий код в метод Main (): static void Main(string [ ] args) { DataSet thisDataSet = new DataSet (); thisDataSet.ReadXml(@"c:\Northwind\nwinddata.xml"); foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine("ID заказчика: " + custRow["CustomerlD"] + " Название: " + custRow["CompanyName"]); } Console.WriteLine("Таблица, созданная ReadXml, называется {0}", thisDataSet.Tables[0].TableName); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); 4. Запустите приложение. Вы должны увидеть вывод, подобный приведенному ниже, если ранее запускали предыдущий пример для создания файла С:\Northwind\nwinddata.xml: ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика ID заказчика SANTG Название: Sante Gourmet SAVEA Название: Save-a-lot Markets SEVES Название: Seven Seas Imports SIMOB Название: Simons bistro SPECD Название: Specialites du monde SPLIR Название: Split Rail Beer & Ale SUPRD Название: Supremes delices THEBI Название: The Big Cheese THECR Название: The Cracker Box TOMSP Название: Toms Spezialitaten TORTU Название: Tortuga Restaurante TRADH Название: Tradicao Hipermercados TRAIH Название: Trail's Head Gourmet Provisioners VAFFE Название: Vaffeljernet VICTE Название: Victuailles en stock VINET Название: Vins et alcools Chevalier WANDK Название: Die Wandernde Kuh WARTH Название: Wartian Herkku WELLI Название: Wellington Importadora WHITC Название: White Clover Markets
Глава 28. ADO.NET и LINQ поверх DataSet 929 ID заказчика: WILMK Название: Wilman Kala ID заказчика: WOLZA Название: Wolski Zajazd ID заказчика: ZACZI Название: Zachary Zithers Ltd. Таблица, созданная ReadXml, называется Customers Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Здесь используется только одно пространство имен — System. Data. Обращений к каким-либо базам данных нет, потому и не нужны пространства System. Data. SqlClient или System. Data. OleDb. Все, что делается— это создание DataSet с последующим использованием метода ReadXml () для загрузки данных из файла С: \Northwind\ nwinddata.xml. Перегрузка ReadXml (), используемого здесь, требует лишь указания файла XML: DataSet thisDataSet = new DataSet (); this DataSet .ReadXml (@Mc: \Northwmd\nwinddata . xml") ; Затем выводится содержимое таблицы Customers (этот код должен быть знаком по примеру DataRelationExample) и осуществляется проход циклом по каждому DataRow в коллекции Rows таблицы Customers с отображением значений столбцов CustomerlD и CompanyName: foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine("ID заказчика: " + custRow["CustomerlD"] + " Название: " + custRow["CompanyName"]); } Откуда вы узнали, что таблица называлась Customers? Как упоминалось ранее, объект DataTable именуется по корневому узлу документа XML, из которого он прочитан; если вернуться к методу WriteXml (), легко увидеть, что корневой узел произведенного документа XML в действительности называется Customers. Чтобы удостовериться, выведите имя первого DataTable в коллекции Tables объекта DataSet, используя свойство TableName этого объекта DataTable: Console.WriteLine("Таблица, созданная ReadXml, называется {0}", thisDataSet.Tables[0].TableName); Поддержка SQL в AD0.NET Эта глава описывает базовые операции ADO.NET, не требуя знания языка запросов баз данных SQL. Все команды ADO.NET, которые читают и записывают в источники данных, транслируются в команды SQL, выполняющие "сырые" операции в базе данных. Практическое применение ADO.NET в реальных повседневных ситуациях требует некоторого знания SQL; за более полным изложением SQL, для которого здесь нет места, обращайтесь к соответствующим книгам. Альтернативой является применение LINQ, и тогда вы привязаны к С#! Но некоторые основы здесь все же будут изложены. Команды SQL в адаптерах данных В примерах, приведенных ранее, использовались SQL-команды SELECT, которые возвращают все строки таблицы, такие как эта: SqlDataAdapter thisAdapter = new SqlDataAdapter ( "SELECT * FROM Customers", thisConnection);
930 Часть IV. Доступ к данным Команда SELECT возвращает все строки и столбцы таблицы заказчиков при вызове метода Fill (), загружая их в память вашей программы. Это хорошо для небольших таблиц вроде Customers из базы Northwind, в которой только 11 столбцов и менее 100 строк данных; однако маловероятно, что такой подход будет хорошо работать с крупными таблицами, часто встречающимися в бизнес-приложениях, в которых может быть 100 000 или даже 1 000 000 строк. Необходимо сконструировать команду SELECT, чтобы она доставляла только те данные, которые действительно нужно обработать. Один способ ограничения количества столбцов используется, если программа взаимодействует только с некоторыми из столбцов, когда команда SELECT специфицирует лишь нужные столбцы: SELECT CustomerlD, CompanyName FROM Customers Однако обычно это не подходит при добавлении строк, поскольку при этом необходимо специфицировать значения для всех столбцов. Использование select с конструкцией where Другая техника для минимизации объема данных, загружаемых в память, состоит в том, чтобы всегда задавать в SQL-операторе SELECT конструкцию WHERE, что ограничит количество выбираемых строк. Например, оператор SELECT * FROM Customers WHERE CustomerlD = 'ZACZI' загружает в память только одну строку, содержащую информацию о заказчике Zachary Zithers, занимая только малую часть той памяти, что понадобилась бы для загрузки всей таблицы. В конструкции WHERE может быть указан также диапазон, поэтому следующий оператор SELECT * FROM Orders WHERE OrderlD BETWEEN 10900 AND 10999 загружает только те строки, у которых значение OrderlD находится в заданном диапазоне. Если можете ограничить количество строк, загружаемых из большой таблицы посредством конструкции WHERE, всегда так и поступайте. Никогда не загружайте все строки таблицы в DataSet с последующим поиском в нем в цикле f oreach; для выполнения такого поиска применяйте этого оператор SELECT с конструкцией WHERE. Ваша цель — найти наиболее эффективный баланс между локально обработкой данных на клиенте, где выполняется программа ADO.NET, и обработкой на сервере, где выполняется SQL. Объектная модель ADO.NET и С# больше подходят для сложных вычислений и навигационной логики, чем SQL. Заполняйте DataSet данными из таблиц, которые хотите обработать, и выполняйте логику этого рода на клиенте. Однако ограничение количества выбираемых строк из каждой таблицы соответствующими условиями значительно повысит производительность (особенно если данные передаются по сети), а также сократит затраты памяти. Просмотр SQL-команд select, update, insert и delete SQL использует базовые команды для запросов, обновления, добавления и удаления строк в таблице. Эти команды, соответственно— SELECT, UPDATE, INSERT и DELETE. В предыдущих примерах применялся объект CommandBuilder для создания команд SQL, используемых для обновления базы данных: SqlDataAdapter thisAdapter = new SqlDataAdapter("SELECT CustomerlD from Customers", thisConnection); SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter);
Глава 28. ADO.NET и LINQ поверх DataSet 931 Построитель команд генерирует операторы SQL для модификации данных (UPDATE, INSERT и DELETE) на основе заданной команды SELECT. В программе, которой посвящено следующее практическое занятие, команды будут генерироваться методами GetUpdateCommand (), GetlnsertCommand () и GetDeleteCommandO объекта CommandBuilder. Практическое занятие Пример Отображения SQL-КОМЭНД Выполните следующие шаги для создания примера ShowSQL в Visual C# 2008. 1. Создайте новое консольное приложение по имени ShowSQL в каталоге С: \BegVCSharp\Chapter28 и добавьте обычные директивы using в начало кода: #region Using Directives using System; using System.Data; // Использовать пространство имен AD0.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server using System.Collections .Generic- using System.Text; #endregion 2. Теперь добавьте следующий код в метод Main (): static void Main(string[] args) { // Указать специфичную для SQL Server строку соединения SqlConnection thisConnection = new SqlConnection ( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF';" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); thisConnection.Open(); SqlDataAdapter thisAdapter = new SqlDataAdapter("SELECT CustomerlD from Customers", thisConnection); SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter); Console.WriteLine("SQL-команда SELECT:\n{0}\n", thisAdapter.SelectCommand.CommandText); SqlCommand updateCommand = thisBuilder.GetUpdateCommand(); Console.WriteLine("SQL-команда UPDATE:\n{0}\n", updateCommand.CommandText); SqlCommand msertCommand = thisBuilder.GetlnsertCommand(); Console.WriteLine("SQL-команда INSERT:\n{0}\n", insertCommand.CommandText); SqlCommand deleteCommand = thisBuilder.GetDeleteCommandO; Console.WriteLine("SQL-команда DELETE:\n{0}", deleteCommand.CommandText); thisConnection.Close (); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console. ReadLme () ; }
932 Часть IV. Доступ к данным Вывод этого примера выглядит так: SQL-команда SELECT: SELECT CustomerlD from Customers SQL-команда UPDATE: UPDATE [Customers] SET [CustomerlD] = @pl WHERE (([CustomerlD] = @p2) ) SQL-команда INSERT: INSERT INTO [Customers] ([CustomerlD]) VALUES (@pl) SQL-команда DELETE: DELETE FROM [Customers] WHERE (([CustomerlD] = @pl)) Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Обратите внимание, что команды UPDATE и DELETE используют конструкцию WHERE, сгенерированную объектом CommandBuilder. Вопросительный знак (?) помечает места, куда исполняющая система ADO.NET подставит действительные значения параметров команды; например, при использовании метода Delete () для удаления строки, которая содержит CustomerlD, равный "ZACZI", в момент вызова Update () будет выполнена следующая команда, удаляющая строку ZACZI: DELETE FROM Customers WHERE ( CustomerlD = 'ZACZI' ) Для вывода команды SELECT применялось свойство SelectCommand для получения команды непосредственно от DataAdapter. Класс DataAdapter также имеет свойства UpdateCommand, InsertCommand и DeleteCommand для получения набора команд SQL, используемых во время обновления непосредственно. Разработчик, знакомый с SQL, может оптимизировать эти команды для более эффективного их выполнения, чем те команды, что автоматически сгенерированы CommandBuilder, особенно когда все столбцы включены в SQL-оператор SELECT. Непосредственное выполнение команд SQL Если вашей программе нужно выполнять ориентированные на множества операции, такие как удаление или обновление всех строк, отвечающих определенному условию, будет намного эффективнее, особенно для больших таблиц, делать это в единственной команде SQL, чем выполнять расширенную обработку в коде С#. ADO.NET представляет объекты SqlCommand или OleDbCommand для выполнения команд SQL. Эти объекты предлагают методы для непосредственного выполнения команд SQL. Вы использовали метод ExecuteReader () в начале этой главы, когда речь шла об объекте DataReader. Здесь будут показаны другие методы выполнения команд SQL, а именно — ExecuteScalar () и ExecuteNonQuery (). Извлечение одиночных значений Во многих случаях необходимо возвращать единственный результат от запроса SQL, такой как количество записей в заданной таблице. Метод ExecuteScalar () позволяет достичь этого — он используется для выполнения команд SQL, возвращающих скалярное (одиночное) значение, в противоположность возврату множества строк, как в случае ExecuteReader (). В следующем практическом занятии для выполнения запроса используется метод ExecuteScalar() объекта SqlCommand.
Глава 28. ADO.NET и LINQ поверх DataSet 933 практическое занятие Извлечение одиночных значений с помощью ExecuteScalar () В качестве первого примера рассмотрим программу, которая получает количество строк в таблице Customers. Это подобно приведенному ранее примеру DataReader, но использует другой оператор SQL и метод выполнения. 1. Создайте новое консольное приложение по имени ExecuteScalarExample в каталоге С: \BegVCSharp\Chapter28 и добавьте обычные директивы using в начало кода: #region Using Directives using System; using System.Data; // Использовать пространство имен ADO.NET using Systern.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server 2. Добавьте следующий код в метод Main (): static void Main(string [ ] args) { SglConnection thisConnection = new SglConnection( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF';" + @"Integrated Secunty=True;Connect Timeout=30;User Instance=true" ); thisConnection.Open(); SglCommand thisCommand = thisConnection.CreateCommand(); thisCommand.CommandText = "SELECT COUNT(*) FROM Customers"; Object countResult = thisCommand.ExecuteScalar(); Console.WriteLine("Количество строк в таблице Customers = {0}", countResult); thisConnection.Close (); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine (); } Описание полученных результатов Эта программа использует SQL Server поставщика данных .NET. Центральная часть программы — такая же, как и в первом примере настоящей главы — открывает соединение с SQL Server на локальной машине с интегрированной системой безопасности и базой данных Northwind. Bbifсоздаете объект SqlCommand и присваиваете команду SELECT COUNT (*) ее свойству CommandText. COUNT () — функция SQL, возвращающая результат подсчета строк, которые соответствуют условию WHERE. Затем вызывается метод ExecuteScalar () объекта SqlCommand для выполнения запроса, извлекающего значение счетчика. Он отображается и программа завершается. При выполнении на базе данных Northwind программа отображает следующее (исходя из того, что запись для Zachary Zithers Ltd была удалена): Количество строк в таблице = 91 Это эквивалентно загрузке таблицы Customers в объект DataTable и использованию свойства Count объекта Rows, как в предыдущем примере. Зачем может понадобиться выполнять работу подобным образом? Это зависит от структуры данных и от того, что еще делается в программе. Когда есть лишь небольшой объем данных, или
934 Часть IV. Доступ к данным по той или иной причине загружаются все строки в DataSet, имеет смысл просто воспользоваться DataTable. Rows. Count. Однако если требуется подсчитать точное количество строк в очень большой таблице, скажем, с 1 000 000 строк, то намного эффективнее издать запрос SELECT COUNT (*) с помощью метода ExecuteScalar (), вместо того, чтобы пытаться загрузить 1 000 000 строк в память. Извлечение пустых данных Довольно странный заголовок, учитывая то, что SQL-операции модификации данных вроде INSERT, UPDATE и DELETE не возвращают данных. Что интересно в этих командах, так это количество строк, которые они затрагивают. Это число и возвращается методом ExecuteNonQuery (). Предположим, что один из поставщиков повысил цену на 5% для всех своих продуктов. В следующем практическом занятии показано, как использовать объект SqlCommand для выполнения SQL-команды UPDATE для повышения на 5% всех цен на продукты, поставляемые этим поставщиком. шшшшяшяшшшшяшшшшшшт Модификация данных с помощью ExecuteNonQuery Выполните следующие шаги для создания примера ExecuteNonQueryExample в Visual Studio 2008. 1. Создайте новое консольное приложение по имени ExecuteNonQueryExample в каталоге С: \BegVCSharp\Chapter28 и добавьте обычные директивы using в начало кода: #region Using Directives using System; using System.Data; // Использовать пространство имен ADO.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server 2. Добавьте следующий код в метод Main (): static void Main(string [ ] args) { SqlConnection thisConnection = new SqlConnection( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename=fC:\SQL Server 2000 Sample Databases\NORTHWND.MDF';" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); thisConnection.Open (); SqlCommand thisCommand = thisConnection.CreateCommand(); thisCommand.CommandText = "UPDATE Products SET " + "UnitPrice=UmtPrice*1.05 WHERE Supplierld=12"; int rowsAffected = thisCommand.ExecuteNonQuery(); Console.WriteLine("Обновлено строк = {0}", rowsAffected); thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); }
Глава 28. ADO.NET и LINQ поверх DataSet 935 Описание полученных результатов Эта программа открывает соединение, как и в прошлом примере. Затем создается объект SqlCommand и присваивается команда Update. После этого вызывается метод ExecuteNonQuery () объекта SqlCommand для выполнения запроса, который возвращает количество строк, затронутых им в базе данных. Это количество строк отображается и программа завершается. При выполнении на базе данных Northwind программа отображает следующее: Обновлено строк = 5 Это говорит о том, что изменения коснулись пяти продуктов. Вызов хранимой процедуры SQL И, наконец, рассмотрим пример вызова хранимой процедуры SQL — процедуры, написанной на SQL и хранящейся в базе данных. Хранимые процедуры инкапсулируют сложные запросы SQL и процедуры базы данных в едином целом, и могут быть вызваны множеством прикладных программ или непосредственно пользователями; администратор базы данных может быть уверен, что при каждом вызове хранимой процедуры будут выполнены в точности одни и те же шаги, гарантируя согласованное использование базы данных. Пользователи и разработчики приложений не обязаны запоминать сложный синтаксис запросов SQL; им лишь следует знать имя хранимой процедуры, и что она делает. База данных Northwind содержит несколько хранимых процедур. Следующий пример вызывает процедуру Ten Most Expensive Products (десять наиболее дорогих продуктов), которая включена в базу данных примеров Northwind. Эта процедура возвращает два столбца: TenMostExpensiveProducts и UnitPrice. Испытаем ее. По существу эта программа представляет собой вариант самого первого примера настоящей главы— DataReading. Подобно командам SQL, хранимые процедуры могут возвращать одиночные значения, и в этом случае необходимо использовать ExecuteScalar или ExecuteNonQuery, как было только что показано для обычных команд SQL. Альтернативно они могут возвращать результаты запроса, которые читаются DataReader, как в приведенном ниже примере. 1. Создайте новое консольное приложение по имени SQLStoredProcedure в каталоге С: \BegVCSharp\Chapter28 и добавьте обычные директивы using в начало кода: #region Using Directives using System; using System.Data; // Использовать пространство имен ADO.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server 2. Измените метод Main (), как показано ниже (здесь все так же, как в DataReading, за исключением выделенных полужирным отличий): static void Main(string[] args) { // Указать специфичную для SQL Server строку соединения
936 Часть ГУ. Доступ к данным SqlConnection thisConnection = new SqlConnection( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\NORTHWND.MDF';" + @"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Открыть соединение thisConnection.Open(); // Создать команду для данного соединения SqlCommand thisCommand = thisConnection.CreateCommand() ; // Установить в качестве типа команды StoredProcedure (хранимая процедура) thisCommand. CommandType = CommandType. StoredProcedure ; thisCommand.CommandText = "Ten Most Expensive Products"; // Выполнить DataReader для указанной команды SqlDataReader thisReader = thisCommand.ExecuteReader (); // Пока читаются строки while (thisReader.Read()) { // Вывести столбцы наименования продукта и его цены Console.WriteLine("\t{0}\t{l}", thisReader["TenMostExpensiveProducts"], thisReader["UnitPrice" ]) ; } // Закрыть читатель thisReader.Close(); // Закрыть соединение thisConnection.Close(); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 3. Нажмите <F5> для запуска отладчика; вы должны увидеть результаты, показанные ниже (точные значения могут отличаться в зависимости от используемой версии базы данных Northwind): Cote de Blaye 263.5000 Thuringer Rostbratwurst 123.7900 Mishi Kobe Niku 97.0000 Sir Rodney's Marmalade 81.0000 Carnarvon Tigers 62:5000 Raclette Courdavault 55.0000 Manjimup Dried Apples 53.0000 Tarte au sucre 49.3000 Ipoh Coffee 46.0000 Rossle Sauerkraut 45.6000 Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Эта программа открывает соединение, как и предыдущие примеры. Затем создается объект SqlCommand, и его параметр CommandType устанавливается в CommandType. StoredProcedure — значение перечисления внутри ADO.NET, обозначающее вызов хранимой процедуры. Когда CommandType равно StoredProcedure, свойство CommandText хранит имя хранимой процедуры, а не команду SQL. Остальная часть программы в точности такая же, как и при выполнении обычных команд SQL. Возвращаемые столбцы хранят разные значения, поэтому вывод может слегка отличаться.
Глава 28. ADO.NET и LINQ поверх DataSet 937 Использование LINQ поверх DataSetcADO.NET В предыдущей главе вы изучили LINQ to SQL и его преимущества, позволяющие упрощать запросы и доступ к базе данных. Можно также использовать LINQ в сочетании с ADO.NET, применяя средство LINQ поверх DataSet, что и рассматривается в этом разделе. Когда использовать LINQ поверх DataSet При наличии унаследованного кода, который уже использует объекты DataSet ADO.NET, LINQ поверх DataSet добавляет полный набор операторов запросов LINQ к ограниченным возможностям запросов, встроенным в исходный класс DataSet, не требуя переписывания всего приложения для использования классов LINQ to SQL. LINQ поверх DataSet также полезен, если приложению требуются расширенные манипуляции отключенными наборами данных. практическое занятие Использование LINQ поверх DataSet Выполните следующие шаги для создания программы LINQoverDataSet в Visual Studio 2008. 1. Создайте новое консольное приложение по имени LINQoverDataSet в каталоге С:\BegVCSharp\Chapter28. 2. Добавьте ссылку на компонент System. Data. Linq, как показано на рис. 28.7. NET |cOM | Pro*tl | Brow» 1 System. Addln Contract 1 System.Configuration 1 System Configuration Instal 1 System.Core 1 System Data 1 System Data Enttv 1 System£eta^ntt^)esign^ 1 System Data OacleCkent 1 System Data.Sqlxml 1 System. Deployment 1 System.Design 1 System Direct or у Services 1 System. De-ectoryServtces. Proto, 1 System Drawing м Recent | 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 iRuntkM v2.0.50727 V2.0.50727 V2.0.50727 V2.0.50727 v2.0.S0727 V2.050727 V2.0.50727 V2.0.50727 V2.0.50727 V2.0.50727 v2.0.50727 V2.0.50727 V2.0.50727 V2.050727 P*h M CAProgremFew C\WlNDOWS\r/ C:\WINDOWS\N C:\ProgramRlej C:\WINDOWS\N C:\Program F*es CVProgremFles AWINDOWSAK | C:\WINDOWS\K-J C:\WINDOWSV C:\WINDOWS\N C:\WINDOWS\N C:\WINDOWS\N CAWWDOWSV^j -J 21 OK Cancel 1 Рис. 28.7. Добавление ссылки на компонент System .Data. Linq Обратите внимание, что LINQ поверх DataSet также использует пространство имен System.Data.DataSetExtensions, которое автоматически добавляется Visual Studio. В случае LINQ поверх DataSet используется только малая часть средств LINQ to Entity, упомянутого в начале этой главы. Хотя LINQ to Entities сам по себе может служить темой целой книги для начинающих вроде этой, LINQ to DataSet — это лишь часть LINQ to Entity, которую можно применять немедленно на основе того, что уже известно о LINQ
938 Часть IV. Доступ к данным 3. Начните с добавления директив using для классов ADO.NET и LINQ, которые вы будете использовать: #region Using Directives using System; using System. Collect ions. Generic- using System.Linq; using System.Data; // Использовать пространство имен ADO.NET using System.Data.SqlClient; // Использовать пространство имен // поставщика данных SQL Server using System.Data.Linq; // Использовать коннектор LINQ / ADO.NET using System.Data.Common; using System.Text; tendregion 4. Добавьте показанный ниже код в метод Main (). Поскольку этот код такой же, как и начало кода программы DataRelationExample, вы можете счесть удобным вырезать и вклеить ее исходный файл: static void Main(string[] args) * { II Указать специфичную для SQL Server строку соединения SglConnection thisConnection = new SqlConnection( @"Data Source=.\SQLEXPRESS;"+ @"AttachDbFilename='C:\SQL Server 2000 Sample Databases\N0RTHWND.MDF';" + ©"Integrated Security=True;Connect Timeout=30;User Instance=true" ); // Создать объект DataAdapter для обновления и прочих операций SqlDataAdapter thisAdapter = new SqlDataAdapter ( "SELECT CustomerlD, CompanyName FROM Customers", thisConnection); // Создать объект CommandBuilder для построения команд SQL SqlCommandBuilder thisBuilder = new SqlCommandBuilder(thisAdapter); // Создать DataSet для хранения связанных таблиц данных, строк и столбцов DataSet thisDataSet = new DataSet (); // Установить объекты DataAdapter для каждой таблицы и заполнить их SqlDataAdapter custAdapter = new SqlDataAdapter( "SELECT * FROM Customers", thisConnection); SqlDataAdapter orderAdapter = new SqlDataAdapter( "SELECT * FROM Orders", thisConnection); custAdapter.Fill(thisDataSet, "Customers"); orderAdapter.Fill(thisDataSet, "Orders"); // Установить DataRelation между заказчиками и заказами DataRelation custOrderRel = thisDataSet.Relations.Add("CustOrders", thisDataSet.Tables["Customers"].Columns["CustomerlD"] , thisDataSet.Tables["Orders"].Columns["CustomerlD"] ) ; 5. Теперь добавьте следующий код, использующий LINQ: var customers = thisDataSet.Tables["Customers"].AsEnumerable() ; var orders = thisDataSet.Tables["Orders"].AsEnumerable(); var preferredCustomers = from с in customers where c.GetChildRows("CustOrders").Length > 10 orderby c.GetChildRows("CustOrders").Length select c; Console.WriteLine("Заказчики, у которых заказов > 10:");
Глава 28. ADO.NET и LINQ поверх DataSet 939 foreach (var customer in preferredCustomers) { Console. WriteLineC {0} заказов: {1} {2}, {3} {4}" customer.GetChildRows("CustOrders").Length, customer["CustomerlD"], customer["CompanyName"], customer["City"], customer["Region"] ); 6. Завершите метод Main () стандартной логикой закрытия, которая использовалась в прочих примерах: thisConnection.Close (); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); 7. Откомпилируйте и запустите приложение. Вы должны увидеть следующий вывод: у которых заказов > 10: LINOD LINO-Delicateses, I. de Margarita Nueva Esparta REGGC Reggiani Caseifici, Reggio Emilia SUPRD Supremes delices, Charleroi AROUT Around the Horn, London MEREP Mere Paillarde, Montreal Quebec QUEEN Queen Cozinha, Sao Paulo SP BOTTM Bottom-Dollar Markets, Tsawassen ВС HANAR Hanari Carnes, Rio de Janeiro RJ KOENE Koniglich Essen, Brandenburg LAMAI La maison d'Asie, Toulouse LILAS LILA-Supermercado, Barquisimeto Lara WHITC White Clover Markets, Seattle WA FRANK Frankenversand, Munchen LEHMS Lehmanns Marktstand, Frankfurt a.M. WARTH Wartian Herkku, Oulu BONAP Bon app', Marseille BERGS Berglunds snabbkop, Lulea HILAA HILARION-Abastos, San Cristobal Tachira RATTC Rattlesnake Canyon Grocery, Albuquerque NM F0LK0 Folk och fa HB, Bracke HUNGO Hungry Owl Ail-Night Grocers, Cork Co. Cork QUICK QUICK-Stop, Cunewalde ERNSH Ernst Handel, Graz SAVEA Save-a-lot Markets, Boise ID Программа завершена, нажмите Enter для продолжения: Описание полученных результатов Прежде чем конструировать DataRelation, вы должны создать объект DataSet и связать с ним таблицы базы данных, которые собираетесь использовать, как показано ниже: DataSet thisDataSet = new DataSet (); SqlDataAdapter custAdapter = new SqlDataAdapter( "SELECT * FROM Customers", thisConnection); SqlDataAdapter orderAdapter = new SqlDataAdapter( "SELECT * FROM Orders", thisConnection); custAdapter.Fill(thisDataSet, "Customers"); orderAdapter.Fill(thisDataSet, "Orders"); Вы создаете объект DataAdapter для каждой таблицы, на которую ссылаетесь. Затем вы наполняете DataSet данными из столбцов, с которыми будете работать; в этом случае вы не заботитесь об эффективности, потому просто используете все дос- 3ai 12 12 12 13 13 13 14 14 14 14 14 14 15 15 15 17 18 18 18 19 19 28 30 31 <:азчики, заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов заказов
940 Часть IV. Доступ к данным тупные столбцы (SELECT * FROM <таблица>). Затем строится объект DataRelation, который связывается с Data Set: DataRelation custOrderRel = thisDataSet.Relations.Add("CustOrders", thisDataSet.Tables["Customers"].Columns ["CustomerlD"], thisDataSet.Tables["Orders"].Columns["CustomerlD"]); Теперь вы готовы искать заказчиков и заказы. Ваш первый шаг — цикл for each для отображения информации о каждом заказчике: foreach (DataRow custRow in thisDataSet.Tables["Customers"].Rows) { Console.WriteLine("ID заказчика: " + custRow["CustomerlD"] + " Название: " + custRow["CompanyName"]); Здесь осуществляется проход циклом по коллекции Rows таблицы Customers с печатью CustomerlD и CompanyName для каждого заказчика. После отображения заказчика выводятся связанные с ним заказы. Для этого добавляется вложенный цикл foreach, инициализированный методом GetChildRows () объекта DataRow. Объект DataRelation передается методу GetChildRows (), который возвращает DataRowCollection, содержащую только связанные с данным заказчиком объекты таблицы Orders. Чтобы отобразить эти связанные строки, выполняется цикл foreach по каждой DataRow в коллекции: namespace LINQtoDataSet { class Program { static void Main(string[] args) { var customers = thisDataSet.Tables["Customers"].AsEnumerable(); var orders = thisDataSet.Tables["Orders"].AsEnumerable(); var preferredCustomers = from с in customers where c.GetChildRows("CustOrders").Length > 10 orderby c.GetChildRows("CustOrders").Length select c; Затем процесс повторяется для каждого заказчика. К выводу Order ID добавляются ведущие пробелы, так что заказы каждого заказчика отображаются со смещением относительно информации о заказчике. Благодаря такой организации отображения, можно наглядно видеть отношение "родительский-дочерний" между каждым заказчиком и его заказами. Заказчик "Zachary Zithers Ltd." не имеет заказов, поскольку он был добавлен в таблицу в предыдущих примерах. Резюме В главе рассматривались следующие основные темы. □ ADO.NET— часть .NET Framework, которая позволяет обеспечить доступ к реляционным базам данных и другим источникам информации. Технология ADO.NET спроектирована так, чтобы предоставлять простой, гибкий каркас доступа к данным. Она ориентирована на многослойную архитектуру приложений и интегрирует реляционные данные с XML. □ Классы ADO.NET содержатся в пространстве имен System. Data. Была рассмотрена объектная модель ADO.NET и роли, которые играют ее основные объекты, включая соединения, команды, DataReader, адаптер данных, DataSet, DataTable, DataRow и DataColumn.
Глава 28. ADO.NET и LINQ поверх DataSet 941 □ Поставщики данных .NET, предоставляющие доступ к определенным источникам данных, и возможность расширения ADO.NET за счет написания поставщиков для новых источников. Вы ознакомились с такими поставщиками данных, как ADO.NET для Microsoft SQL Server и источники OLE DB, а также узнали о том, что они содержатся в пространствах имен System. Data . SqlClient и System. Data. OleDB соответственно. □ Вы узнали, как реализуется быстрый доступ только для чтения к данным через объект DataReader, и как написать эквивалентную программу для поставщиков данных .NET— как SqlClient, так и OleDb. Вы научились обновлять данные и добавлять новые строки данных через объекты DataSet, адаптеры данных и объекты CommandBuilder. Вы также узнали, как находить строки с применением первичного ключа, а также удалять строки. □ Доступ к множеству таблиц в DataSet через объект DataRelation для генерации XML-представления этих данных. И, наконец, вы кратко ознакомились с преимуществами поддержки языка SQL баз данных в ADO.NET, включая отображение сгенерированных команд SQL, прямое выполнение SQL и вызов хранимых процедур. Как было показано, создание кода баз данных может оказаться достаточно сложной задачей, если приложение не относится к числу самых простых. В следующей главе мы рассмотрим инструменты Visual Studio 2008, которые создают большую часть сложного кода и значительно облегчают разработку приложений баз данных. Упражнения 1. Модифицируйте программу, приведенную в первом примере, чтобы она использовала таблицу Employees базы данных Northwind. Извлеките столбцы EmployeelD и LastName. 2. Модифицируйте первую программу, демонстрирующую применение метода Update (), для изменения названия компании обратно в "Bottom-Dollar Markets". 3. Напишите программу, запрашивающую у пользователя идентификатор заказчика. 4. Напишите программу для создания некоторых заказов для заказчика Zachary Zithers; используйте простые программы для просмотра заказов. 5. Напишите программу для отображения различных частей иерархических отношений в базе данных Northwind, использованных в настоящей главе, таких как Products, Suppliers и Categories. 6. Запишите данные, сгенерированные в предыдущем упражнении, в виде документа XML. 7. Измените программу, выводящую детальную информацию о заказах и продуктах для всех заказчиков, чтобы она использовала конструкцию WHERE в операторе SELECT для таблицы Customers, тем самым ограничивая количество обрабатываемых заказчиков. 8. Модифицируйте программу, выводящую текст операторов UPDATE, INSERT и DELETE, чтобы она использовала в качестве SQL-команды "SELECT * FROM Customers". Обратите внимание на сложность сгенерированных операторов. 9. Модифицируйте пример LINQoverDataSet для использования таблицы Employees базы данных Northwind, как в первом упражнении. Извлеките столбцы EmployeelD и LastName.
29 LINQ to XML В предыдущих двух главах мы уже представили LINQ и показали, как LINQ работает с объектами и базами данных. Эта глава посвящена LINQ to XML — версии LINQ, позволяющей использовать LINQ для запросов и манипуляций XML. Язык XML был описан в главе 25, так что мы не станем здесь повторяться. Вы уже должны быть знакомы с документами XML, их элементами, атрибутами и т.п,; если вы еще не прочли введение в XML в главе 25, сделайте это сейчас. LINQ to XML не предназначен стать заменой стандартным API XML, таким как XML DOM (Document Object Model— объектная модель документов), XPath, XQuery, XSLT и т.д. Если вы знакомы с этими API или нуждаетесь в их применении или изучении, продолжайте этим заниматься. LINQ to XML дополняет стандартные классы XML и облегчает работу с XML. LINQ to XML предлагает дополнительные возможности для создания и опроса данных XML, в результате чего упрощается и ускоряется разработка для многих стандартных ситуаций, особенно если уже используется LINQ to Objects или LINQ to SQL в других частях программ. В частности, в этой главе будут рассматриваться следующие темы. □ Как создавать документы XML средствами функциональных конструирующих методов LINQ to XML. □ Как форматировать, загружать и сохранять документы XML с LINQ to XML. □ Как использовать LINQ to XML для работы с неполными документами XML (фрагментами). □ Как генерировать документ XML из запросов LINQ to SQL или LINQ to Object. □ Как опрашивать документ XML средствами LINQ to XML, используя стандартный синтаксис запросов LINQ. □ Как использовать агрегатные операции LINQ с LINQ to XML.
Глава 29. LINQ to XML 943 Функциональные конструкторы LINQ to XML Как было показано в предыдущих главах, одним из преимуществ С# 3.0 является легкость конструирования объектов, с помощью таких средств, как инициализаторы объектов и анонимные типы. LINQ to SQL продолжает эту тему, предлагая новый, облегченный способ создания документов XML, называемый функциональным конструированием, при котором вызовы конструкторов могут быть вложены таким образом, что естественно отображают структуру документа XML. В следующем практическом занятии функциональные конструкторы используются для создания простого документа XML, содержащего заказчиков и заказы. Прагп,чвсковзанят,ж Конструкторы LINQ to XML Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новое консольное приложение по имени BegVCSharp_29_l_LinqToXml Constructors в каталоге C:\BegVCSharp\Chapter29. 2. Откройте главный файл Program, cs. 3. Добавьте ссылку на пространство имен System.Xml .Linq в начало Program, cs, как показано ниже: using System; using System.Collections .Generic- using System.Linq; using System. Xml. Linq; using System.Text; 4. Добавьте следующий код в метод Main () в Program, cs: static void Main(string[] args) { XDocument xdoc = new XDocument ( new XElement("customers", new XElement("customer", new XAttnbute("ID", "A"), new XAttributeC'City", "New York"), new XAttribute("Region", "North America"), new XElement("order", new XAttribute("Item", "Widget"), new XAttribute("Price", 100) ), new XElement("order", new XAttribute("Item", "Tire"), new XAttribute("Price", 200) ) ), new XElement("customer", new XAttribute("ID", "B"), new XAttributeC'City", "Mumbai"), new XAttribute("Region", "Asia"), new XElement("order", new XAttribute("Item", "Oven"), new XAttribute("Price", 501) ) ) ) );
944 Часть ГУ. Доступ К данным Console.WriteLine(xdoc) ; Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 5. Откомпилируйте и выполните программу (можно просто нажать <F5> для запуска отладки). После этого вы увидите следующий вывод: <customers> <customer ID="A" City="New York" Region="North America"> <order Item="Widget" Price=00" /> <order Item="Tire" Price=00" /> </customer> <customer ID="B" City="Mumbai" Region="Asia"> <order Item="Oven" Price=01" /> </customer> </customers> Программа завершена, нажмите Enter для продолжения: Документ XML, показанный в выводе, содержит очень упрощенную версию данных о заказчиках/заказах, которые вы видели в предыдущих примерах. Обратите внимание, что корневой элемент документа XML — <customers> — содержит в себе вложенные элементы <customer>. Они, в свою очередь, содержат ряд элементов <order>. Элементы <customer> имеют два атрибута — City и Region, а элементы <order> — два атрибута Item и Price. Нажмите <Enter> для выхода из программы и очистки экрана консоли. Если использовалась комбинация <Ctrl+F5> (запуск без отладки), может понадобиться нажать <Enter> два раза. Описание полученных результатов Первый шаг состоит в добавлении ссылки на пространство имен System.Xml. Linq. Все последующие примеры настоящей главы требуют добавления в программу такой строки: using System.Xml.Linq; В то время как пространство System. Linq включается по умолчанию при создании проекта, пространство System.Xml .Linq потребуется включать вручную; эту строку необходимо ввести явно. Затем идут вызовы конструкторов LINQ to XML— XDocument () , XElement () и XAttribute (), — которые вложены внутрь друг друга, как показано ниже: XDocument xdoc = new XDocument( new XElement("customers", new XElement("customer", new XAttribute("ID", "A"), Обратите внимание, что код уже выглядит как XML — документ содержит элементы, и каждый элемент содержит атрибуты и прочие элементы. Рассмотрим каждый из конструкторов по очереди. □ XDOcument (). Это объект самого верхнего уровня в иерархии конструкторов LINQ to XML, представляющий полный документ XML. Он появляется в коде следующим образом: static void Main(string [ ] args) { XDocument xdoc = new XDocument ( );
Глава 29. LINQ to XML 945 Список параметров XDocument () в приведенном фрагменте кода опущен, так что вы можете видеть начало и конец вызова XDocument (). Подобно всем конструкторам LINQ to XML, XDocument () принимает массив объектов (Object [ ]) как один из его параметров, так что ряд других объектов, созданных другими конструкторами, может быть передан ему. Все прочие конструкторы, которые вызываются в этой программе, являются параметрами одного вызова конструктора XDocument (). Первый (и единственный) параметр, который передается в программе — это конструктор XElement (). □ XElement (). В документе XML должен присутствовать корневой элемент, так что в большинстве случаев список параметров XDocument () начинается с объекта XElement. Конструктор XElement () принимает имя элемента в виде строки, за которым следует список объектов XML, содержащихся внутри элемента. Здесь корневым элементом является "customers", который, в свою очередь, содержит список элементов "customer": new XElement("customers", new XElement("customer", ), ) □ Элемент "customer" не содержит в себе никаких других элементов. Вместо этого он содержит три атрибута XML, которые создаются конструктором XAttributeO. □ XAttribute (). Здесь к элементу "customer" добавляются три атрибута XML, именуемые "ID", "City" и "Region": new XAttribute("ID", "A"), new XAttribute("City", "New York"), new XAttribute("Region", "North America"), □ Поскольку атрибут XML по определению является листовым узлом XML, не содержащим в себе никаких других узлов XML, конструктор XAttributeO принимает только имя атрибута и его значение в качестве параметров. В данном случае три сгенерированных атрибута— это ID="A", City="New York" и Region="North America". □ Другие конструкторы LINQ to XML. Хотя они не вызываются в рассматриваемой программе, существуют и другие конструкторы LINQ to XML для всех типов узлов XML, такие как XDeclaration () — для объявления XML в начале XML-документа, XComment () — для комментария XML и т.д. Другие конструкторы не используются часто, но доступны, если вы нуждаетесь в них для тонкого контроля форматирования документа XML. Дополнительное описание полученных результатов В завершение объяснений первого примера: к элементу "customer" добавляются два дочерних элемента "order", сопровождаемые атрибутами "ID", "City" и "Region": new XElement("order", new XAttribute ("Item", "Widget"), new XAttribute("Price", 100) ), new XElement("order", new XAttribute("Item", "Tire"), new XAttribute("Price", 200)
946 Часть ГУ. Доступ к данным Элементы "order" имеют атрибуты "Item" и "Price", но не имеют других дочерних элементов. Затем содержимое полученного документа выводится на консоль: Console.WriteLine(xdoc) ; Этот оператор выводит текст XML-документа, используя метод ToStringO по умолчанию объекта XDocument. И, наконец, вывод на экран приостанавливается, чтобы можно было увидеть консольный вывод, ожидая нажатия пользователем клавиши <Enter>: Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); После этого программа выходит из метода Main () и завершается. Конструирование текста элемента XML со строками Приведенный выше пример форматирует XML без какого-либо текста в элементах. Но часто код XML нуждается также в некотором текстовом содержимом, и это очень легко реализовать с помощью конструктора LINQ to XML объекта XElement (). Например, чтобы сделать идентификатор текстом элемента <customer> вместо его атрибута, просто передайте строку в параметрах конструктора XElement () вместо вложенного XAttribute: XDocument xdoc = new XDocument( new XElement("customers", new XElement("customer", "AAAAAA", new XAttribute("City", "New York"), new XAttribute("Region", "North America") ), new XElement("customer", "BBBBBB", new XAttribute("City", "Mumbai"), new XAttribute("Region", "Asia") ) ); Это создаст документ XML следующего вида: <customers> <customer City="New York" Region="North America">AAAAAA</customer> <customer City="Mumbai" Region="Asia">BBBBBB</customer> </customers> Конструктор XElement () соединяет все строки в списке параметров в один текстовый раздел элемента. Сохранение и загрузка документа XML Вы, возможно, заметили, что при отображении документа XML на экране консоли методом Console.WriteLine () он не отображает нормального объявления XML, начинающегося с <?xml version="l .0". Хотя такое объявление можно создать явно с помощью конструктора XDeclaration (), обычно незачем это делать, поскольку оно создается автоматически при сохранении XML-документа в файл методом Save () из LINQtoXML.
Глава 29. LINQ to XML 947 Вдобавок, хотя конструирование документов XML в программе полезно для понимания работы конструкторов, делать это часто не придется. Чаще доведется загружать XML-документы из внешнего источника, такого как файл. В следующем практическом занятии вы испытаете эти операции в действии. практическое занятие Сохранение и загрузка документа XML Для создания примера выполните следующие шаги в Visual C# 2008. 1. Либо модифицируйте предыдущий пример, либо создайте новое консольное приложение по имени BegVCSharp_2 9_2_SaveLoadXML в каталоге С:\ BegVCSharp\Chapter2 9. 2. Откройте главный файл Program, cs. 3. Добавьте ссылку на пространство имен System.Xml .Linq в начало Program, cs, как показано ниже: using System; using System.Collections .Generic- using System.Linq; using System.Xml.Linq; using System.Text; Если вы модифицируете предыдущий пример, эта строка уже есть. 4. Если это еще не сделано, добавьте конструктор документа XML с вложенным элементом XML и вызовами атрибутов из предыдущего примера в метод Main () в Program.cs: static void Main(string[] args) { XDocument xdoc = new XDocument( new XElement("customers", new XElement("customer", new XAttributeC'ID", "A"), new XAttribute("City", "New York"), new XAttribute("Region", "North America"), new XElement("order", new XAttribute ("Item", "Widget"), new XAttribute("Price", 100) ), new XElement("order", new XAttribute("Item", "Tire"), new XAttribute("Price", 200) ) ), new XElement("customer", new XAttributeC'ID", "B"), new XAttribute("City", "Mumbai"), new XAttribute("Region", "Asia"), new XElement("order", new XAttribute ("Item", "Oven"), new XAttribute("Price", 501) ) ) ) );
948 Часть IV. Доступ к данным 5. После добавления кода конструктора на предыдущем шаге добавьте следующий код для сохранения, загрузки и отображения документа XML в конец метода Main () в Program.cs: string xmlFileName = @"c:\BegVCSharp\Chapter29\Xml\example2.xml"; xdoc.Save(xmlFileName); XDocument xdoc2 = XDocument.Load(xmlFileName); Console.WriteLine("Содержимое xdoc2:"); Console.WriteLine(xdoc2); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 6. Откомпилируйте и выполните программе (можно просто нажать <F5> для запуска отладки). После этого в окне консоли должен появиться следующий вывод: Содержимое xdoc2: <customers> <customer ID="A" City="New York" Region="North America"> <order Item="Widget" Price=00"/> <order Item="Tire" Price=00"/> </customer> <customer ID="B" City="Mumbai" Region="Asia"> <order Item="Oven" Price=01"/> </customer> </customers> Программа завершена, нажмите Enter для продолжения: Нажмите <Enter> для выхода из программы и очистки экрана консоли. Если использовалась комбинация <Ctrl+F5> (запуск без отладки), может понадобиться нажать <Enter> два раза. Описание полученных результатов Как и ранее, первый шаг — ссылка на пространство имен System.Xml. Linq. Затем идут вложенные вызовы конструкторов LINQ to XML— XDocument (), XElement () и XAt tribute (). Объяснение этих частей кода, которые повторяются здесь, ищите в описании первого примера. После создания объекта XDocument вы специфицируете имя файла в виде строки и сохраняете документ XML в файл вызовом метода Save (): string xmlFileName = @"с:\BegVCSharp\Chapter29\Xml\example2.xml"; xdoc.Save(xmlFileName); Хотя в данном конкретном случае вы сохраняете в файл с определенным именем файла, метод Save () также имеет перегрузки для сохранения в System. 10. TextWriter или System. Xml .XmlWriter, что может подойти, если придется разрабатывать другую программу, в которой применяется один из этих классов для записи в файл. Метод Save () также имеет перегрузку, с помощью которой можно специфицировать SaveOptions для отключения форматирования (по умолчанию XML-документ сохраняется с отступами и пробелами для улучшения читабельности). После сохранения документа в файл он загружается в новый экземпляр XDocument по имени xdoc2: XDocument xdoc2 = XDocument.Load(xmlFileName);
Глава 29. LINQ to XML 949 Метод XDocument. Load () — статический, поскольку это метод типа фабрики, который создает новый экземпляр XDocument (); его можно использовать для загрузки документа, созданного совершенно другой программой. Затем документ отображается точно так же, как ранее, но на этот раз с использованием экземпляра xdoc2, загруженного из файла. Остальная часть программы остается такой же, как и в предыдущем примере: Console.WriteLine("Содержимое xdoc2:"); Console.WriteLine(xdoc2); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); Загрузка XML из строки Иногда вместо загрузки XML из файла он присылается другой программой в виде строки — через один или более методов. В LINQ to XML создавать XML-документы можно из строк, используя для этого метод Parse (): XDocument xdoc = XDocument.Parse (@" <customers> <customer ID=MMA"" City=""New York"" Region=""North America""> <order Item=""Widget"" Price="00"" /> <order Item=""Tire"" Price="00"" /> </customer> <customer ID=""B"" City=""Mumbai"" Region=""Asia""> <order Item=""Oven"" Price="01"" /> </customer> </customers> "); Это произведет тот же результат, что и загрузка документа из файла. Как и Load (), Parse () — метод уровня класса,-создающий новый экземпляр XDocument; конструировать новый объект XDocument перед вызовом метода Parse () не нужно. Хотя строковый литерал XML из предыдущего примера имеет удвоенные кавычки (" "), в действительном содержимом строки они не удваиваются. Удвоенные кавычки— просто соглашение, позволяющее включать кавычки в строковый литерал, помеченный символом @ в начале. Содержимое сохраненного документа XML Воспользуйтесь Internet Explorer для открытия только что сохраненного в предыдущем примере документа. Укажите в адресной строке полное путевое имя — С: \BegVCSharp\ Chapter29\Xml\example2 .xml. Вы увидите документ, как показано на рис. 29.1. 0 СЛВ^У^$Ыф\сЬ*р^29\хтЛ<в<тр*5лтт>|'" Internet E Q Ч|У1 ' CABe9VCSh*P ch«pter29\xml\ex*mpk2j<ml £М belt Х*"" rJVOOte Х****** D**P <?xml versions'l.O* encodmg=*utf-8" ?> 1 <customers> «rustomer 1D="A" Gty="Wew Yocfc* Region <order Item="Wldget" Pnce=00* /> <order Item=*Tlr*" Pnce=*200* /> 1 </customer> ccustomer ID=*B" City="Mumbai" Region= <order Item="Oven" Pnce=01' /> </customer> </customers> Dooe North America"> | Asla"> 1 Puc. 29.1. Просмотре: \BegVCSharp\Chapter29\Xml\example2.xml в Internet Explorer
950 Часть IV. Доступ к данным Обратите внимание, что объявление XML-документа <?xml version="l. 0" encoding="utf-8"?> появляется в начале сохраненного документа, даже несмотря на то, что оно не отображается, когда объект XDocument выводится на экран методом Console. Writeline (). Беспокоиться об объявлении и многих других деталях XML незачем — можно положиться на поведение по умолчанию, предлагаемое LINQ to XML. Кодировка по умолчанию XML-документов в Windows — UTF-8 (8-битный формат Unicode Transformation Format). Вам не придется изменять это за исключением очень редких ситуаций, таких как создание кодированного в ASCII документа XML, который может понадобиться для унаследованной программы, не воспринимающей UTF-8. В таком случае можно либо добавить объект XDeclaration () с кодировкой, установленной в ASCII, в начало списка параметров для конструктора XDocument (), либо установить свойство Declaration объекта XDocument: xdoc.Declaration = new XDeclaration (.0", "us-ascii", "yes"); Работа с фрагментами XML В отличие от некоторых API-интерфейсов для XML, LINQ to XML может работать с фрагментами XML (частичными или неполными документами XML) почти таким же образом, как и с полными документами XML. При работе с фрагментом, вы просто имеете дело с XElement в качестве объекта верхнего уровня вместо XDocument. Единственное ограничение к сказанному состоит в том, что вы не можете добавлять никакие другие более эзотерические типы узлов, применимые только к XDocument, вроде XComment для комментариев XML, XDeclaration для объявления документов XML и XProcessinglnstruction для инструкций обработки XML. В следующем практическом занятии вы загрузите, сохраните и будете манипулировать элементом XML и его дочерними узлами, как уже это делалось с документом XML. Практическое занятие Работа С фрагментами XML Для создания примера выполните следующие шаги в Visual C# 2008. 1. Либо модифицируйте предыдущий пример, либо создайте новое консольное приложение по имени BegVCSharp_29_3_XMLFragments в каталоге С: \BegVCSHarp\ Chapter 2 9. 2. Откройте главный файл Program, cs. 3. Добавьте ссылку к пространству имен System.Xml .Linq в начало Program, cs, как показано ниже: using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using System.Text; Если вы модифицируете предыдущий пример, эта строка уже есть. 4. Добавьте элемент XML без включающего конструктора документа XML, использованного в предыдущем примере, к методу Main () в Program, cs:
Глава 29. LINQ to XML 951 static void Main(string[] args) { XElement xcust = new XElement("customers", new XElement("customer", new XAttributeC'ID", "A"), new XAttributeC'City", "New York"), new XAttribute("Region", "North America"), new XElement("order", new XAttribute("Item", "Widget^), new XAttribute ("Price", 100) ), new XElement("order", new XAttribute("Item", "Tire"), new XAttribute("Price", 200) ) ), new XElement("customer", new XAttributeC'ID", "B"), new XAttributeC'City", "Mumbai"), new XAttribute("Region", "Asia"), new XElement("order", new XAttribute("Item", "Oven"), new XAttribute("Price", 501) ) ) ) 5. После добавления кода конструктора элемента XML на предыдущем шаге добавьте следующий код для сохранения, загрузки и отображения элемента XML: string xmlFileName = @"c:\BegVCSharp\Chapter29\Xml\example3.xml"; xcust.Save(xmlFileName); XElement xcust2 = XElement.Load(xmlFileName); Console.WriteLine("Содержимое xcust:"); Console.WriteLine(xcust); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 6. Откомпилируйте и выполните программу (можно просто нажать <F5> для запуска отладки). После этого вы должны увидеть следующий вывод к окне консоли: Contents of XElement xcust2: <customers> <customer ID="A" City="New York" Region="North America"> <order Item="Widget" Price=00" /> <order Item="Tire" Price=00" /> </customer> <customer ID="B" City="Mumbai" Region="Asia"> <order Item=ven" Price=01" /> </customer> </customers> Программа завершена, нажмите Enter для продолжения: Нажмите <Enter> для выхода из программы и очистки экрана консоли. Если использовалась комбинация <Ctrl+F5> (запуск без отладки), может понадобиться нажать <Enter> два раза.
952 Часть IV. Доступ ic данным Описание полученных результатов И XElement, и XDocument наследуются от класса LINQto XML по имени XMLContainer, который реализует узел XML, содержащий другие узлы XML. Оба класса также реализуют Load () и Save (), так что большинство операций, которые могут быть выполнены в LINQ to XML над XDocument, также могут быть выполнены на экземпляре XElement и его дочерних объектах. Вы просто создаете экземпляр XElement, который имеет ту же структуру, что и XDocument, использованный в предыдущем примере, но пропускает включающий XDocument. Все операции этой конкретной программы работают с фрагментом XElement аналогично. XElement также поддерживает методы Load () и Parse () для загрузки XML из файлов и строк, соответственно. Генерация XML из LINQ to SQL Часто XML применяется для передачи данных между клиентской и серверной машинами, либо между "слоями" многоуровневого приложения. Довольно часто приходится запрашивать одни и те же данные из базы данных, а затем производить документ XML или его фрагмент из этих данных для передачи его на другой уровень. В следующем практическом занятии вы создадите запрос для нахождения некоторых данных в базе Northwind, воспользуетесь LINQ to SQL для запроса этих данных, а затем примените LINQ to XML для преобразования их в XML. Пример требует наличия базы данных примеров Northwind, описанной в начале главы 27. J^le°K^_3a_H™~ Генерация ХмГиз LINQ to SQL Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новое консольное приложение по имени BegVCSharp_2 9_4_XMLFrom LINQtoSQL в каталоге C:\BegVCSharp\Chapter29. 2. Как описано в шагах 3-12 практического занятия "Первый запрос LINQto SQL' в начале главы 27, добавьте к проекту класс LINQto SQL по имени Northwind. dbml, а затем добавьте соединение с базой данных Northwind. 3. Перетащите таблицы Customers, Orders и Order Details на поверхность проектирования O/R Designer Northwind.dbml. Внешний вид O/R Designer показан на рис. 29.2, где присутствуют три таблицы с их отношениями. 4. Откомпилируйте программу, чтобы классы и свойства, определенные в Northwind.dbml, были доступны через средство IntelliSense при редактировании кода в последующих шагах. 5. Откройте главный файл Program, cs. 6. Добавьте ссылку на пространство имен System.Xml .Linq в начало Program, cs, как показано ниже: using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using System.Text;
Глава 29. LINQ to XML 953 Ц B«jVCSh*rp.»* Ш 14» i«* £ro|ect fio.M Debug D|ti Гемм» Bl Щщт^т ' .* CuMomtrtO V CcnpanyNeme 7Я1 Cont*ctN*m« 9 ConttctTitt» 1 1 Aoo<«« ! ffC* ---«>:- •jf PoKdCock '3* Country 1 Phcn« •*** v ) • .j Stoted Pro<cduf« к .j function-. . _J Synonym» t ^i Type * Си«оп<«»ГО "Я* Smptoy«eID *3" OfdetCW* " R«qu<f«dO«tc 3f Sn.pptdO»» If 9t.pV.« tf Sh'oN*m« * Sh.pAdo>*« ^f Sn.pCrty * Sh.pR^n "З1 Sn.pPo»t*<ede 4 ShipCcuntr, • [(Mar.DaUl 1 Prop***» J *Зр0тЧ«Н0 j « 2f PfOductlD 3f UnnPnce 3f Quantity гУ amount Рис. 29.2. Таблицы Customers, Orders и Order Details и отношения между ними 7. Добавьте следующий код в метод Main () в Program, cs: static void Main(string [ ] args) { NorthwindDataContext northWindDataContext = new NorthwindDataContext(); XElement northwindCustomerOrders = new XElement("customers", from с in northWindDataContext.Customers select new XElement("customer", new XAttribute("ID", c.CustomerlD), new XAttribute("City", c.City), new XAttribute("Company", c.CompanyName), from о in c.Orders select new XElement("order", new XAttribute("orderlD", o.OrderlD), new XAttribute("orderDay", o.OrderDate.Value.Day), new XAttribute("orderMonth", o.OrderDate.Value.Month), new XAttribute("orderYear", o.OrderDate.Value.Year), new XAttribute("orderTotal", o.Order_Details.Sum(od = > od.Quantity * od.UnitPrice)) ) // конец order ) // конец customer ) ; // конец customers string xmlFileName = @"C:\BegVCSharp\Chapter29\Xml\NorthwindCustomerOrders.xml"; northwindCustomerOrders.Save(xmlFileName); Console.WriteLine("Заказы для заказчиков Northwind успешно сохранены в:")
954 Часть IV. Доступ к данным Console.WriteLine(xmlFileName); Console.Write("Программа завершена, нажмите Enter для продолжения:"); Console.ReadLine(); } 8. Откомпилируйте и выполните программу (можно просто нажать <F5> для запуска отладки). После этого вы увидите следующий вывод: Заказы для заказчиков Northwind успешно сохранены в: С:\BegVCSharp\Chapter2 9\Xml\NorthwindCustomerOrders.xml Программа завершена, нажмите Enter для продолжения: Нажмите <Enter> для выхода из программы и очистки экрана консоли. Если использовалась комбинация <Ctrl+F5> (запуск без отладки), может понадобиться нажать <Enter> два раза. Описание полученных результатов Вы добавили в Program, cs ссылку на пространство имен System.Xml.Linq, чтобы вызывать конструкторы классов LINQ to XML. Как было описано в главе 27, создается соединение с базой данных Northwind и затем с помощью O/R Designer создается отображение LINQ to SQL для данных Northwind. В главной программе создается экземпляр класса контекста Northwind, чтобы использовать последующее отображение: NorthwindDataContext northWindDataContext = new NorthWindDataContext(); Запрос LINQ to SQL очень похож на тот, что применялся в практическом занятии "Дальнейшее углубление в LINQ to SQL' в главе 27. Он использует член Customers контекста данных Northwind в качестве источника данных и проходит вниз по таблицам Customers, Orders и Order Details для генерации списка всех заказов заказчика. Однако результаты запроса проектируются в конструкции select запроса на вложенные элементы и атрибуты LINQ to XML: XElement northwindCustomerOrders = new XElement("customers", from с in northWindDataContext.Customers select new XElement("customer", new XAttribute("ID", c.CustomerlD), new XAttribute("City", c.City), new XAttribute("Company", c.CompanyName), from о in c.Orders select new XElement("order", new XAttribute("orderID", o.OrderID), new XAttribute("orderDay", о.OrderDate.Value.Day), new XAttribute("orderMonth", o.OrderDate.Value.Month) , new XAttribute("orderYear", o.OrderDate.Value.Year), new XAttribute("orderTotal", o.Order_Details.Sum(od = > od.Quantity * od.UnitPrice)) ) // конец order ) // конец customer ) ; // конец customers Для извлечения всех заказов для заказчика используется второй запрос LINQ to SQL (from о in с.Orders . . .), вложенный внутрь первого (from с in northWindDataContext.Customers...).
Глава 29. LINQ to XML 955 Поле OrderDate разделяется на компоненты — месяц, дату и год, — чтобы облегчить запросы XML; в следующем примере будет показано, как это используется. И, наконец, сгенерированный XML сохраняется в файл, как и в предыдущем примере: string xmlFileName = @"C:\BegVCSharp\Chapter2 9\Xml\NorthwindCustomerOrders.xmlM; northwindCustomerOrders.Save(xmlFileName); Console.WriteLine("Заказы для заказчиков Northwind успешно сохранены в:"); Console.WriteLine(xmlFileName); Теперь просмотрим содержимое этого файла в качестве подготовки к написанию запроса по нему. Отображение XML-документа Northwind Customer Orders Откройте Internet Explorer, выберите пункт меню File^Open (Файл^Открыть), а затем введите путь к созданному XML-файлу: C:\BegVCSharp\Chapter29\Xml\ NorthwindCustomerOrders.xml. XML будет отображен в Internet Explorer, как показано на рис. 29.3. Этот документ содержит всех заказчиков из базы данных примеров Northwind вместе с их заказами. В следующем разделе будет написана программа LINQ to XML для запроса этих данных. C.\B^VCSh«p\cha0«29\*nl\noftbiMndCiisto«ncfOrd«nj№nl 6 CAf^VCSh*p\cK^«etS\«^northw«><K:Ueo .ujto/TvrOfdc^i-xml ■ lntt*T>et tJ ft » q - * - ^e«tf tfT.*- f> <?*ml version-'l.O" eneodmg-'utf-в* ?> <customers> «rustomer FD="ALFICr City=*Bertfci' Company -'Alfreds Futterfciste' > <ordor order ID ='10643' ordcrOay=*25' or dec Month ="8* orderYear ='1997* orderTotal=086.(] <order orderlD ='10692' ocderOay=*3' orderMonth ='10* orderYear =997* order 101 i 8/8.000000 <order orderlD='10702* orderDay='13' orderMonth ='10* ofderY«ar='19»7' order Total-"ЗЭО.00000О* /> <order orderlD ='10835' orderDay='15* orderMonth ='1' orderYear='1998' orflerTota! -"851.000000* f> <order or derlD ='10952* orderDay='16* orderMonth=' ord«rYear=*1996' orderTotal='491.200000' - <order orderlD=*11011" orderOay='9* orderMonth ='4' orderYear ='1998* order!otal ='960.000000 </custoroet > <customer ID="AHATT»* C"ity="t4*Klco D.F/ Company -'Ana TruJNto Emparedados у heiados' > «roider orderlD =40308* o<dert>ay='18' ordenMonth--*9* orderYear ='1996* 0rderTotaU=*88.8OOOO0" corder orderID=*10625* ord*>rDay='8* orderMonth =*6* orderYear ='1997* ord«r!otjU79.750000- <order orderlD='10759* orderOay=*28* orderMonth =*11* order Year =99 7* order!otal='320.000000' I> <order order ID ='10926' ord4rOay=*4* orderMonth =*3* ord*rY*jr=*1998' or den vtal ='514.400000 </customer> «xustome* lD=*AHTOH* Gty=164xk» O.F.* Company=*Antonk> Moreno Taqueria' <order or derlD ='10365* orde@ay=7* orderMonth='ll" orderYear ='1996* order Total-'4O3.20OOO0 /-• <order orderlD='10507* oro>fDay=*15' orderMonth =*4' orderYear=997* orderTotal ='881.250000' ':• <order order ID ='10535' ordenDay=*13' orderMonth ='5* orderYear ='1997* orderTotal ='2156.500000* f> <order orderlD='10573' orderOay=9' orderMonth ='6' orderYear =*199Г orderTot.il ='2082.000000* /> <oider orderID=*1067r orderOay=*22' orderMonth ='9* orderYear ='199Г orderTotal=*956.900000 <order otderlD='10682* orderDay='25' orderMonth ="9* orderYear='199r 0iderTotal='37S.50OOO0" • <order orderlO ='10856* orderOay='28' orderMonth='l* orderYear=*i998' orderTоtal='660.000000* </customer> <customer ID='AJtOUT* <. ity -'London* Company 'Around the Horn*> <order ordwTD ='10355' order()ay=*15* orderMonth='ll* orderY«jar='1996' orderTotaU'480.01 «rorder orderlD ='10383* orderl)ay=*16* orderMonth=2" orderYear='1996' order! otal ='899. •.order or derlD ='10453* orderOay='21* orderMonth=*2' orderYear='1997' orderTota!=53.B <order order ID ='10558* orderOay=*4* ordeiMonj.h=*6' orderYear ='1997* orderTotal='2142.9< <order order ID ='10707* orderOay=*16' orderMonth ='10' orderYear=*1997* orderTotal-'1704.0 <order orderID='10741' OJderDay='14* orderMonth=*ll' orderYear='1997* orderTotal='265.« <o«der ordertD ='10743' wderDay='17* orderMonth='ll' orderYear=*1997* orderToi.ji='336.« <order orderlD ='10768' order0ay='8* orderMonth ='12' orderYf!ar='1997* orderTotat='1477.C <order orderlD =*10793' orderOay=*24* orderMonth='12' orderYear ='1997* orderTotal='191.1 <order orderlD ='10864* orderOay=*2* 0fderMonth=*2* orderYear=•l99в• 0rderTotalrr*2e2.0O0OO0' <ofder orderiD='10920' orderOay='3* orderMonth =*3* orderYe jr='1998' orderTotal='390.000000' /> <order orderlD ='10953* orderDay=*16* orderMonth ='3* orderYear ='1998* order!оtal ='4675.000000' /> <order orderlD ='11016' ordcrDay='10' 0fderMonth='4" orderYear='l998* orderTotal='491.500000' /> </fJ'?*n***«*T> rrw (9 Computer | Protected Mode 08 I Рис. 29.3. ФайлС: \BegVCSharp\Chapter29\Xml\NorthwindCustomerOrders.xml в Internet Explorer
956 Часть ГУ. Доступ R данным Как запрашивать документ XML Зачем могут понадобиться запросы LINQ к XML-документу? Если программа получает XML, сгенерированный другой программой, вы можете искать в нем специфические элементы XML или атрибуты, чтобы определить, как его следует обрабатывать. Программу может интересовать только подмножество содержимого XML, или же нужно подсчитать элементы внутри документа, либо может понадобиться искать элементы или атрибуты, удовлетворяющие специфическому условию. Запросы LINQ— мощное решение для задач подобного рода. Чтобы сформулировать запрос к XML-документу, классы LINQ to XML, такие как XDocument и XElement, предоставляют свойства-члены и методы, которые возвращают запрашиваемые LINQ коллекции объектов LINQ to XML, содержащихся внутри документа XML или его фрагмента, представленного классом LINQ to XML. В следующем практическом занятии будут использоваться эти запрашиваемые члены-методы и свойства документа XML, созданного в предыдущем примере. Практическое занятие 3ЭПрОС К ДОКуМвНТу XML Для создания примера выполните следующие шаги в Visual C# 2008. 1. Создайте новое консольное приложение по имени BegVCSharp_29_5_QueryXML в каталоге С: \BegVCSharp\Chapter29. 2. Откройте главный файл Program.cs. 3. Добавьте ссылку к пространству имен System. Xml .Linq в начало Program, cs, как показано ниже: using System; using System.Collections.Generic; using System.Linq; using System. Xml. Linq; using System.Text; 4. Добавьте следующий код в метод Main () в Program, cs: static void Main(string[] args) { string xmlFileName = @"C:\BegVCSharp\Chapter29\Xml\ NorthwindCustomerOrders.xml"; XDocument customers = XDocument.Load(xmlFileName); Console.WriteLine("Элементы в загруженном документе:"); var gueryResult = from с in customers.Elements() select сName; foreach (var item in gueryResult) { Console.WriteLine(item); } Console. Write ("Для продолжения нажмите Enter:"); Console.ReadLine(); } 5. Откомпилируйте и запустите программу (можно просто нажать <F5> для запуска отладки). После этого вы увидите следующий вывод: Элементы в загруженном документе: customers Для продолжения нажмите Enter:
Глава 29. LINQ to XML 957 6. Нажмите <Enter> для выхода из программы и очистки экрана консоли. Если использовалась комбинация <Ctrl+F5> (запуск без отладки), может понадобиться нажать <Enter> два раза. Описание полученных результатов Как известно из объяснений каждого из методов запроса, вы модифицируете только что созданный пример запроса LINQ to XML, чтобы использовать его. Каждый из этих запросов возвращает коллекцию элементов LINQ to XML или атрибутов объектов, имеющих свойство Name, так что конструкция select просто возвращает это имя для вывода на консоль в цикле f oreach: var queryResult = from с in customers.Elements() select с Name; foreach (var item in queryResult) { Console.WriteLine(item); } Код такого типа можно использовать поначалу при разработке и отладке программы, чтобы увидеть, что возвращает запрос. Позднее вывод будет модифицирован для отображения бизнес-результатов, которые выглядят более осмысленно для конечных пользователей. Использование членов запроса В этом разделе мы рассмотрим доступные члены запроса LINQ to XML. Затем они будут испробованы все по очереди с использованием файла NorthwindCustomerOrders. xml в качестве источника данных. Elements () Первый применяемый метод запроса LINQ to XML — это член Elements () класса XDocument. Этот член также доступен в классе XElement. Elements () возвращает набор элементов первого уровня в документе XML или его фрагменте. В корректном документе XML, таком как представлен в только что созданном файле NorthwindCustomerOrders. xml, есть только один элемент первого уровня, который называется Customers: <?xml version="l.0" encoding="utf-8"?> <customers> </customers> Все прочие элементы являются дочерними по отношению к customers, поэтому Elements () возвращает только один элемент: Элементы в загруженном документе: customers Фрагмент XML может содержать множество элементов первого уровня, но обычно он более полезен для опроса дочерних элементов, которые мы рассмотрим ниже, в разделе, посвященном Descendants (). Descendants () Следующий метод запроса LINQ to XML, который мы рассмотрим — это член Descendants () класса XDocument. Этот член также доступен в классе XElement.
958 Часть ГУ. Доступ к данным Descendants () возвращает двумерный список всех дочерних элементов (на всех уровнях) в документе XML или его фрагменте. Попробуйте модифицировать пример BegVCSharp_29-5-QueryXML следующим образом: Console.WriteLine("Все дочерние элементы в документе:"); queryResult = from с in customers.Descendants() select с.Name; foreach (var item in queryResult) { Console.WriteLine(item); } Откомпилируйте пример и запустите. Вы увидите повторяющиеся имена элементов customer и order, как они находятся в документе: Все дочерние элементы в документе: customer order order customer order customer order order order Для продолжения нажмите Enter: Вывод прокрутится на экране, так что вы, возможно, не увидите его начала. Это отражает тот факт, что файл NorthwindCustomerOrders .xml содержит только одни элементы customer и order под корневым элементом customers: <?xml version=.0" encoding="utf-8"?> <customers> <customer ... [{[SPACE]}]> <order.../> <order.../> </customer> <customer...> <order.../> Вывод можно сделать более управляемым, добавив к обработке результатов LINQ- операцию Distinct(): Console. WriteLine ("Все неповторяющиеся дочерние элементы в документе:") ; var queryResult = from с in customers.Descendants() select с.Name; foreach (var item in queryResult.Distinct()) Результирующий список содержит только неповторяющиеся имена элементов: Все неповторяющиеся дочерние элементы в документе: customers customer order Для продолжения нажмите Enter:
Глава 29. LINQ to XML 959 Это очень полезно для исследования структуры документа перед началом работы с ним, но нахождение всех элементов — не та проблема, которую придется решать часто в завершенных производственных приложениях. Более распространенный сценарий состоит в том, что необходимо будет искать элементы-наследники с определенным именем. Метод Descendants () имеет перегрузку, принимающую необходимое имя элемента в качестве строкового параметра, как показано ниже: Console.WriteLine("Наследники по имени 'customer' :") ; var queryResult = from c in customers.Descendants("customer") select c.Name; foreach (var item in queryResult) // убираем Distinct () { Console.WriteLine(item); } Это возвращает только элементы customers: Наследники по имени 'customer': customer customer customer customer customer Для продолжения нажмите Enter: Ясно, что это более часто используемый запрос. Запрашивая список элементов известного типа, можно затем поискать определенные атрибуты, что будет показано далее. Для полноты картины следует знать, что LINQ to XML также предлагает метод Ancestors (), противоположный методу Descendants (), который возвращает двумерный список всех элементов, находящихся выше заданного в структуре XML-документа. Он используется не так часто, как Descendants (), потому что разработчикам свойственно начинать обработку документов XML с корневого элемента и затем переходить от него к листовым уровням дерева элементов и атрибутов. Свойство Parent, которое указывает на единственного предка уровнем выше, используется чаще. Attributes () Следующий метод запроса LINQ to XML, который мы рассмотрим — это член Attributes (). Он возвращает все атрибуты текущего выбранного элемента. Чтобы увидеть, как работает этот метод, попробуйте модифицировать пример BegVCSharp_ 29-5-QueryXML следующим образом: Console. WriteLine ("Атрибуты наследников по имени 'customer' :") ; var queryResult = from c in customers.Descendants ("customer") .Attributes () select c.Name; foreach (var item in queryResult) { Console.WriteLine (item) ; } Откомпилируйте и выполните его. Вы должны увидеть имена атрибутов элементов customer:
960 Часть ГУ. Доступ к данным Атрибуты наследников по имени 'customer': ID City Company ID City Company ID City Company ID City Company Для продолжения нажмите Enter: Опять-таки, вывод прокрутится на экране вверх. Этот запрос нашел имена атрибутов элементов customer: <customer ID= . . . City= . . . Company= . . . > <customer ID= . . . City= ... Company= . . . > <customer ID= . . . City= . . . Company= . . . > Как и в случае с методом Descendants (), можно передать определенное имя Attributes () для его поиска. Вдобавок вы можете не ограничиваться отображением только имени. Можно также отобразить и сам атрибут. Приведем пример запроса, который отображает атрибуты элемента customer по имени Company: Console.WriteLine("Атрибуты customer по имени 'Company' :") ; var queryResult = from с in customers.Descendants ("customer") .Attributes ("Company") select c; foreach (var item in queryResult) { Console.WriteLine(item); } Откомпилируйте и выполните его. Вы увидите атрибуты, содержащие наименования компаний элементов customer: Company="Toms Spezialitaten" Company="Tortuga Restaurante" Company="Tradigao Hipermercados" Company="Trail's Head Gourmet Provisioners" Company="Vaffeljernet" Company="Victuailles en stock" Company="Vins et alcools Chevalier" Company="Die Wandernde Kuh" Company="Wartian Herkku" Company="Wellington Importadora" Company="White Clover Markets" Company="Wilman Kala" Company="Wolski Zajazd" Для продолжения нажмите Enter: А вот другой пример, на этот раз, с элементами order и атрибутом orderYear:
Глава 29. LINQ to XML 961 Console.WriteLine("Атрибуты order по имени 'orderYear' :") ; var queryResult = from с in customers.Descendants("order").Attributes("orderYear") select c; foreach (var item in queryResult) { Console.WriteLine(item); } Откомпилируйте и выполните его. Вы увидите следующий вывод: orderYear=998" orderYear=998" orderYear=998" orderYear=996" orderYear=997" orderYear=997" orderYear=998" orderYear=998" orderYear=998" orderYear=998" Для продолжения нажмите Enter: Можно также получить специфические значения атрибута (здесь— year) через свойство Value: Console.WriteLine("Значения атрибутов order no имени 'orderYear':"); var queryResult = from с in customers.Descendants("order").Attributes("orderYear") select сValue; foreach (var item in queryResult) { Console.WriteLine(item); } Откомпилируйте и выполните его. Вы увидите следующий вывод: 1996 1997 1997 1998 1998 1998 1998 Для продолжения нажмите Enter: Теперь можно задавать специфические вопросы: например, в каком самом раннем году был размещен первый заказ? Ответить на это можно с помощью того же запроса, но применить агрегатную операцию Min () к результату, вместо обычного цикла foreach: var queryResult = from c in customers.Descendants("order").Attributes("orderYear") select c.Valued- Console. WriteLine ("Самый ранний год, в котором были размещены заказы: {0}", queryResuits.Min()); Откомпилируйте и выполните это. Вы увидите ответ— 1996: Самый ранний год, в котором были размещены заказы: 1996 Для продолжения нажмите Enter:
962 Часть IV. Доступ к данным Можете потренироваться в формулировании и более специфичных вопросов на основе примеров настоящей главы. Резюме На этом мы завершаем исследование LINQ to XML. Как вы убедились, LINQ to XML интегрирует концепцию LINQ в легкий для применения альтернативный XML API, позволяющий быстро встраивать XML в программы, использующие LINQ. Это делает запросы XML-документов простыми и естественными для программистов, уже знакомых с LINQ в его других формах. В этой главе вы изучили, как строятся XML-документы с помощью функциональных конструкторов, а затем — как загружать и сохранять документы с помощью LINQ to XML. Вы освоили применение LINQ to XML для работы с неполными документами XML (фрагментами), а также узнали, как легко сгенерировать XML-документ на основе запросов LINQ to SQL или LINQ to Objects. И, наконец, вы узнали, как опрашивать существующие XML-документы посредством LINQ to XML, и применять расширенные средства LINQ, такие как агрегатные операции LINQ с LINQ to XML. Упражнения 1. Создайте следующий XML-документ, используя конструкторы LINQ to XML: <employees> <employee ID=001" FirstName="Fred" LastName="Lancelot"> <Skills> <Language>C#</Language> <Math>Calculus</Math> </Skills> </employee> <employee ID=002" FirstName=MJerry" LastName="Garcia"> <Skills> <Language>French</Language> <Math>Business</Math> </Skills> </employee> </employees> 2. Напишите запрос к файлу NorthwindCustomerOrders .xml, который вы создали, чтобы найти самых старых заказчиков (с заказами, размещенными в первый год операций, хранимых в Northwind — в 1996). 3. Напишите запрос к файлу NorthwindCustomerOrders .xml для нахождения заказчиков, которые размещали индивидуальные заказы на сумму свыше $10 000. 4. Напишите запрос к файлу NorthwindCustomerOrders .xml для нахождения времени существования самых активных заказчиков, например, компаний, общая сумма заказов которых превышает $100 000.
ЧАСТЬ Дополнительные технологии В ЭТОЙ ЧАСТИ... Глава 30. Атрибуты Глава 31. XML-документация Глава 32. Передача данных по сети Глава 33. Введение в GDI+ Глава 34. Windows Presentation Foundation Глава 35. Windows Communication Foundation Глава 36. Windows Workflow Foundation v
30 Атрибуты Эта глава посвящена атрибутам, и в ней описано, что они собой представляют и для чего их можно использовать. Глава содержит также несколько примеров, демонстрирующих применение нескольких атрибутов, доступных в .NET Framework. Пользовательские атрибуты — атрибуты, которые можно создавать самостоятельно для расширения возможностей системы — также нашли свое отражение, как и несколько работающих примеров. Вы узнаете также, как использовать дизассемблер промежуточного языка (Intermediate Language Disassembler (Ildasm)) для выяснения атрибутов существующих сборок. Атрибуты — одно из наиболее полезных свойств каркаса .NET Framework, и Microsoft часто их применяет. Чтобы эффективно их использовать, придется затратить значительное время, но результат окупает затраченные усилия. В частности, в этой главе будут рассматриваться следующие темы. □ Использовать атрибуты для определения фрагментов кода, которые входят только в отладочные сборки. □ Использовать атрибуты для определения информации о сборке, такой как информация об авторских правах. □ Использовать атрибуты для пометки фрагментов кода как устаревших, чтобы со временем сборки можно было усовершенствовать. □ Создавать собственные атрибуты и использовать их для отслеживания хронологии изменений. В последнем разделе этой главы подробно описано создание собственных атрибутов, которые расширяют функциональные возможности системы, и приведен работающий пример пользовательского атрибута, который можно применять для отслеживания хронологии изменения кода. После прочтения этой главы вы должны располагать достаточными знаниями об атрибутах, чтобы применять их в собственных проектах.
Глава 30. Атрибуты 965 Что такое атрибут Понятие атрибутов трудно определить одним предложением — с ними лучше ознакомиться, исследуя способы их использования. Приводя простое определение атрибута, можно сказать, что это дополнительная информация, которая может быть применена к фрагментам кода внутри сборки — таким как класс, метод или свойство. Эта информация доступна любому другому классу, который использует сборку. практическое занятие Использование атрибутов сборки При создании проекта .NET программа Visual Studio добавляет в него файл Assemblylnf о. cs. Рассмотрим этот файл. Создайте новое Windows-приложение AttributePeek в папке C:\BegVCSharp\Chapter30 и откройте проводник решений. Разверните узел Properties (Свойства), как показано на рис. ЗОЛ. I Solution Explorer • Solution 1 Attribut... -r -c X I I 13 Solution 01 AttributePeek A project) j3 01 Attribute*eefc w Properties щ Assembrylnfo.cs ♦ _J Resources.resx ф di Settings.settmgs ♦ ^ References (f) ц§1 Forml.cs \£) Program.cs Рис. 30.1. Развернутый узел Properties Дважды щелкните на файле AssemblyInfo.cs, чтобы увидеть код, сгенерированный Visual Studio. Часть этого кода имеет следующий вид: using System.Reflection; using System.Runtime.CompilerServices; // // Управление общей информацией о сборке осуществляется посредством следующего // набора атрибутов. Измените значения этих атрибутов, чтобы изменить // информацию, связанную со сборкой. // [assembly: AssemblyTitle("AttributePeek")] [assembly: AssemblyDescription ("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("MorganSkinner.com")] [assembly: AssemblyProduct("AttributePeek")] [assembly: AssemblyCopyright("Copyright Morgan Skinner 2008")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] Данный файл содержит ряд строк, начинающихся с [assembly: — это определения атрибутов. При компиляции файла любые определенные атрибуты сохраняются в результирующей сборке — этот процесс называют декапированием (pickling). Чтобы увидеть его в действии, измените один из приведенных выше атрибутов — например, атрибут AssemblyTitle и скомпилируйте сборку: [assembly: AssemblyTitle("AttributePeek - by ME")]
966 Часть V. Дополнительные технологии По завершении компиляции щелкните правой кнопкой мыши на сборке (которую можно найти в каталоге \bin\Debug проекта) в окне проводника Windows и в контекстном меню выберите опцию Properties (Свойства). Вкладка Details (Подробно) окна свойств в среде Windows Vista показана на рис. 30.2. Поле File Description (Описание файла) содержит описание, хранящееся в атрибуте AssemblyTitle. • J 01 AttributePeetei* Properties 15S1 Property Description Fie dejcnplion Type Rtevemon Product name Product vemon Size DetemooVwd Language Vtfue «ttrixiePeek b> ME Appicabon 1000 AtthbutePeek 1000 lyr^T^TXJiTrri'ijTrTnjIHBB 700 KB 27/10/2007 22:11 Language Neutral РОТГО PrcPfffol tTYJ Pffyaj НР!"Х№П r^ni Рис. 30.2. Вкладка Details Атрибуты сборок и соответствующие им имена свойств на вкладке Details приведены в табл. 30.1. Таблица 30.1. Атрибуты сборок и имена свойств на вкладке Details Атрибут Имя свойства AssemblyTitle AssemblyTrademark AssemblyFileVersion AssemblyCopyright AssemblyProduct File description (Описание файла) Legal trademarks (Правомочные торговые марки) File version and product version (Версия файла и версия продукта) Copyright (Авторские права) Product name (Название продукта) Возможно, вы обратили внимание, что список атрибутов, доступных на странице свойств сборки, короче списка атрибутов, определенных внутри сборки — одним из таких примеров служит атрибут AssemblyConf iguration. Компания Microsoft отобразила некоторые из наиболее часто используемых атрибутов на странице свойств Details, но чтобы обратиться к другим атрибутам, придется либо создать определенный код (как показано в разделе "Рефлексия"), либо использовать Ildasm или утилиту Reflector. Чтобы выяснить все атрибуты данной сборки, можно применить Ildasm для исследования сборки и нахождения определенных в ней атрибутов. Чтобы добавить ILDASM
Глава 30. Атрибуты 967 в качестве инструмента, используемого внутри Visual Studio 2008, вызовите опцию External Tools (Внешние инструменты) меню Tools (Сервис), щелкните на кнопке Add (Добавить), а затем установите ILDASM в качестве заголовка и С: \Program Files\ Microsoft SDKs\Windows\v6.0A\bin\ildasm.exe в качестве исполняемой команды. После того, как этот инструмент добавлен в меню инструментальных средств, можно открыть программу Ildasm и выбрать нужную сборку с помощью диалогового окна File Open (Открытие файла). Двойной щелчок на выделенном разделе MANIFEST (Манифест) ведет к открытию второго окна, содержащего манифест сборки. Прокрутив содержимое файла, вы обнаружите ряд странно выглядящих строк кода — это код на промежуточном языке (Intermediate Language — IL), созданный компилятором С#: .assembly AttributePeek { .custom instance void [mscorlib]Systern.Refleetion.AssemblyTitleAttribute::.ctor(string) = ( 01 00 15 41 74 74 72 69 62 75 74 65 50 65 65 6B // ...AttributePeek 20 2D 20 62 79 20 4D 45 00 00 ) // - by ME. . .custom instance void [mscorlib]Systern.Refleetion.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 20 43 6F 70 79 72 69 67 68 74 20 C2 A9 20 // . . Copyright .. 4D 6F 72 67 61 6E 20 53 6B 69 6E 6E 65 72 20 32 // Morgan Skinner 2 30 30 37 00 00 ) // 007.. .hash algorithm 0x00008004 .ver 1:0:0:0 } Просматривая файл, вы обратите внимание на ряд объявлений, напоминающих объявления типа: [mscorlib]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 15 41 74 74 72 69 62 75 74 65 50 65 65 6B // ...AttributePeek Введенный атрибут AssemblyTitle сохраняется в манифесте сборки — если применить таблицу преобразования шестнадцатеричных значений в ASCII, легко убедиться, что символы, следующие за 01 00 15, представляют ASCII-коды текста AttributePeek. Просто для информации, скажу, что первые два байта 01 00 представляют двухбайтовый идентификатор, а 15 — длину строки B1 символ). Последнее значение может быть двухбайтовым, если длина данных превышает 256 символов. Как уже было сказано, этот процесс сохранения атрибута внутри сборки называют декапированием, с которым можно ознакомиться, просматривая теоретические материалы по .NET, доступные в Web. Обратите внимание, что в приведенном фрагменте кода из файла Assemblylnf о. cs использован член AssemblyTitle, в то время как в приведенном коде IL он представлен как AssemblyTitleAttribute. Вначале компилятор С# ищет класс атрибута AssemblyTitle и, не найдя его, затем добавляет слово Attribute и повторяет поиск. Поэтому, как бы ни было введено имя класса — полностью или без суффикса Attribute — в результате будет сгенерирован один и тот же код. В примерах этой главы суффикс Attribute опущен. Объявление атрибута, сохраненное (декапированное) в манифесте, подозрительно напоминается объект и его конструктор. Байты, заключенные в скобки — параметры, переданные конструктору.
968 Часть V. Дополнительные технологии Теперь, когда вам знакомы теоретические основы, должно быть понятно следующее определение атрибута. Атрибут — это класс, который внутри сборки может содержать дополнительные данные, касающиеся сборки или любого типа внутри нее. Учитывая, что атрибут является классом, и что в манифесте атрибут хранится в вышеприведенном формате, снова рассмотрим определение атрибута, показанное в начале главы: [assembly: AssemblyTitle("AttributePeek - by ME")] Синтаксис этой строки несколько отличается от обычного синтаксиса С#, поскольку атрибут заключен в квадратные скобки. Дескриптор assembly: определяет область действия атрибута (которая освещена далее в этой главе), а остальная часть кода объявляет атрибут. Атрибут AssemblyTitle обладает конструктором, который принимает только один аргумент — строковое значение. Компилятор включает это значение в сборку. Доступ к этому значению можно получить через стандартную страницу Properties (Свойства) проводника Windows, просматривая сборку внутри Ildasm, или программно с помощью рефлексии, которая рассматривается в следующем разделе. Кроме простых атрибутов, связанных с информацией о сборке, каркас .NET Framework определяет несколько сотен атрибутов, используемых для выполнения таких разнообразных задач, как отладка, управление поведением элемента управления во время разработки и множества других. В последующих разделах вы ознакомитесь с некоторыми из стандартных атрибутов, а затем научитесь дополнять каркас .NET собственными пользовательскими атрибутами. Рефлексия Если вы не знакомы с основами языка Java, вероятно, понятие рефлексии является для вас новым, поэтому было решено посвятить несколько страниц ее определению и возможному использованию. Рефлексия позволяет программно исследовать сборку и получать информацию о ней, в том числе информацию обо всех содержащихся в ней типах объектов. Эта информация охватывает атрибуты, добавленные к этим типам. Классы рефлексии определены внутри пространства имен System.Reflection. Кроме считывания типов, определенных внутри данной сборки, посредством служб System.Reflection.Emit или System.CodeDom можно также генерировать (порождать) собственные сборки и типы. Эта тема несколько сложна для книги по основам С#, но те, кого она интересует, могут найти информацию по порождению динамических сборок в MSDN. Первый пример этого раздела посвящен исследованию сборки и отображению списка всех определенных в ней атрибутов — в результате должен отобразиться список, подобный приведенному ранее. В этой главе формату примеров кода уделено несколько меньше внимания, поскольку к настоящему моменту читатели должны уже достаточно хорошо осознавать выполняемые ими действия! Весь код можно найти в папке загружаемого кода для главы 30. Некоторые примеры этой главы содержат лишь наиболее важные фрагменты кода, поэтому для получения полного представления о нем обязательно просмотрите весь загружаемый код. Код первого примера можно найти в каталоге Chapter30\02 FindAttributes. Ниже приведено полное содержимое исходного файла: // Импорт типов из сборок System и System.Reflection using System; using System.Reflection;
Глава 30. Атрибуты 969 namespace FindAttributes { class Program { /// <summary> /// Главная точка входа .ехе /// </summary> /// Аргументы командной строки < param name="args" > - имя сборки < /param > static void Main(string[] args) { // Информация об использовании выходных данных, если это необходимо, if (args.Length == 0) Usage () ; else if ( (args.Length ==1) & & (args[0] == "/?")) Usage () ; else { // Циклический просмотр аргументов, переданных консольному приложению. // Это сделано, поскольку при указании полного имени пути, содержащего // пробелы, это приведет к генерированию нескольких аргументов - // они всего лишь снова объединяются в одно имя файла. . . StringBuilder sb = null; for (int pos = 0; pos < args.Length; pos++) { if (null == sb) sb = new StringBuilder(args[pos]); else sb.AppendFormat (" {0}", args[pos]); } string assemblyName = sb.ToString (); try { // Попытка загрузки указанной сборки. Assembly a = Assembly.LoadFrom(assemblyName); // Выяснение атрибутов, определенных в сборке. // Параметр игнорируется, поэтому я выбрал значение true. object[] attributes = a.GetCustomAttributes(true); // Если какие-то атрибуты были определены. . . if (attributes.Length > 0) { Console.WriteLine ("Assembly attributes for '{0}'...", assemblyName); // Console.WriteLine("Атрибуты сборки для '{0}'...", // assemblyName); //Их следует выгрузить... foreach (object о in attributes) Console.WriteLine (" {0}", o.ToString ()); } else Console.WriteLine("Assembly {0} contains no Attributes.", assemblyName); // Console.WriteLine("Сборка {0} не содержит атрибутов.", // assemblyName); }
970 Часть V. Дополнительные технологии catch (Exception ex) { Console.WriteLine("Exception thrown loading assembly {0}...", assemblyName); // Console.WriteLine("Исключение при загрузке сборки {0}...", // assemblyName); Console.WriteLine(); Console.WriteLine(ex.ToString() ) ; } } } /// <summary> /// Отображение информации по использованию .ехе. /// </summary> static void Usage() { Console.WriteLine("Usage:") ; Console.WriteLine(" FindAttributes <Assembly> "); // Console.WriteLine("Использование:"); // Console.WriteLine(" FindAttributes <Сборка> "); } } } Чтобы запустить приложение FindAttributes, необходимо указать имя сборки, которую нужно исследовать. Пока можно использовать саму сборку FindAttributes. ехе, показанную на рис. 30.3. Рис. 30.3. Запуск приложения FindAttributes для сборки FindAttributes. ехе Код примера вначале проверяет параметры, переданные в командной строке — при отсутствии таких параметров, или если пользователь вводит строку FindAttributes /?, код вызывает метод Usage (). Этот метод отображает краткую простую справку по использованию: if (args.Length == 0) Usage () ; else if ( (args.Length ==1) & & (args[0] == "/?")) Usage () ;
Глава 30. Атрибуты 971 Затем код преобразует аргументы командной строки в единую строку. Это связано с тем, что имена каталогов часто содержат пробелы, подобно каталогу Program Files, в результате чего имя воспринимается как два аргумента. Поэтому код циклически просматривает все аргументы, объединяя их в одну строку, которая используется в качестве имени сборки, предназначенной для загрузки: StringBuilder sb = null; for (int pos = 0; pos < args.Length; pos++) { if (null == sb) sb = new StringBuilder(args[pos]); else sb.AppendFormat (" {0}", args[pos]); } string assemblyName = sb.ToString (); Затем код предпринимает попытку загрузки сборки и получения с помощью метода GetCustomAttributes () всех определенных в ней пользовательских атрибутов: Assembly a = Assembly.LoadFrom (assemblyName); // Выяснение атрибутов, определенных в сборке, object[] attributes = a.GetCustomAttributes(true); Любые найденные атрибуты выводятся на консоль. Применение программы к файлу FindAttributes .ехе привело к отображению атрибута DebuggableAttribute. Хотя он и не был указан, компилятор С# добавил его и, как легко убедиться, любые исполняемые файлы, скомпонованные для отладки, содержат этот атрибут. Чтобы просмотреть атрибуты данной сборки, необходимо либо запустить пример из командной строки, либо изменить параметры командной строки, добавив в их число сборку, которую нужно исследовать. Позднее в этой главе мы вернемся к теме рефлексии, чтобы выяснить, как можно получить атрибуты классов и методов, определенных внутри сборки. Встроенные атрибуты В предшествующих разделах было показано, что каркас .NET Framework включает в себя ряд атрибутов, таких как DebuggableAttribute и AssemblyTitleAttribute. Данный раздел посвящен некоторым часто применяемым атрибутам, определенным в .NET Framework, и ситуациям, в которых может требоваться их использование. В этом разделе освещены следующие атрибуты: □ System.Diagnostics.ConditionalAttribute □ System.ObsoleteAttribute □ System.SerializableAttribute □ System.Reflection.AssemblyDelaySignAttribute Более подробную информацию о других атрибутах, поставляемых с .NET Framework, можно найти в документации по .NET Framework SDK. Еще один чрезвычайно полезный инструмент для работы с каркасом .NET — программа Reflector, которую можно загрузить с сайта www. aisto. com/roeder/dotnet. Это программное средство позволяет исследовать сборки и просматривать все типы, определенные внутри них. Его можно использовать, чтобы с помощью всего несколь-
972 Часть V. Дополнительные технологии ких щелчков кнопкой мыши найти все классы, производные от System.Attribute. Это средство одно из тех, которым определенно не стоит пренебрегать. Кроме того, Reflector позволяет выполнять декомпиляцию методов и преобразовывать код IL в исходный текст С#, что может быть очень полезно при анализе "внутренней механики" определенного кода .NET. System.Diagnostics.ConditionalAttribute Это один из наиболее полезных атрибутов, поскольку он позволяет включать или исключать фрагменты кода в зависимости от определения символа во время компиляции. Этот атрибут определен в пространстве имен System.Diagnostics, которое включает классы, предназначенные для отладки и трассировки вывода, регистрации событий, счетчиков производительности и получения информации о процессах. Использование этого атрибута демонстрируется в следующем практическом занятии. using System; using System.Diagnostics; namespace TestConditional { class Program { static void Main(string[] args) { // Вызов метода, доступного, только если символ DEBUG определен... Program.DebugOnly (); } // Этот метод снабжен атрибутом и будет включен в // порожденный код, ТОЛЬКО если символ DEBUG определен //во время компиляции программы. [Conditional("DEBUG")] public static void DebugOnly() { // Эта строка будет отображаться только в отладочных компоновках... Console.WriteLine("This string only displays in Debug"); } } } Исходный код этого примера доступен в каталоге Chapter30/03 Test Conditional. Код вызывает статический метод DebugOnly (), снабженный атрибутом Conditional, как видно из раздела [Conditional ("DEBUG") ] приведенного кода. Эта функция всего лишь выводит строку текста на консоль. При компиляции исходного файла С# символы можно определять в командной строке. Атрибут Conditional ведет к пропуску обращений к коду, помеченному соответствующим символом на этапе компиляции. Поэтому при наличии метода, помеченного как [Conditional ("DEBUG") ], вызовы этого метода существуют только, если символ DEBUG определен во время компиляции. При компиляции отладочной (Debug) версии в среде Visual Studio 2008 символ DEBUG устанавливается автоматически. Если нужно определить или переопределить символы для конкретного проекта, откройте диалоговое окно Properties проекта и перейдите к опции Build (Компоновка) страницы Configuration Properties (Свойства конфигурации), как показано на рис. 30.4.
Глава 30. Атрибуты 973 Application Build Build Events Debug Resources Services Settings Reference Paths Configuration: [Active flkbuQ) Conditional compilation symbols: / Define DEBUG constant У Define TRACE constant Platform target Allow unsafe code Optimize code »[ Platform: \Лл*тУ*у 1 [Any CPU -| u»u> •1 Рис. 30.4. Страница Build свойств проекта Обратите внимание, что по умолчанию для компоновки Debug определены оба символа DEBUG и TRACE. Чтобы определить символ в командной строке, используйте переключатель /d: (сокращенная форма записи переключателя /define: — при желании эту строку можно вводить полностью): > esc /d:DEBUG TestConditional.es Если скомпилировать и запустить файл, на консоли отобразится строка вывода This string only displays in Debug (Эта строка отображается только в режиме отладки). Если назначение компоновки изменить на Release и выполнить приложение, эта строка не будет отображаться в окне консоли. Чтобы получить более ясное представление о происходящем, воспользуйтесь программой Ildasm для просмотра сгенерированного кода (рис. 30.5). т^щ Р ХЛОЗ TestCcwxJrtKKwATestCondrbon^VbHiXDeb.. | - ишдыв шшт тттттчтт File View Help ► MANIFEST Щ TestCorxkional ■ £ TestCorxkional.Program ► .class private auto ansi beforefiekimr. ■ .«or : votd() J DeburjOnry : vokK) J Mar : voxKstrrxXD assembly TestCondUonai — Puc. 30.5. Использование Ildasm для просмотра сгенерированного кода Когда символ DEBUG не определен, код IL, сгенерированный для метода Main () дмеет следующий вид: .method private hidebysig static void Main(string [ ] args) cil managed { .entrypoint // Размер кода 1 (Oxl) .maxstack 8 IL_0000: ret } // конец метода Program::Main Этот код просто выполняет возврат из метода Main.
974 Часть V. Дополнительные технологии Однако при компиляции файла с использованием переключателя /d: DEBUG генерируемый код будет иметь следующий вид: .method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Размер кода 8 @x8) .maxstack 8 IL_0000: nop IL_0001: call void TestConditional. Program: :DebugOnly () IL_0006: nop IL_0007: ret } // конец метода Program::Mam Выделенная строка — вызов условно скомпилированного метода. Применение метода Conditional () удаляет вызовы метода, но не сам метод. Атрибут Conditional можно использовать только с методами, которые возвращают значение void — в противном случае удаление вызова означало бы, что никакое значение не было возвращено. Однако этим атрибутом можно пометить метод, обладающий параметрами out или ref — переменные сохранят свои исходные значения. Итак, подытожим: атрибут Conditional ведет к пропуску вызовов тех методов, которые помечены этим атрибутом, если соответствующий символ определен во время компиляции. Класс Debug из пространства имен System. Diagnostics содержит ряд статических методов, снабженных атрибутом [Conditional ("DEBUG") ], которые используются для вывода отладочной информации во время выполнения приложения. При переключении на окончательную компоновку все вызовы этих методов опускаются из результирующей сборки. Это — прекрасный способ добавления отладочной информации и автоматического ее удаления при компоновке окончательной версии программного продукта. System. ObsoleteAt tribute Этот атрибут ориентирован на детали, которые разработчики Microsoft помещают в каркас .NET Framework. Атрибут Obsolete можно использовать для пометки класса, метода или любого другого компонента сборки как больше не используемого. Например, он может быть полезен при публикации библиотеки классов. В ходе разработки набора классов некоторые из этих классов/методов/свойств неизбежно окажутся заменены более совершенными. Этот атрибут можно использовать для подготовки пользователей кода к возможному удалению конкретной функциональной возможности. Предположим, что первая версия приложения содержит класс, подобный следующему: public class Coder { public Coder () { } public void CodelnCPlusPlus () { } }
Глава 30. Атрибуты 975 Этот класс успешно используется в течение ряда лет, но затем появляется некая новая функциональная возможность, приходящая на смену старой: public void CodelnCSharp () { } Естественно, желательно, чтобы пользователи вашей библиотеки в течение некоторого времени продолжали применять метод CodelnCPlusPlus О , но хотелось бы поставить их в известность о наличии более нового метода, отображая во время компиляции предупреждающее сообщение, информирующее о существовании метода CodelnCSharp (). Для этого достаточно добавить атрибут Obsolete, как показано в следующем примере: [Obsolete("CodelnCSharp instead.")] public void CodelnCPlusPlus () { } Теперь при компиляции для отладки или создания окончательной версии компилятор С# отобразит предупреждение об использовании устаревшего (или вскоре ставшего таковым) метода, как показано на рис. 30.6. Обычно предупреждения не будут останавливать работу компилятора, если только не установить опцию Treat warnings as errors (Обрабатывать предупреждения как ошибки) на вкладке Build страницы настроек конфигурации проекта — эту вкладку можно открыть, выбирая пункт меню Properties проекта. Рис. 30.6. Предупреждение об использовании устаревшего метода Со временем пользователям надоест читать это предупреждающее сообщение при каждой компиляции кода, поэтому постепенно все (или почти все) пользователи начнут использовать метод CodelnCSharp (). Рано или поздно вы решите полностью отказаться от поддержки метода CodelnCPlusPlus (), поэтому к атрибуту Obsolete нужно добавить дополнительный параметр: [Obsolete ("You must CodelnCSharp instead. " , true)] public void CodelnCPlusPlus () { } В этом случае при попытке компиляции кода, который вызывает этот метод, компилятор будет генерировать ошибку и прекращать компиляцию с выводом сообщения об ошибке. Использование этого атрибута предоставляет пользователям класса помощь по изменению приложений, использующих данный класс, по мере его совершенствования. Этот способ управления версиями не слишком подходит для двоичных классов, таких как компоненты, приобретенные без исходного кода — каркас .NET Framework предлагает прекрасные встроенные возможности управления версиями. Однако атрибут Obsolete предоставляет удобный способ сообщения о том, что конкретная функциональная возможность классов больше не должна использоваться.
976 Часть V. Дополнительные технологии System. SerializableAttribute Сериализация (serialization) — это термин, обозначающий способ хранения и извлечения объекта из дискового файла, памяти или любого другого источника. Когда объект сериализован, все данные экземпляра постоянно сохраняются на носителе хранилища, а если он десериализован, объект реконструируется и по существу совпадает с исходным экземпляром. Любой, кому раньше доводилось программировать с применением MFC, ATL или VB и нужно было беспокоиться о сохранении и извлечении данных экземпляров, убедится, что этот атрибут значительно уменьшит объем ввода с клавиатуры. Предположим, что существует класс С#, подобный следующему, который нужно хранить в файле: public class Person { public Person () { } public int Age { get; set; }; public int WeightlnPounds { get; set; }; } В С# (и в любом языке, построенном на основе .NET Framework) члены экземпляра можно сериализовать без написания — или почти без написания — какого-то кода. Достаточно добавить к классу атрибут Serializable, и модуль времени выполнения .NET выполнит все остальные необходимые действия. Когда модуль времени выполнения получает запрос на сериализацию объекта, он проверяет, реализует ли класс объекта интерфейс ISerializable. Если нет, модуль времени выполнения проверяет, снабжен ли класс атрибутом Serializable. Мы не будем подробно останавливаться на интерфейсе ISerializable — скажем только, что он служит более совершенным способом изменения того, какие данные сериализуются. Если класс содержит атрибут Serializable, .NET использует рефлексию для извлечения всех данных экземпляра — общедоступных, защищенных или приватных — и сохраняет их в качестве представления объекта. Десериализация — обратный процесс. Данные считываются с носителя хранилища и присваиваются переменным экземпляров класса. Следующий пример демонстрирует класс, помеченный атрибутом Serializable: [Serializable] public class Person { public Person () { } public int Age { get; set; }; public int WeightlnPounds { get; set; }; } Полный код этого примера доступен в подкаталоге Chapter30/05 Serialize. Для хранения экземпляра этого класса Person необходимо использовать объект Formatter — этот объект преобразует данные, хранящиеся внутри класса, в поток байтов. Система оснащена двумя форматировщиками, используемыми по умолчанию: BinaryFormatter и SoapFormatter (которые обладают собственными пространствами имен в пространстве имен System.Runtime . Serialization. Formatters). Использование BinaryFormatter для сохранения объекта person показано в следующем примере:
Глава 30. Атрибуты 977 using System; using System.Runtime.Serialization.Formatters.Binary; using System.10; public static void Serialize () // Конструирование объекта person. Person me = new Person () ; // Установка данных, которые должны быть сериализованы. me.Age =40; те.WeightInPounds = 200; // Создание дискового файла для хранения объекта... Stream s = File.Open ("Me.dat" , FileMode.Create); // И применение BinaryFormatted для записи объекта в поток... BinaryFormatter bf = new BinaryFormatter (); // Сериализация объекта, bf.Serialize (s , me); // Закрытие потока, s.Close (); } Вначале код создает объект person (персона) и устанавливает данные Age (возраст) и Weight InPounds (вес в фунтах), а затем формирует проток в файле Me. dat. Двоичный форматировщик использует поток для сохранения экземпляра класса person в файле Me. dat, после чего поток закрывается. Используемый по умолчанию код сериализации сохраняет все общедоступное содержимое объекта, что требуется в большинстве случаев. Однако в ряде обстоятельств может требоваться определение одного или более полей, которые не должны подвергаться сериализации. Это также легко сделать — достаточно добавить атрибут [NonSerialized] к данным, которые нужно исключить из процесса сериализации. Этот атрибут применим только к полям, поэтому свойство Weight InPounds в приведенном примере изменено так, чтобы оно использовало вспомогательное поле: [Serializable] public class Person { public Person () public int Age { get; set; }; [NonSerialized] private int _weightInPounds; public int WeightlnPounds { get { return _weightInPounds; ; set { _weight!nPounds = value;
978 Часть V. Дополнительные технологии [NonSerializedO ] public int weight; public int WeightlnPounds { get { return weight; } set { weight = value; } } При сериализации этого класса только член Age будет сохранен — член WeightlnPounds сохранен не будет и, следовательно, не будет извлекаться во время десериализации. Десериализация — по существу процесс, противоположный представленному приведенным кодом сериализации. Следующий пример открывает поток для созданного ранее файла Me. dat, конструирует метод BinaryFormatter для чтения объекта и вызывает метод Deserialize для извлечения этого объекта. Затем он отображает его в объекте Person и выводит возраст и вес данного лица на консоль: public static void DeSerialize () { // Открытие файла. Stream s = File.Open ("Me.dat" , FileMode.Open); // И использование BinaryFormatted для считывания объекта (объектов) из потока. BinaryFormatter bf = new BinaryFormatter (); // Десериализация объекта, object о = bf.Deserialize (s) ; // Обеспечение правильности типа объекта... Person p = о as Person; if (p != null) Console.WriteLine ("DeSerialized Person aged: {0} weight: {1}" , p.Age , p.WeightlnPounds); // Закрытие потока, s.Close (); } Как правило, атрибут NonSerialized применяют для пометки данных, которые не нужно подвергать сериализации, таких как пересчитываемые или вычисляемые при необходимости. Примером служит класс, который вычисляет простые числа — для ускорения отклика при использовании класса вполне можно кэшировать простые числа, но сериализация и десериализация простых чисел не обязательна, поскольку их можно просто вычислить заново по запросу. В других ситуациях член может иметь значение только для конкретного применения объекта. Например, в объекте, представляющем документ текстового процессора, обычно требуется выполнять сериа- лизацию содержимого документа, но не позиции точки вставки — при последующих загрузках точка вставки просто помещается в начало документа.
Глава 30. Атрибуты 979 Если требуется еще большая степень контроля над способом сериализации объекта, можно реализовать интерфейс I Serial izable, но эта более сложная тема не нашла своего отражения в настоящей книге. System. Reflection. AssemblyDelaySignAttribute Пространство имен System.Reflection предоставляет ряд атрибутов, часть из которых рассматривались ранее в этой главе. Один из наиболее сложных для использования атрибутов — Assembly Delay Sign, который позволяет откладывать подписание сборки, чтобы ее можно было регистрировать в глобальном кэше сборок (Global Assembly Cache — GAC) для тестирования без секретного ключа. Одна из ситуаций, когда может требоваться отложенное подписание — разработка коммерческой программы. Каждая сборка, разработанная внутри компании, перед поставкой клиентам должна быть подписана секретным ключом этой компании — это предоставляет клиентам определенную гарантию, что сборка действительно было создана данной компанией. При компиляции сборки, прежде чем ее можно будет зарегистрировать в GAC, нужно указать ссылку на файл ключа. Этот файл содержит два ключа — открытый и секретный. Однако многие организации не желают, чтобы их секретный ключ хранился на компьютере каждого разработчика. Поэтому модуль времени выполнения позволяет выполнять частичное подписание сборки и изменять несколько настроек, чтобы сборку можно было регистрировать внутри GAC. После того, как сборка будет полностью протестировала, она может быть окончательно подписана держателем файла секретного ключа. Им может быть отдел контроля качества, одно или более доверенных лиц или отдел маркетинга. В следующем практическом занятии показано, как отложить подписание типичной сборки, зарегистрировать ее в GAC для выполнения тестирования и, наконец, окончательно подписать, добавляя секретный ключ. ЯВШшшшяшшвшшяшяншшшшяшшвяшшшшшяшяшшшшшшшшшшшшшшшшшшшашшшштшшшшшт Практическое занятие Отложенное ПОДПИСЭНИе 1. С помощью утилиты sn.exe создайте файл ключа. Если вы используете Express- версию Visual Studio, придется загрузить также комплект средств разработки .NET Framework 2.0 SDK, включающий в себя эту утилиту. Файл ключа будет содержать открытый и секретный ключи и, как правило, будет назван по названию компании. По принятому соглашению файлам ключей присваивается расширение .snk: > sn -k Company, snk 2. С помощью опции -р извлеките часть открытого ключа, которая будет использоваться разработчиками: > sn -p Company.snk CompanyPublic.snk Эта команда создает файл ключа CompanyPublic. snk, содержащий только открытую часть ключа. Этот файл открытого ключа может быть скопирован на все компьютеры и не нуждается в особой защите — в тайне нужно хранить только файл секретного ключа. Сохраните файл Company. snk в каком-то надежном месте, поскольку он потребуется только для окончательного подписания сборок.
980 Часть V. Дополнительные технологии 3. Чтобы выполнить отложенное подписание и зарегистрировать сборку внутри GAC, необходимо также получить маркер открытого ключа — по сути, он представляет собой сокращенную версию открытого ключа, используемую при регистрации сборок. Маркер можно получить двумя способами: • из самого файла открытого ключа: > sn -Ь CompanyPublic.snk • из сборки, подписанной файлом ключа CompanyPublic. snk: > sn -T < assembly > Оба эти метода отображают хешированную версию открытого ключа и зависят от регистра. Подробнее мы рассмотрим этот вопрос при регистрации сборки. Файл ключа, хранящийся в каталоге Chapter30/06 DelaySign этой главы, создает значение маркера открытого ключа aladla5619382d41. Это значение потребуется позднее в этой главе и на вашем компьютере оно будет иным в случае создания нового файла ключа. Отложенное подписание сборки Следующий код демонстрирует пометку сборки для отложенного подписания. Он доступен в каталоге Chapter30/06 DelaySign: using System; using System.Reflection; // Определение файла, содержащего открытый ключ, [assembly: AssemblyKeyFile ("CompanyPublic.snk") ] // И указание того, что эта сборка предназначена для отложенного подписания, [assembly: AssemblyDelaySign (true) ] public class DelayedSignmg { public DelayedSignmg () Атрибут AssemblyKeyFile определяет файл, в котором нужно искать ключ. Он может быть файлом открытого ключа или, для наиболее доверенных лиц, файлом, содержащим и открытый, и секретный ключи. Атрибут AssemblyDelaySign определяет, подписана ли сборка полностью (false) или ее подписание отложено (true). Обратите внимание, что в среде Visual Studio 2008 файл ключа и опцию отложенного подписания можно устанавливать из интерфейса пользователя, как показано на рис. 30.7 — эти опции являются составной частью окна Properties проекта. Signing Security Publish J Sign the assembly Choose a strong mme key file Code Andys» companypublicsok »| • Delay sign only When delay signed, the project will not run or be debuggable. Puc. 30.7. Установка отложенного подписания сборки
Глава 30. Атрибуты 981 После компиляции манифест сборки будет содержать запись открытого ключа. Фактически в нем будет выделено место и для секретного ключа, поэтому повторное подписание сборки не приведет к каким-либо изменениям в ней (за исключением записи нескольких дополнительных байтов в манифесте). Попытка отладки сборки приведет к отображению сообщения об ошибке, показанного на рис. 30.8. Microsoft Visual Stucfo ©Error whik try»ng to run project Could not load file or assembly I Dele.Sign. Version=lX)u.O. Сulture= neutral. II PublkKeyToken=aledla5619382d41 or one of its dependencies. Strong Щ name validation filled. (Exception from HRESULT: 0x8013141 А) I 11 сдД Рис. 30.8. Сообщение об ошибке Это обусловлено тем, что по умолчанию исполняющая среда .NET будет загружать только неподписанные сборки (не имеющие определенного файла ключа) либо запускать только полностью подписанные сборки, содержащие и открытые, и секретные ключи. По существу .NET не допускает загрузку частично подписанных сборок, что и служит причиной отображения сообщения об ошибке, показанного на рис. 30.8. В разделе, следующем за посвященным регистрации сборки в GAC, показано, как разрешить загрузку частично подписанных сборок, используя процесс пропуска подтверждения подлинности. Регистрация в GAC Попытка использования утилиты Gacutil (доступной также в .NET 2.0 Framework SDK) для регистрации частично подписанной сборки в GAC ведет к отображению сообщения об ошибке, подобного следующему: Microsoft (R) .NET Global Assembly Cache Utility. Version 3.5.20706.1 Copyright (C) Microsoft Corporation. All rights reserved. Failure adding assembly to the cache: Strong name signature could not be verified. Was the assembly built delay-signed? Ошибка добавления сборки в кеш: Не удалось проверить подлинность подписи строгого имени. Не было ли отложено подписание сборки? На данный момент сборка подписана только частично, а по умолчанию GAC и Visual Studio .NET будут принимать сборки только со строгим именем. Однако с помощью утилиты sn из .NET можно потребовать пропуск проверки надежности строгого имени сборки, снабженной отложенной подписью. Помните ранее полученный маркер открытого ключа? Именно теперь он вступает в дело: > sn -Vr *,aladla5619382d41 Эта команда разрешает каркасу .NET регистрировать в GAC любые сборки, снабженные маркером открытого ключа aladla5619382d41, и, что еще важнее для данного примера, позволяет запускать DelaySign.exe из среды Visual Studio. Ввод приведенного кода в командной строке генерирует следующее сообщение: Microsoft (R) .NET Framework Strong Name Utility Version 3.5.20706.1 Copyright (C) Microsoft Corporation. All rights reserved. Verification entry added for assembly '*,aladla5619382d41' Запись подтверждения подлинности добавлена для сборки ' *,aladla5619382d41 '
982 Часть V. Дополнительные технологии Теперь попытка установки сборки в GAC с помощью утилиты Gacutil завершается успешно. Добавляя запись проверки подлинности для сборки, не обязательно применять значение открытого ключа — с помощью следующей команды можно было бы указать возможность регистрации всех сборок: > sn -Vr * Или же можно определить конкретную сборку, вводя ее полное имя, подобно следующему: > sn -Vr DelaySign.exe Эти данные постоянно хранятся в так называемой таблице пропуска проверки подлинности, представляющей собой файл на диске. Для получения списка всех записей таблицы пропуска проверки подлинности введите следующую команду (команды чувствительны к регистру): > sn -V1 В результате на экран компьютера будет выведена следующая информация: Microsoft (R) .NET Framework Strong Name Utility Version 3.5.20706.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembly/Strong Name Users *,aladla5619382d41 All users Обратите внимание на столбец Users (Пользователи) — в нем можно определить, что данная сборка может загружаться в GAC поднабором из всех пользователей. Для получения дополнительных сведений по этой и другим возможностям именования сборок обратитесь к документации утилиты sn. ехе. Завершение строгого имени Последний этап процесса — это компиляция открытого и секретного ключей в сборку; сборку, снабженную обеими записями, называют обладающей строгим именем, и она может быть зарегистрирована в GAC без использования записи пропуска проверки подлинности. Снова используйте утилиту sn.exe, на этот раз с переключателем -R. Переключатель -R означает, что мы собираемся заново подписать сборку и добавить часть секретного ключа: > sn -R delaysign.dll Company.snk В результате отображается следующее: Microsoft (R) .NET Framework Strong Name Utility Version 3.5.20706.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembly 'delaysign.dll' successfully re-signed Сборка 'delaysign.dll' успешно подписана заново Остальные параметры, следующие за ключом -R — имя сборки, которая должна быть заново подписана, и файл ключа, содержащий открытый и секретный ключи. Пользовательские атрибуты До сих пор мы уделяли внимание некоторым атрибутам, входящим в состав .NET Framework. Однако доступные возможности не ограничиваются ими — можно создавать также собственные атрибуты.
Глава 30. Атрибуты 983 В этом разделе в качестве примера пользовательского атрибута мы создадим атрибут BugFixAttribute, который можно применять для записи в исходный код информации о том, кто внес изменения в данную область функциональных возможностей. Пользовательский атрибут — это всего лишь специальный класс, который должен соответствовать двум требованиям. □ Пользовательский атрибут должен быть производным от System. Attribute. □ Конструктор (конструкторы) атрибута могут содержать только те типы, которые могут быть подставлены во время компиляции — такие как строки и целочисленные типы. Ограничения, налагаемые на типы параметров конструкторов атрибутов, обусловлены способом хранения атрибутов в метаданных сборки. При использовании атрибута внутри кода конструктор атрибута применяется внутри его текста, как показано в следующей строке кода: [assembly: AssemblyKeyFile ("CompanyPublic.snk") ] В метаданных сборки этот атрибут присутствует в виде инструкции по вызову конструктора AssemblyKeyFileAttribute, который принимает строку. В предыдущем примере эта строка — CompanyPublic. snk. Определение пользовательского атрибута означает, что пользователи атрибута в основном записывают параметры в конструктор класса. BugFixAttribute При создании производственного кода было бы замечательно иметь возможность автоматически создавать список исправлений, внесенных между версиями приложения. Как правило, выполнение этой задачи достигается посредством просмотра базы данных ошибок для выяснения тех ошибок и усовершенствований, которые были добавлены в период между двумя датами, и последующей генерации отчета на основе полученных данных. Другой, и, возможно, более рациональный способ — внедрение сведений об изменениях в сам код, поскольку в этом случае отчет может быть создан исключительно на основе самого кода (а код обманывать не может). В этом разделе мы создадим атрибут BugFix. Еще одно преимущество включение сведений об исправленных ошибках в код — доступность этих атрибутов в скомпилированной сборке и, следовательно, возможность их получения во время выполнения. Полный исходный код этого примера доступен в каталоге Chapter30/07 BugFix. Чтобы создать класс пользовательского атрибута, необходимо выполнить перечисленные ниже действия. □ Создать класс, производный от System. Attribute. □ Создать необходимые конструктор (конструкторы) и общедоступные свойства. □ Снабдить класс атрибутом, определяющим, где можно применять пользовательский атрибут. Все эти шаги рассматриваются в последующих разделах. Создание класса пользовательского атрибута Этот шаг самый простой. Для его выполнения достаточно создать класс, производный от System.Attribute: public class BugFixAttribute : Attribute { }
984 Часть V. Дополнительные технологии Создание конструкторов и свойств Как уже было отмечено, когда пользователи используют атрибут, они, по существу, вызывают конструктор атрибута. Для атрибута BugFix вероятно требуется записать номер исправленной ошибки и определенные комментарии: using System; public class BugFixAttribute : Attribute { public BugFixAttribute (string bugNumber, string comments) { BugNumber = bugNumber; Comments = comments; } public readonly string BugNumber; public readonly string Comments; Этот код определяет единственный конструктор и две доступные только для чтения переменные-члены BugNumber и Comments. Снабжение класса атрибутом, описывающим его назначение Последний шаг, который нужно выполнить — пометка класса атрибута и указание его возможного использования. Для атрибута BugFix нужно указать, что "этот атрибут применим только к классам, свойствам, методам и конструкторам". Можно определить, где созданный атрибут допустим. Подробнее этот вопрос рассматривается далее в главе. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor, AllowMultiple = true, Inherited=false)] public class BugFixAttribute : Attribute Атрибут AttributeUsage обладает единственным конструктором, который принимает значение от перечисления AttributeTargets (подробнее оно описано далее в этом разделе). Определение атрибута использует также два свойства атрибута — AllowMultiple и Inherited. Подробнее они освещены далее в настоящем разделе. Когда определение атрибута BugFix готово, можно приступить к оснащению им кода. Следующий фрагмент демонстрирует код, содержащий ряд исправлений программных ошибок: [BugFix(01", "Created some methods")] public class MyBuggyCode { [BugFix("90125", "Removed call to base()")] public MyBuggyCode () { } [BugFix (112", "Returned a non null string")] [BugFix(8382", "Returned OK")] public string DoSomething () { return "OK"; } }
Глава 30. Атрибуты 985 Позднее в этой главе мы вернемся к атрибуту BugFix и добавим в него дополнительные функциональные возможности. System. At tributeUsageAt tribute При определении класса пользовательского атрибута необходимо определить тип или типы, с которыми его можно применять. В предыдущем примере атрибут BugFix был применим только к классам, методам, свойствам и конструкторам. Чтобы определить объекты, в которые можно помещать атрибут, необходимо использовать еще один атрибут — AttributeUsage. В простейшей форме его применение выглядит следующим образом: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property I AttributeTargets.Method I AttributeTargets.Constructor )] Единственный параметр — перечисление, определяющее объекты, в которых данный атрибут допустим. Попытка снабжения поля атрибутом BugFix приведет к генерированию компилятором сообщения об ошибке. Перечисление AttributeTargets определяет описанные в табл. SO.2 члены, которые можно объединять операцией логического or( |) для определения набора элементов, допускающих использование атрибута. Таблица 30.2. Перечисление AttributeTargets Значение перечисления 0писание AttributeTargets All Assembly Class Constructor Delegate Enum Event Field Interface Method Атрибут допустим для любого элемента внутри сборки Атрибут допустим для сборки — примером может служить ранее рассмотренный в этой главе атрибут AssemblyTitle Атрибут допустим для определения класса. Атрибут TestCase использует это значение. Еще один пример — атрибут Serializable Атрибут допустим только для конструкторов класса Атрибут допустим только для делегата Атрибут может быть добавлен к перечисленным значениям. Один из примеров этого атрибута — System. FlagsAttribute, который при применении к enum определяет, что пользователь может использовать операцию побитового or для объединения значений, указанных в перечислении. AttributeTargets enum использует ЭТОТ атрибут Атрибут допустим для определений событий Атрибут может быть помещен в поле, такое как внутренняя переменная-член. Пример такого атрибута — NonSerialized, который ранее использовался для определения того, что конкретное значение не должно сохраняться при сериализации класса Атрибут допустим для интерфейса. Один из примеров такого атрибута — GuidAttribute, определенный внутри System.Runtime. interopServices, который позволяет явно определять глобально уникальный идентификатор (GUID) интерфейса Атрибут допустим для метода. Атрибут OneWay из System. Runtime. Remoting.Messaging использует ЭТО значение
986 Часть V. Дополнительные технологии Окончание табл. 30.2 Значение перечисления AttributeTargets Module Атрибут допустим для модуля. Сборка должна быть создана из ряда модулей кода, поэтому данное значение можно использовать для помещения атрибута в отдельный модуль, а не в целую сборку Parameter Атрибут может применяться к параметру внутри определения метода Property Атрибут может быть применен к свойству ReturnValue Атрибут связан с возвращаемым значением функции Struct Атрибут допустим для структуры Область действия атрибута В ранее приведенных примерах этой главы встречались атрибуты Assembly*, синтаксически подобные следующему: [assembly: AssemblyTitle("AttributePeek")] Строка assembly: определяет область действия атрибута, которая в данном случае указывает компилятору, что атрибут AssemblyTitle должен применяться к самой сборке. Модификатор области действия нужно использовать, только если компилятор не может определить область действия самостоятельно. Например, может требоваться добавление атрибута к возвращаемому значению функции: [MyAttribute ()] public long DoSomething () Когда компилятор достигает этого атрибута, он принимает вполне обоснованное решение, что атрибут применяется к самому методу, что в данном случае не соответствует действительности. Поэтому можно добавить модификатор, явно указывающий, к чему именно присоединен атрибут: [return:MyAttnbute ()] public long DoSomething () { Чтобы определить область действия атрибута, выберите одно из перечисленных ниже значений: □ assembly — атрибут применяется к сборке; □ field — атрибут применяется к полю перечисления или класса; □ event — атрибут применяется к событию; □ method — атрибут применяется к методу, которому он предшествует; □ module — атрибут хранится в модуле; □ param — атрибут применяется к параметру; □ property — атрибут применяется к свойству; □ return — атрибут должен применятся к возвращаемому значению функции; □ type — атрибут применяется к классу, интерфейсу или структуре.
Глава 30. Атрибуты 987 Многие из этих значений применяются редко, поскольку обычно область действия атрибута не вызывает сомнений. Однако значения assembly, module'и return требуют использования флага области действия. При наличии определенных сомнений относительно того, где атрибут определен, компилятор выбирает объект, которому атрибут будет присвоен. Чаще всего это происходит при присвоении атрибута возвращаемому значению функции, как показано в следующем примере: [SomeAttribute] public string DoSomething (); В данном случае компилятор решает, что атрибут применяется к методу, а не к возвращаемому значению. Для достижения требуемого эффекта нужно следующим образом определить область действия атрибута: [return:SomeAttribute] public string DoSomething (); AttributeUsage. AllowMultiple Этот атрибут определяет возможность добавления пользователем одного или более одинаковых атрибутов к элементу. Синтаксис установки флага AllowMultiple несколько необычен. Конструктор AttributeUsage принимает только один параметр — список флагов возможного применения атрибута. AllowMultiple — свойство атрибута AttributeUsage, поэтому следующая синтаксическая конструкция означает "создать атрибут, а затем установить значение свойства AllowMultiple равным true": [AttributeUsage (AttributeTargets.Class | AttributeTargets.Property I AttributeTargets.Method | AttributeTargets.Constructor , AllowMultiple=true)] public class BugFixAttribute : Attribute { } Аналогичный метод используется для установки свойства Inherited. Если пользовательский атрибут обладает свойствами, их можно установить аналогичным образом. Возможный пример — добавление имени лица, исправившего ошибку: public readonly string BugNumber; public readonly string Comments; public readonly string Author/ public override string ToString () { if (string.IsNullOrEmpty(Author)) return string.Format ("BugFix {0} : {1}" , BugNumber , Comments); else return string.Format ("BugFix {0} by {1} : {2}" , BugNumber , Author , Comments); } Этот код добавляет поле Author и перегруженную реализацию метода ToString (), которая отображает все сведения, если поле Author установлено. Если свойство Author не определено во время присвоения атрибута коду, вывод атрибута BugFix отображает только номер ошибки и комментарии. Метод ToString () будет использоваться для отображения списка исправленных ошибок в данном фрагменте кода — возможно, для печати и записи в какой-либо файл. После того как атрибут BugFix создан,
988 Часть V. Дополнительные технологии нам потребуется какой-то способ формирования отчета об исправлениях, выполненных в классе и членах этого класса. Доступный способ создания отчета об исправленных ошибках класса — передача типа класса (в очередной раз System.Type) следующей функции DisplayFixes. Эта функция использует также рефлексию для отыскания любых исправлений ошибок, примененных к классу, а затем просматривает все методы данного класса, отыскивая в них атрибуты BugFix: public static void DisplayFixes (System.Type t) { // Получение всех исправления ошибок для данного типа, // который считается классом. object[] fixes = t.GetCustomAttributes (typeof (BugFixAttribute) , false); Console.WriteLine ("Displaying fixes for {0}" , t) ; // Console.WriteLine ("Отображение исправлений для {0}" , t) ; // Отображение информации об основных исправлениях, foreach (BugFixAttribute bugFix in fixes) Console.WriteLine (" {0}" , bugFix); // Получение всех членов (т.е. функций) класса. foreach (Memberlnfo member in t.GetMembers (BindingFlags.Instance | BindingFlags.Public | BindingFlags.Nonpublic | BindingFlags.Static)) { // И отыскание любых значительных изменений примененных к ним. object[] memberFixes = member.GetCustomAttributes(typeof(BugFixAttribute) , false); if (memberFixes.Length > 0) { Console.WriteLine (" {0}" , member.Name); // Циклический просмотр и отображение информации об этих исправлениях, foreach (BugFixAttribute memberFix in memberFixes) Console.WriteLine (" {0}" , memberFix); } } } Сначала код извлекает все атрибуты BugFix из самого типа: object[] fixes = t.GetCustomAttributes (typeof (BugFixAttribute), false); Эти атрибуты нумеруются и отображаются. Затем код с помощью метода GetMembers () циклически просматривает все члены, определенные в классе: foreach (Memberlnfo member in t.GetMembers ( BindingFlags.Instance | BindingFlags.Public I BindingFlags.Nonpublic I BindingFlags.Static)) Метод GetMembers извлекает свойства, методы и поля из данного типа. Для ограничения списка возвращенных членов служит перечисление BindingFlags (определенное внутри System.Reflection).
Глава 30. Атрибуты 989 Флаги связывания, переданные этому методу, указывают интересующие члены — в данном случае нас интересуют все члены экземпляров и статические члены, независимо от видимости, поэтому мы указываем члены Instance и Static, а также Public и Nonpublic. После извлечения всех членов мы проходим по ним циклом, отыскивая любые атрибуты BugFix, связанные с конкретным членом, и выводим их на консоль. Чтобы вывести список исправления ошибок для данного класса, достаточно вызвать статический метод DisplayFixes (), передавая ему тип класса: BugFixAttribute.DisplayFixes (typeof (MyBuggyCode)); Для ранее представленного класса MyBuggyCode это приводит к следующему выводу: Displaying fixes for MyBuggyCode BugFix 101 : Created some methods DoSomething BugFix 2112 : Returned a non-null string BugFix 38382 : Returned OK .ctor BugFix 90125 : Removed call to base() Если бы нужно было отобразить исправления для всех классов данной сборки, можно было бы воспользоваться рефлексией для извлечения всех типов из сборки и передать каждый из них статическому методу BugFixAttribute . DisplayFixes. AttributeUsage. Inherited Посредством установки этого флага при определении пользовательского атрибута атрибут можно определить как наследуемый: [AttributeUsage (AttributeTargets.Class, Inherited = true)] public class BugFixAttribute { . .. } Этот код указывает, что атрибут BugFix будет наследоваться любыми подклассами класса, который использует атрибут, что может быть как желательным, так и не желательным. В случае атрибута BugFix такое поведение, вероятно, нежелательно, поскольку обычно исправление ошибок применяется к отдельному классу, но не к его производным классам. Предположим, существует следующий абстрактный класс с примененным исправлением ошибки: [BugFix(8383","Fixed that abstract bug")] public abstract class GenericRow : DataRow { public GenericRow (System.Data.DataRowBuilder builder) : base (builder) { } } При создании подкласса этого класса нежелательно, чтобы этот же атрибут BugFix был отражен в подклассе — подкласс не содержит этого исправления. Однако при определении набора атрибутов, которые связывают члены класса с полями таблицы базы данных, очевидно, эти атрибуты наследовать желательно. Теперь, располагая определенными знаниями о пользовательских атрибутах, легко представить себе несколько ситуаций, когда добавление дополнительной информации о сборках полезно. Можно было бы также расширить определенный ранее атрибут BugFix — например, для определения номера версии программы, в которой
990 Часть V. Дополнительные технологии ошибка была исправлена. Это позволило бы отображать конечным пользователям все ошибки, исправленные в данной версии приложения, просто используя рефлексию для отыскания атрибутов BugFix и последующего сравнения номера версии данного исправления с версией, запрошенной конечным пользователем. Этот подход прекрасно подошел бы для выпуска в конце цикла разработки документации, содержащей список всех исправлений, выполненных в последней версии. Конечно, в добавлении этих атрибутов приходится полагаться на разработчиков, но проверку документирования всех исправлений можно сделать частью цикла контрольного просмотра кода. Резюме В этой главе было описано, что собой представляют атрибуты, и были рассмотрены некоторые атрибуты, определенные внутри каркаса .NET Framework. Внутри каркаса определено множество других классов атрибутов, и лучший способ ознакомления с их применением — просмотр документации .NET Framework SDK. Чтобы выяснить все атрибуты, определенные в сборке, достаточно загрузить ее в утилиту Reflector, перейти к классу System. Attribute, а затем отобразить все производные типы. Количество атрибутов, определенных внутри каркаса .NET, может вызвать удивление. В этой главе были рассмотрены следующие темы, связанные с атрибутами. □ Использование атрибута Conditional для исключения кода отладки из двоичных файлов окончательного выпуска. □ Использование атрибута Obsolete для пересмотра библиотек в будущем. □ Использование атрибутов при сериализации для определения компонентов, подвергаемых и не подвергаемых сериализации. □ Отложенное подписание сборки. □ Способ создания пользовательских атрибутов. Атрибуты — мощное средство добавления дополнительной информации в сборки. Они позволяют создавать собственные атрибуты и добавлять в классы необходимую дополнительную информацию. В этой главе было описано создание атрибута BugFix, который можно было бы использовать для связывания исправленного исходного кода с программной ошибкой, первоначально выявленной конечным пользователем. Существует также множество других применений атрибутов — поэтому настоятельно рекомендуется поискать другие классы атрибутов в каркасе .NET Framework.
XML-документация До этой главы книги вы уже ознакомились с языком С# в целом, а также с множеством задач, которые можно выполнять с его помощью, в том числе — с программированием приложений для Web и Windows. В процессе чтения этих материалов интенсивно использовалось средство IntelliSense, доступное в Visual Studio. Это полезное средство радикально уменьшает трудоемкость ввода с клавиатуры, поскольку в процессе ввода предлагает ключевые слова, имена типов, имена переменных и параметры. Оно облегчает также запоминание имеющихся в распоряжении методов и свойств и часто явно указывает способ их использования. Однако поддержка создаваемых классов реализована несколько хуже. Хотя программа и предлагает имена классов и их членов, какие-либо всплывающие подсказки о способе выполнения тех или иных задач отсутствуют. Для достижения поведения, аналогичного поведению типов каркаса .NET Framework, нужно применять XML-документацию. XML-документация позволяет включать описание синтаксиса, справку и примеры создаваемых классов на уровне исходного кода. Затем эту информацию можно использовать для предоставления информации IntelliSense средствам Visual Studio, как было описано ранее, но существуют также и другие возможности. XML-документацию можно применять в качестве исходной для создания полной MSDN-документации проектов. Кроме того, с помощью XSLT XML-документацию можно стилизовать для получения непосредственной HTML-документации, затрачивая при этом минимальные усилия. В предшествующих главах было показано, насколько гибким является язык XML, и все его возможности можно использовать в процессе разработки и эксплуатации приложений. Документация особенно важна при работе в составе команды, поскольку позволяет подробно описать работу созданных типов. Она может быть также очень полезна конечным пользователям при создании комплектов средств разработки или аналогичных программных продуктов, когда предполагается, что пользователи будут применять созданные классы в собственном коде. Наличие средства встраивания этой документации в инструменты, используемые для разработки кода — очень мощная возможность. Хотя добавление XML-документации в код может быть процессом, тре-
992 Часть V. Дополнительные технологии бующим больших затрат времени, достигаемый результат вполне это окупает. Вставка поясняющего текста в процессе разработки может облегчать работу в долговременной перспективе и предоставляет удобную краткую справочную информацию, которая помогает избегать путаницы, когда количество классов в проекте начинает выходить из-под контроля. В этой главе будут рассматриваться следующие темы. □ Добавление и просмотр основной XML-документации. □ Использование XML-документации. Добавление ХМL-документации Добавление XML-документации в код — поразительно простая задача. Как и атрибуты, рассмотренные в предыдущей главе, XML-документация может применяться к любому типу или члену за счет ее помещения в строках, предшествующих этому члену. Добавление XML-документации осуществляется посредством расширенной версии синтаксиса комментариев С#, в которой используются три косых черты вместо двух. Например, следующий код демонстрирует помещение XML-документации в файл исходного кода класса MyClass и метода MyMethod () этого класса: /// (XML-документация располагается здесь.) class MyClass { /// (И здесь.) public void MyMethod() { } } XML-документация не может применяться к объявлениям пространств имен. Как правило, XML-документация будет занимать несколько строк, и помечаться в соответствии со словарем XML-документации: /// <XMLDocElement> III Содержимое. /// Еще содержимое. /// <IXMLDocElement> class MyClass { В этом примере <XMLDocElement> — один из нескольких доступных элементов, который может содержать вложенные элементы, часть из которых могут использовать атрибуты для дальнейшего описания функциональных возможностей. Обратите внимание, что строки кода, помеченные символами ///и содержащие XML-документацию, полностью игнорируются и обрабатываются как пробелы. Это необходимо, чтобы предотвратить обработку компилятором этих строк документации в качестве кода, что приводило бы к ошибкам компилятора. Наиболее важный элемент XML-документации — <summary>, который предоставляет краткое описание типа или члена. Прежде чем приступать к рассмотрению более сложных вопросов, необходимо ознакомиться со следующим примером, который иллюстрирует использование этого элемента и визуализацию результатов.
Глава 31. XML-документация 993 практическое занятие Добавление и просмотр основной ХМ L-документации 1. Создайте новое консольное приложение FirstXMLDocumentation и сохраните его в каталоге С: \BegVCSharp\Chapter31. 2. Добавьте класс DocumentedClass. 3. Откройте код класса DocumentedClass, объявите определение класса общедоступным, добавьте новую строку перед объявлением класса и введите в ней символы ///. 4. Обратите внимание, что функция автозавершения IDE добавляет следующий код, помещая курсор внутрь элемента <summary>. namespace FirstXMLDocumentation { /// <summary> /// /// </summary> public class DocumentedClass { 5. Добавьте следующий текст в элемент <summary>: namespace FirstXMLDocumentation { /// <summary> /// This is a summary description for the DocumentedClass class. /// </summary> public class DocumentedClass { 6. В файле Program, cs введите doc в методе Main () и обратите внимание на открывшееся всплывающее окно с информацией IntelliSense, показанное на рис. 31.1. DocumenUdCU».a- Pioguaw»' ^ FintXMl Documentation. Program ijg Using System; .: t using System.Collections.Generic; 3 using System.Linq; 4 l using System.Text; 6Q namespace FirstXHLDocumentation ^Miin($tnng|]«rgJl i о 0 11 12; L3 class Program < static void Haln(string[] args) < docj [ij Dictionary* > *| I ; **J DrvideByZeroException 1 1 *; DIINotFoundException ' Jo : J i'1'-'1 .h.'.-'m.*——■ I ; ф- double 1 +> Double 1 j *t} DupliceteWaitObjectException 1 -J «•« •; Encoder ^j > ii [class FirstXMLDocumentation DocumentedClass [This is a summary description for the DocumentedClass class Рис. 31.1. Всплывающее окно с информацией IntelliSense
994 Часть V. Дополнительные технологии 7. Откройте окно Object Browser (Браузер объектов) и разверните элемент проекта FirstXMLDocumentation вплоть до объекта DocumentedClass. Обратите внимание на краткую информацию, отображенную в нижней правой панели, как показано на рис. 31.2. ом*——,n iimnmiiMtii | Biome: AN Components ! «S*«Ch. j Q \& FlrstXMlDocumentttton В {) FlrstXMlDocumentation ■ ■, tffBffffiWFIWWW '<£, jj Program : ♦ • J MIcrosoftBuild.Conversions.5 ; ♦ _j Microsort.Build.Engine 1 ffi -C3 Microsoft.Build.Framework Ш -a MicroJoft.Build.Utilitiei.v3.5 i ♦ -u3 Microsort.VuualBajic Щ ^3 MIcrosoftvlsualC.STiaR u --i m j со Hid |йЧ«в PresentationBuildTajH J ii ^3 PresentationCore 1<$ -<a PresentatlonFramework 1 ffi -a PresentatlonFramework Aero [ffi <C3 PresentationFramework.Classic Iffi -vJ PresentationFrameworfc.luna & чЛ PresentationFramework.Royale I * uJ ReactiFramework ri) ЧЭ System j faki ---J SyjtemAddln I t*< J SyjtemJkddln.Contract 1 | J System.Configuration j ffi «a System.Configuration.Install L' i"» fw«t>m Cm», = • ... » -г Ъ J • ' J . . public clajjDocunentedCUtt Member of FfrttXMl Documentation Suaaary: This is a summary descnption for the DocumentedClass class. a4... w XI Рис. 37.2. Отображение элемента XML-документации <summary> Описание полученных результатов Этот пример демонстрирует общую методику добавления XML-документации и использования этой документации в интегрированной среде разработки (IDE). При добавлении краткой информации о классе DocumentedClass мы видели, как IDE динамически определяет необходимые элементы информации и автоматически заполняет основную часть соответствующего кода. Средства IntelliSense и Object Browser выявляет документацию даже без компиляции проекта. Эта общая методика применима ко всем аспектам XML-документации и облегчает добавление и использование этой информации. Комментарии XML-документации Фактически в XML-документацию целевого объекта (под целевым объектом подразумевается тип или член) можно добавлять любые элементы XML. Однако применение ряда рекомендуемых элементов и атрибутов будет способствовать тому, чтобы документация соответствовала стандартным рекомендациям. Рекомендуемых элементов XML достаточно для большинства ситуаций, и выполнение этого стандарта означает, что программные средства, в которых применяется XML-документация (в том числе IntelliSense и Object Browser), смогут эффективно ее использовать. Краткое описание основных элементов XML-документации приведено в табл. 31.1. Подробнее они рассматриваются в последующих разделах.
Глава 31. XML-документация 995 Таблица 31.1. Основные элементы XML-документации Элемент Описание <с> <code> <description> <example> <exception> <include> <item> <list> <listheader> <para> <param> <paramref> <permission> <remarks> <returns> <see> <seealso> <summary> <term> <typeparam> <typeparamref> <value> Форматирует текст с использованием шрифта кода. Применяйте этот элемент для отдельных кодовых слов, внедренных в другой текст Форматирует текст с использованием шрифта кода. Применяйте этот элемент для нескольких строк кода, не являющихся внедренными в другой текст Помечает текст в качестве описания элемента. Используется в качестве дочернего элемента элементов <item> или <iistheader> в списках Помечает текст в качестве примера использования целевого объекта Указывает исключение, которое может порождаться целевым объектом Извлекает XML-документацию из внешнего файла Представляет элемент списка. Используется в качестве дочернего элемента для <iist> и может иметь дочерние элементы <description> и <term> Определяет список. Может иметь дочерние элементы <listheader> и <item> Представляет строку заголовка табличного списка. Используется в качестве дочернего элемента для <iist> и может иметь дочерние элементы <description> и <term> Используется для разбиения текста на отдельные абзацы Описывает параметр целевого объекта Определяет ссылку на параметр метода Указывает разрешения, необходимые для целевого объекта Содержит дополнительную информацию о целевом объекте Описывает возвращаемое значение целевого объекта; используется с методами Определяет ссылку на другой целевой объект, используемую в теле такого элемента, как <summary> Определяет ссылку на другой целевой объект, обычно используемую вне других элементов или в конце элемента, например, <summary> Содержит краткую информацию о целевом объекте Помечает текст в качестве определения элемента. Используется в качестве дочернего элемента элементов <item> или <iistheader> в списках Описывает параметр типа обобщенного целевого объекта Определяет ссылку на параметр типа Описывает возвращаемое значение целевого объекта; используется с методами Элементы форматирования текста Многие из элементов, перечисленных в табл. 31.1, предназначены для форматирования текста внутри других элементов. Например, элемент <summary> может содержать комбинацию других элементов, которые указывают текст, предназначенный для отображения. Элементы форматирования текста — <с>, <code>, <list> и связанные с ними элементы <para>, <paramref > и <see>. Элемент <seealso> — особый случай, который можно также включить в этот список, поскольку его можно вставлять в тело текста, хотя обычно он встречается в конце.
996 Часть V. Дополнительные технологии <рага> Элемент <рага> используется для разделения текста на абзацы: /// <summary> /// <рага>1-й абзац краткого описания .</parа> /// <рага>2-й абзац краткого описания.</parа> /// </summary> <с> и <code> Оба элемента <с> и <code> используются для форматирования шрифтом кода, обычно моноширинным шрифтом, таким как Courier. Различие между ними в том, что элемент <с> представляет "код в тексте", т.е. кодовые слова, встречающиеся внутри предложений, a <code> служит для форматирования фрагментов кода вне текста. Элементы <с> и <code> могут использоваться следующим образом: /// <summary> /// <рага> /// Это краткое описание посвящено <с>классу</с> с интересными возможностями. /// Попробуйте это: /// </рага> /// <code> /// MyPoet poet = new My Poet ("Homer") ; /// роеt.AddMuse("Thalia"); /// poet.WriteMeAnEpic() ; /// </code> /// </summary> <see>, <seealso>, <paramref > и <typeparamref > Все эти четыре элемента используются для ссылки на другие записи в XML-документации проекта или на внешние записи MSDN. Обычно каждый из них будет отображаться в виде гиперссылки, позволяя браузерам документации выполнять переход к другим записям. Элементы <see> и <seealso> указывают свой целевой объект с помощью атрибута cref, причем целевым объектом может быть любой тип или член любого класса, в проекте или где-либо. Элементы <paramref > и <typeparamref > используют атрибут name для ссылки на параметр текущего целевого объекта: /// <summary> /// <рага> /// Этот метод использует <paramre£ name="museName" /> для выбора музы. /// Для получения дополнительной информации см. <see cref="MyPoet" />. /// </para> /// <seealso cref="MyParchment" /> /// <seealso cref="MyTheme" /> /// </summary> Элемент <see> может быть особенно полезен для ссылки на ключевые слова С# посредством другого атрибута — langword. /// <summary> /// Для получения дополнительной информации см. <see langword="null" />. /// </summary> Преимущество этого подхода в том, что указание ключевого слова, специфичного для языка, позволяет подготавливать документацию к другим языкам, таким как Visual Basic. Ключевое слово null в С# эквивалентно ключевому слову Nothing в Visual Basic, поэтому возможно обслуживание обоих языков — при условии, что подобные нюансы известны средству, используемому для форматирования XML-документации.
Глава 31. XML-документация 997 Обратите внимание, что элементы не включают в себя текст, предназначенный для отображения, который, как правило, создается атрибутами name, cref или langword. Это обстоятельство следует иметь в виду во избежание повторения текста — например: /// Этот метод использует <paramref name="museName" /> museName для выбора музы. По всей видимости, в этом примере слово museName будет повторено. <list> и связанные с ним элементы Элемент <list> — наиболее сложный элемент форматирования текста, поскольку он может использоваться различными способами. Этот элемент обладает атрибутом type, который может принимать любое из следующих значений: □ bullet — форматирует маркированный список; □ number — форматирует нумерованный список; □ table — форматирует таблицу. Как правило, элемент <list> содержит один дочерний элемент <listheader> и несколько дочерних элементов <item>. Каждый из них может содержать дочерние элементы <term> и <description>. Выбор конкретного дочернего элемента будет зависеть от типа списка и способа форматирования списков выбранным средством. Например, элемент <term> может присутствовать или отсутствовать в списках в виде таблицы, в то время как элемент <listheader> имеет смысл только в таблице. Для маркированных списков можно применять код, подобный следующему: /// <summary> /// <рага> /// Этот метод использует <paramref name="museName" /> для выбора музы. /// </рага> /// <рага> /// Попробуйте следующие музы: /// <list type="bullet"> /// <listheader> /// <term>Muse name</term> /// <description>Muse's favorite pizza</description> /// </listheader> /// <item> /// <term>Calliope</term> /// <description>Ham &amp; MushroonK/description> /// </item> /// <item> /// <term>Clio</term> /// <description>Four Seasons</description> /// </item> /// <item> /// <term>Erato</term> /// <description>Meat Feast</description> /// </item> /// </list> /// </para> /// </summary> Поскольку конкретное форматирование, выполняемое этими элементами, может быть различным, лучше немного поэкспериментировать. Имейте в виду, что любые действия, выполняемые этими элементами, не будут проявляться в браузере объектов, который их игнорирует.
998 Часть V. Дополнительные технологии Основные структурные элементы Несколько элементов, перечисленных в табл. 31.1, предназначены для использования в качестве элементов верхнего уровня в описании конкретного целевого объекта. Как было показано ранее, элемент <summary> служит ярким примером. Этот элемент никогда не должен входить в состав другого элемента и всегда используется для предоставления краткой информации о целевом объекте. Другие элементы, соответствующие этому описанию — <example>, <exception>, <param>, <permission>, <remarks>, <returns> и <value>. Элемент <seealso> представляет особый случай элемента, который может быть как элементом верхнего уровня, так и дочерним элементом другого элемента. <include> — еще один особый случай элемента, поскольку он по существу замещает другие элементы, загружая XML-код из внешнего файла. Рассмотрим эти элементы по очереди. <summary>, <example> и <remarks> Каждый из этих трех элементов предоставляет общую информацию о целевом объекте. Мы уже встречались с элементом <summary>, который можно использовать для вывода основной информации о целевом объекте. Поскольку эта информация отображается в подсказках инструмента, ее целесообразно хранить краткой. Дополнительную информацию следует помещать в элементы <example> и <remarks>. Часто при представлении класса полезно приводить пример его использования. Это же относится к методам, свойствам и т.п. Вместо того чтобы вставлять эту информацию в элемент <summary>, имеет смысл поместить ее в новый раздел <example>. /// <summary> /// <рага> /// Это краткое описание посвящено <с>классу</с> с интересными возможностями. /// </рага> /// </summary> /// <example> /// <para> /// Попробуйте следующее: /// </рага> /// <code> /// MyPoet poet = new MyPoet("Homer") ; /// роеt.AddMuse("Thalia"); /// poet.WriteMeAnEpic() ; /// </code> /// </example> Аналогично элемент <remarks> часто используют для предоставления более длинного описания целевого объекта. При этом он может содержать элементы <see> и <seealso>, определяющие перекрестные ссылки. <param> и <typeparam> Эти элементы описывают параметр — стандартный параметр метода либо параметр типа — обобщенных целевых объектов. Ссылка на параметр осуществляется с помощью атрибута name. При использовании нескольких параметров эти элементы могут встречаться несколько раз, как показано в следующем примере: /// <summary> /// Метод, используемый для добавления музы. /// </summary> /// <param name="museName"> /// Параметр <see langword="string" /> указывает имя музы. /// </param>
Глава 31. XML-документация 999 /// <param name="museMood"> /// Значение перечисления <see cref="MuseMood" /> указывает настроение музы. /// </param> Атрибут name элемента <param> или <typeparam> не только указывает имя параметра, но и будет соответствовать атрибуту name любых элементов <paramref > или <typeparamref >, ссылающихся на данный параметр. <re turns > и <value> Эти два элемента аналогичны в том, что они оба связаны с возвращаемым значением: <returns> применяется для возвращаемого значения методов, a <value> — для значения свойства, которое также можно считать возвращаемым значением. Ни один из этих элементов не использует никаких атрибутов. Применительно к методам элемент <returns> можно использовать следующим образом: /// <summary> /// Метод, используемый для добавления музы. /// </summary> /// <param name="museName"> /// Параметр <see langword="string" /> указывает имя музы. /// </param> /// <param name="museMood"> /// Значение перечисления <see cref="MuseMood" /> указывает настроение музы. /// </param> /// <returns> /// Возвращаемое значение этого метода — <see langword="void" />. /// </returns> Пример использования элемента <value> применительно к свойствам выглядит следующим образом: /// <summary> /// Свойство для получения или установки музы. /// </summary> /// <value> /// Тип этого свойства — <see langword="string" />. /// </value> <permission> Этот элемент используют для описания разрешений, связанных с целевым объектом. Фактическая установка разрешений выполняется другими средствами, такими как применение атрибута к методу; элемент <permission> просто позволяет информировать других пользователей об этих разрешениях. Элемент <permission> включает в себя атрибут cref, поэтому, при желании, можно реализовать ссылку на класс, содержащий дополнительную информацию, такой как System. Security. PermissionSet. Например: /// <permission cref="System.Security.PermissionSet"> /// Только администраторы могут использовать этот метод. /// </permission> <exception> Этот элемент используют для описания любых исключений, которые могут генерироваться во время использования целевого объекта. Он имеет атрибут cref, который можно применять для перекрестных ссылок на тип исключения. Например, этот элемент можно было бы использовать для указания диапазона допустимых значений свойства и описания последствий попытки установки недопустимого значения:
1000 Часть V. Дополнительные технологии /// <exception cref="System.ArgumentException"> /// Это исключение будет порождаться при установке <paramref name="Width" /> /// равным отрицательному значению. /// </exception> <seealso> Как было отмечено ранее, элемент <seealso> применяется в качестве элемента верхнего уровня. Можно использовать несколько таких элементов, которые, в зависимости от применяемого инстументального средства, могут быть форматированы в виде списка ссылок в конце записи целевого объекта: /// <summary> /// Это краткое описание класса MyPoet class. /// </summary> /// <seealso cref^'MyParchment" /> /// <seealso cref="MyTheme" /> /// <seealso cref="MyToenails" /> <include> Включение большого объема XML-документации в исходные файлы может приводить к некоторой путанице, поэтому стоит подумать о ее помещении в отдельные файлы. Например, такой подход может быть желательным, если кто-то другой готовит документацию для кода, поскольку этому лицу не нужно предоставлять доступ к самим исходным файлам. Элемент <include> позволяет решать эту задачу посредством двух своих атрибутов file и path. Атрибут file указывает внешний XML-файл, содержащий XML-документацию, которую нужно включить, a path служит для отыскания конкретного раздела XML-кода внутри документа с помощью синтаксиса XPath. Например, ссылку на внешнюю XML-документацию можно было бы реализовать следующим образом: /// <include file="ExternalDoc.xml" path="documentation/classes/MyClass/*" /> public class MyClass В этом примере реализована ссылка на файл ExternalDoc.xml. Он может содержать код, подобный показанному ниже: <?xml version=,,1.0" encoding="utf-8" ?> <documentation> <classes> <MyClass> <summary> Краткая информация во внешнем файле. </summary> <remarks> Замечательно, не правда ли? </remarks> </MyClass> </classes> </documentation^ В свою очередь, этот код мог бы быть эквивалентным следующему: /// <summary> /// Краткая информация во внешнем файле. /// </summary> /// <remarks> /// Замечательно, не правда ли? /// </remarks> public class MyClass
Глава 31. XML-документация 1001 Добавление XML-документации с использованием диаграммы классов В главе 9 вы ознакомились с диаграммами классов, а затем с их применением для создания классов схематическими средствами с отражением изменений в коде в реальном времени. Вы также узнали, как добавлять классы и члены, модифицировать сигнатуры методов и т.п. Поэтому возможность использования диаграмм классов для добавления XML-документации в классы, без необходимости вникания в исходный код, не должна вызывать удивления. Вспомните, что диаграммы классов доступны только в Visual Studio и не входят в состав Visual Studio Express. Для ознакомления с этим примером требуется наличие установленной среды Visual Studio. К сожалению, как будет показано, добавление XML-документации таким способом, не предоставляет таких возможностей, как ее добавление вручную, но зато позволяет выполнять поставленные задачи очень быстро, не прибегая к какому-либо синтаксису XML. Доступные возможности демонстрируются в следующем практическом занятии. практическое занял*! Добавление XML-документации в диаграмму класса 1. Создайте новое приложение библиотеки классов DiagrammaticDocumentation и сохраните его в каталоге С: \BegVCSharp\Chapter31. 2. После загрузки проекта щелкните на кнопке View Class Diagram (Просмотр диаграммы класса) в окне Solution Explorer. Должна открыться диаграмма, содержащая класс Class 1, автоматически сгенерированный при создании приложения библиотеки классов. 3. Добавьте класс DocumentedClass и измените его члены в соответствии с рис. 31.3 и табл. 31.2. Рис. 31.3. Добавление класса DocumentedClass
1002 Часть V. Дополнительные технологии Таблица 31.2. Члены класса DocumentedClass Тип члена Имя Тип Модификатор Краткое описание Метод Свойство Свойство GetFactors IncludeOne IncludeSelf long[] bool bool public public public Извлекает множители числа Включает 1 в результат работы метода GetFactors () Включает себя в результат работы метода GetFactors () 4. Щелкните в поле Summary (Краткая информация) метода GetFactors (), а затем на кнопке ... (многоточие). Измените текст в диалоговом окне Description (Описание), как показано на рис. 31.4 и в табл. 31.3. Description Summary: Gets the factors of a number. Returns: Returns a long array. This method is used to obtain the factors of a number, that is, all the numbers that the number can be divided by without leaving a remainder. If IncludeOne and IncludeSelf are both true and the result returned consists of exactry two numbers (the number itself and one) then the number is prime.) O* J [ CuKtl J Рис. 31.4. Диалоговое окно Description Таблица 31.3. Значения полей в диалоговом окне Description Поле Значение Summary (Краткое описание) Returns (Возвращаемые значения) Remarks (Замечания) Извлекает множители числа. Возвращает массив long. Этот метод служит для получения множителей числа — т.е. всех чисел, на которые число может быть разделено без остатка. Если оба значения IncludeOne и IncludeSelf равны true, и результат состоит только из двух чисел (самого числа и единицы), число является простым. 5. Аналогично измените текст полей Value (Значение) и Remarks (Замечания) для свойств IncludeOne и IncludeSelf в соответствии с табл. 31.4.
Глава 31. XML-документация 1003 Таблица 31.4. Значения полей Value и Remarks для свойств includeOne и includeSelf Свойство Текст поля Value Текст поля Remarks IncludeOne IncludeSelf Значение типа bool Значение типа bool Если значение этого свойства установлено равным true, результат long [ ] метода GetFactors () будет содержать 1. Если значение этого свойства установлено равным true, результат long [ ] метода GetFactors () будет содержать его параметр numberToFactor. 6. Щелкните на классе в диаграмме классов и измените поля Summary (Краткая информация) и Remarks (Замечания) класса в соответствии с табл. 31.5. Таблица 31.5. Значения полей Summary и Remarks для класса Текст поля Summary Текст поля Remarks Этот класс позволяет разлагать числа на Используйте метод GetFactor () для разложения множители в соответствии с определенны- числа на множители в соответствии с правилами правилами. ми, определенными свойствами IncludeOne и IncludeSelf. 7. Просмотрите код класса DocumentedClass. Теперь он должен содержать добавленные XML-комментарии, как показано в следующем примере: /// <summary> /// Извлекает множители числа. /// </summary> /// <гетагкз>Этот метод служит для получения множителей числа — т.е. всех /// чисел, на которые число может быть разделено без остатка. /// Если оба значения IncludeOne и IncludeSelf равны true, и результат /// состоит только из двух чисел (самого числа и единицы) , число является /// простым.</remarks> /// <param name="numberToFactor">Число, которое нужно разложить на множители.</param> /// <returns>Bo3BpamaeT массив long.</returns> public long[] GetFactors(long numberToFactor) { throw new System.NotlmplementedException(); Описание полученных результатов Этот пример демонстрирует использование диаграммы классов для определения XML-документацйи проекта. После прочтения предыдущего раздела может возникнуть вопрос, почему мы не добавили перекрестные ссылки между методами и тому подобными объектами. Например, почему текст Remarks для метода GetFactors () не был определен следующим образом: Этот метод служит для получения множителей числа — т.е. всех чисел, на которые число может быть разделено без остатка. Если оба значения <see cref="IncludeOne" /> и <see cref="IncludeSelf' /> равны <see langword="true" />, и возвращенное значение состоит только из двух чисел (самого числа и единицы), число является простым. Причина состоит в том, что текстовый редактор диаграммы класса автоматически пропускает текст, введенный в диалоговом окне Description, поэтому результат ввода выше приведенного кода в действительности будет выглядеть следующим образом:
1004 Часть V. Дополнительные технологии /// <summary> /// Извлекает множители числа. /// </summary> /// <гетагк8>Этот метод служит для получения множителей числа — т.е. всех /// чисел, на которые число может быть разделено без остатка. /// Если оба значения &lt;see cref="IncludeOne" /&gt; /// и &lt;see cref="IncludeSelf" /&gt; /// равны &lt;see langword="true" /&gt;, и возвращенный результат состоит только /// из двух чисел (самого числа и единицы) , число является простым.</remarks> /// <param name="numberToFactor">4i4C^o, которое нужно разложить на множители.</рагат> /// <returns>B03BpamaeT массив long.</returns> public long[] GetFactors (long numberToFactor) { throw new System.NotlmplementedException (); } К сожалению, это одно из неудобств, с которым приходится мириться при добавлении XML-документации посредством диаграммы классов. Она прекрасно подходит для добавления текста, но для внедрения кода разметки XML-документации код приходится изменять вручную. Генерирование файлов XML-документации До сих пор вся добавляемая XML-документация была ограничена средой разработки во время работы с проектом. Если требуется действительно выполнять какие-то действия с добавленной документацией, необходимо обеспечить, чтобы проект выводил документацию в виде XML-файла. Для этого необходимо изменить настройки компоновки проекта, как показано на рис. 31.5. [^..•деххимлииол- Dwumtnueci.»*;- UM.bnarw.luar CJ...1-U. Start P-*« Application Build* Bund tvtnb D.bug Reioure» Service! Setbng» Reference Ptfl S.anmo i> 1 Configure**-* lMtajttft*flL_.. Condition»! compilation tymboii У Defir-e DEBUG constent } Oefir* TRACE comtant Pl»tfoim terget , Allow jnstfe cod» ■4 maecode Error* and wtrningj We.rn.ng lewl. Suppren wemmgs • None Specific wemmgi All Output peth: / *Ml documtntation file: Register for COM mterop Genet *te seneiaition memory •>: P1»tform Active (Any CPU| An» CPU 4 *»J bin\Debug\ bin\0ebugVDi*gr»mm«tJcOocument*tionJ(MI A*o ' L^".] Admixed.. Рис. 31.5. Установка вывода документации в виде XML-файла
Глава 31. XML-документация 1005 Нужно всего лишь установить флажок XML Documentation File (Файл XML-документации) и указать имя выходного файла. В общем случае целесообразно сохранять файл XML-документации в каталоге самой сборки, поскольку именно здесь IDE ищет его. Если клиентское приложение обращается к документированной сборке в другом решении, преимущества применения IntelliSense и Object Browser доступны, только если IDE может обнаружить файл XML-документации. Как только настройки компоновки, связанные с XML-документацией, будут включены, компилятор окажется сконфигурированным для активного поиска XML-документации в классах. Хотя отсутствие XML-документации для конкретного класса или метода не обязательно является ошибкой, IDE будет предупреждать обо всех отсутствующих компонентах в форме предупреждений в окне Error List (Список ошибок). Пример такого предупреждения показан на рис. 31.6. |e .jrt Description ■■6ш№ЯЯ^б^Я№НЯ^ШВ^^НВ1 К Error list -rj Output ^Breakpoint, File Line Column Project * ¥ X Рис. 31.6. Пример предупреждения об отсутствии XML-комментария для класса Class 1 Это предупреждение, сгенерированное предыдущим примером, указывает на отсутствие документации для класса Class 1 (который мы оставили без изменений). Файл XML-документации, созданный для приведенного примера, имеет следующий вид: <?xml version=.0"?> <doc> <assembly> <name>DiagrammaticDocumentation</name> ' </assembly> <members> <member name="T:DiagrammaticDocumentation.DocumentedClass"> <summary> Этот класс позволяет разлагать числа на множители в соответствии с определенными правилами. </summary> <гетагкз>Используйте метод GetFactorO для разложения числа на множители в соответствии с правилами, определенными свойствами IncludeOne и IncludeSelf.</remarks> </member> <member name="M:DiagrammaticDocumentation.DocumentedClass.GetFactors"> <summary> Извлекает множители числа. </summary> <remarks> Этот метод служит для получения множителей числа — т.е. всех чисел, на которые число может быть разделено без остатка. Если оба значения IncludeOne и IncludeSelf равны true, и возвращенный результат состоит только из двух чисел (самого числа и единицы), число является простым. </remarks> <returns>Bo3BpaujaeT массив long.</returns> </member>
1006 Часть V. Дополнительные технологии <member name="P:DiagrammaticDocumentation.DocumentedClass.IncludeOne"> <summary> Включает 1 в результат работы метода GetFactors (). </summary> <remarks>Ecnn значение этого свойства установлено равным true, результат long[] метода GetFactors () будет содержать единицу.</remarks> <уа1ие>3начение типа bool.</value> </member> <member name="P:DiagrammaticDocumentation.DocumentedClass.IncludeSelf"> <summary> Включает себя в результат работы метода GetFactors(). </summary> <гетагкэ>Если значение этого свойства установлено равным true, результат long[] метода GetFactors() будет содержать его параметр numberToFactor. </remarks> <уа1ие>3начение типа bool.</value> </member> </members> </doc Этот документ содержит перечисленные ниже элементы. □ Элемент <doc> корневого уровня, содержащий всю остальную информацию документации. □ Элемент <assembly>, содержащий элемент <name>, который хранит имя сборки, описываемой XML-документацией. □ Элемент <members>, содержащий XML-документацию каждого из членов сборки. □ Несколько элементов <member>, каждый из которых содержит атрибут name, указывающий, к какому типу или члену применяется содержащаяся в нем XML- документация. Атрибуты name элементов <member> соответствуют единой схеме именования. Все они — имена, принадлежащие одному из следующих пространств имен — т.е. они все начинаются с одной из следующих букв: □ Т — указывает, что член является типом (классом, интерфейсом, распоркой (strut), перечислением или делегатом); □ М — указывает, что член является методом; □ Р — указывает, что член является свойством или индексатором; □ F — указывает, что тип является полем или членом перечисления (этот вариант не нашел своего отражения в примере); □ Е — указывает, что тип является событием (этот вариант не нашел своего отражения в примере). Любые ошибки в XML-документации типа отображаются в сгенерированном файле в виде закомментированного элемента <member>: <!— Badly formed XML comment ignored for member "T:DiagrammaticDocumentation.DocumentedClass" —> <!-- Неправильно сформированный XML-комментарий игнорируется для члена "Т:DiagrammaticDocumentation.DocumentedClass" —> Внутри элемента <member> XML-документация целевого объекта вставляется в том виде, каком она существует (если только не возникает какая-то проблема с XML-кодом).
Глава 31. XML-документация 1007 Пример приложения, оснащенного ХМL-документацией В остальной части этой главы мы рассмотрим способы использования файлов XML-документации. И для облегчения этой задачи мы используем полностью работоспособный пример библиотеки классов DocumentedClasses, поставляемый в пакете XML-документации практически всех типов. Классы, включенные в состав библиотеки DocumentedClasses, показаны на рис. 31.7. г- J* contents f d.pth f width ЭГ Depth 3f Width = Mtthodt ♦ Add "♦ Cont*ns ♦ Gwdtn ♦ GtlCtur ♦ GttPI»n ♦ Removt jji Апеннин * AbMnctCIo) • FwUl J> dipth J* vwdth • xPo, * yPof a ProptrtMi jf Otpth 2f «ar» It Width Hf xpoj ЗГ YPoj > Mtthodt V GirdtnConttnt | * GttPUn ^ . .I ■ . f ¥ Рш:. 3i. 7. Классы, включенные в состав библиотеки DocumentedClasses Основная идея этой библиотеки классов в том, что клиентское приложение может создавать экземпляр класса Garden (Сад), а затем добавлять в сад экземпляры классов, производных от GardenContent. Это можно использовать для получения простого графического представления сада. По существу классы образуют инструмент разработки очень обобщенного сада. На тот случай, если вы решите самостоятельно исследовать код, загружаемый код для данной главы содержит эту библиотеку классов, но его наличие вовсе не обязательно. Загружаемый код содержит также простое клиентское приложение DocumentedClassesClient, код которого имеет следующий вид: static void Main(string[] args) { int gardenWidth = 40; int gardenDepth = 20/ Garden myGarden = new Garden (gardenWidth, gardenDepth) ; myGarden. Add (new Bench D, 2)) ; myGarden. Add (new Bench A0, 2)) ; myGarden.Add(new SprinklerB0, 10)); myGarden .Add (new Tree C, 13)) ; myGarden. Add (new Tree B5, 7)) ; myGarden .Add (new Tree C5, 15)) ;
1008 Часть V. Дополнительные технологии myGarden. myGarden. myGarden. myGarden. myGarden. myGarden, myGarden. myGarden, myGarden, myGarden, myGarden. myGarden. Add(new Add(new Add(new Add(new Add(new Add(new Add(new Add(new Add(new Add(new Add(new Add(new FlowerC6, FlowerC7, FlowerC8, FlowerC9, FlowerC7, FlowerC8, FlowerC9, FlowerC7, FlowerC8, FlowerC9, StatueA6, 0)) 0)) 0)) 0)) D) D) D) 2)) 2)) 2)) 3)) BushB0, 17)) , char[,] plan = myGarden.GetPlan(); for (int у = 0; у < gardenDepth; y++) { for (int x = 0; x < gardenWidth; x++) { } Console.Write (plan[x, y]) ; Console.WriteLine(); } Console.ReadKey(); Этот пример демонстрирует использование классов в библиотеке классов DocumentedClasses, а его результирующий вывод показан на рис. 31.8. Рис. 31.8. Простое клиентское приложение DocumentedC'las sesClient Как видите, пример носит действительно очень общий характер! (К счастью, садовым дизайнерам доступны более совершенные средства.) Естественно, в данном случае нас интересует только XML-документация, содержащаяся в коде приложения DocumentedClasses. Применение XML-документации В этой главе несколько раз отмечалось, что XML-документация пригодна не только для предоставления информации IntelliSense среде IDE; это вовсе не означает, что возможность персональной настройки IntelliSense — чрезвычайно полезная функциональная возможность.
Глава 31. XML-документация 1009 Вероятно, простейшая задача, которую можно выполнять с XML-докумеп чашк-п ее поставка вместе со сборками и предоставление пользователям отыскание ( нос обог ее применения, но такой подход далек от идеала. В этом разделе мы рас с moi рич некоторые возможности максимально эффективного использования документации. Программная обработка XML-документации Наиболее очевидное применение XML-документации — использование обширного множества средств XML в пространствах имен .NET. Применение tcmkuoiiiii и классов, описанных в главе 25, позволяет загружать XML-документацию в обьекч XmlDocument. Следующий практический пример представляет собой про< тое коне о п>- ное приложение, решающее именно эту задачу. Это практическое занятие требует наличия выходной XML-документации, (ом)иииои при мером проекта DocumentedClasses. Прежде чем приступать к этому пракмичег hou\ ia нятию, загрузите и скомпонуйте решение DocumentedClasses. практическое занятие Обработка ХМL-документации 1. Создайте новое консольное приложение XMLDocViewer в каталоге С: \Bog'"J: 1 а Chapter31. 2. Добавьте в файл Program.cs оператор using для пространства имен Syv r . mi using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; 3. Измените код метода Main () следующим образом: static void Main(string[] args) { // Загрузка файла XML-документации. XmlDocument documentation = new XmlDocument () ; documentation.Load(@"..\..\..\..\DocumentedClasses" + @"\DocumentedClasses\bin\debug\DocumentedClasses.xml"); // Получение элементов <member>. XmlNodeList memberNodes = documentation. SelectNodes ("//member") ; // Извлечение элементов <member> для типов. List <XmlNode> typeNodes = new List <XmlNode> () ; foreach (XmlNode node in memberNodes) { if (node. At tributes ["name"] . Value. StartsWi th ("T")) { typeNodes. Add (node) ; > } // Запись типов на консоль. Console.WriteLine("Types:"); foreach (XmlNode node in typeNodes) { Console. WriteLine ("- {0}", node. At tributes ["name"] .Value. Substring B)) ; } Console.ReadKey(); }
1010 Часть V. Дополнительные технологии 4. Запустите приложение. Результат его работы показан на рис. 31.9. Рис. 31.9. Работа приложения XMLDocViewer Описание полученных результатов Этот пример демонстрирует основы загрузки и обработки файла XML-документации в коде С#. Обработка начинается с загрузки файла документации, которым в данном случае является файл, сгенерированный библиотекой классов DocumentedClasses: XmlDocument documentation = new XmlDocument(); documentation.Load(@"..\..\..\..\DocumentedClasses" + @"\DocumentedClasses\bin\debug\DocumentedClasses.xml"); Обратите внимание, что примененная здесь конкатенация строк не обязательна, но она упрощает чтение кода на узкой странице. Затем посредством использования простого XPath-выражения //member код получает список всех элементов <member>: XmlNodeList memberNodes = documentation.SelectNodes("//member"); После того как список элементов <member> получен, в нем можно выполнить поиск тех, которые начинаются с буквы Т, для извлечения элементов, относящихся к типам, и их помещения в коллекцию List<XmlNode>: List<XmlNode> typeNodes = new List <XmlNode> (); foreach (XmlNode node in memberNodes) { if (node.Attributes["name"].Value.StartsWith("T")) { typeNodes.Add(node); } } Конечно, этого же эффекта можно было бы достичь в одном действии, используя более сложное выражение XPath, которое включает проверку атрибутов, но зачем излишне усложнять код? И, наконец, имена найденных типов выводятся на консоль: Console.WriteLine("Types:"); foreach (XmlNode node in typeNodes) { Console.WriteLine("- {0 } ", node.Attributes["name"].Value.SubstringB)); } Можно было бы выполнить значительно больше задач, поскольку в среде .NET отыскание и обработка XML-файлов — простой процесс. Однако поскольку, как будет показано в следующих двух разделах, существуют более совершенные способы обработки XML-документации, нет никакого смысла углубляться в этот пример.
Глава 31. XML-документация 1011 Стилевое оформление XML-документации с помощью XSLT Поскольку по определению XML-документация — код XML, существует огромный простор для использования XSLT-трансформаций для преобразования XML-кода в текст HTML или даже для использования форматирующих объектов XSLT для создания печатных документов. На этом этапе мы хотели бы сослаться на настоящего мастера С# — Андерса Хейлсберга (Anders Hejlsberg). Он опубликовал документ XSLT и связанный с ним файл CSS, предназначенные для преобразования файлов XML-документации в стилизованный HTML-текст. Эти два файла, doc.xsl и doc.ess, можно найти в загружаемом коде данной главы в каталоге XSLTStyledXMLDocumentation. Чтобы использовать эти файлы, просто добавьте директиву обработки в свои файлы XML-документации: <?xml version=.0"?> <?xml:stylesheet href ="doc.xsl" type="text/xsl"?> <doc> </doc> В каталоге XSLTStyledXMLDocumentation вы найдете также версию файла DocumentedClasses .xml, включающую в себя эту директиву. Пример результата показан на рис. 31.10. DocumentedClasses.Garden Class representing a garden, and the of that garden. Kemarfcs Use the AJilCuJtflCCtiai'Wi'J method to add contents, which must inherit from iajCftli^illtui Once contents have been added the garden can be visualised using the itJPUi J method. SmtatJe classes for adding include Class Description Hgwej Represents a flower, toe 1*1 If.it Represents a tree, size 2*2 a bush, see 2x2. ts й ■ 1<1 ЗДЦШ Represents e statue, sin 1*1 utDCh Represents a bench, size ML The following code adds a flower to middle of a garden: «■ OecdenlU, 11I Addloav rioeatO, 5)) i |Garden(Int32,fnt3?) constructor Constructor that s Parameter* s the dimensions of the Cardan object Depth of the cardan object (size on y-axis). Рис. 31.10. Стилизация XML-документации Стилевое оформление не идеально — в частности, элементы <seealso> отображаются неправильно и, как видно из снимка экрана, существуют проблемы с форматированием элемента <code>, — но в целом результат достаточно впечатляет. Файлы служат прекрасной отправной точкой при создании Web-страницы документации. Особенно удачен способ связывания элементов <see> с точками привязки на странице.
1012 Часть V. Дополнительные технологии Вероятно, наибольшая проблема, связанная с этим подходом, состоит в том, что вся информация отображается на одной HTML-странице, которая может оказаться очень большой. Средства документирования Другой способ обработки XML-документации — использования специального средства. До недавнего времени наиболее совершенным средством стилевого оформления XML-документации была программа NDoc. Она представляет собой инструментальное средство от независимых разработчиков, позволяющее преобразовывать документацию в ряд форматов, в том числе в справочные файлы в стиле MSDN. К сожалению, разработка NDoc застопорилась (на момент написания этой книги). Для получения последней версии (бесплатной) NDoc обратитесь на сайт http:// ndoc.sourceforge.net. Хотя она и не модернизировалась в течение некоторого времени, программа остается работоспособной (при использовании каркаса .NET Framework версии 1.x). Не исключено, что в будущем проект получит новую жизнь, поэтому время от времени стоит заглядывать на этот сайт. Пример NDoc-документа- ции, форматированной в стиле справки, показан на рис. 31.11. I Щ Ш •> ; Hide Locale Вес* Г . £orttrt> id*. |se*ct.| ' Type nine keyword to Ind О @ fl i"» GeidenContenl Ren property GetdenContent Wd№ property GetdenContent XPot property GardenC anient YPot property GetdenContert bJm B«oUy J WP4P^ Class representing a gerden, end the contents of thet garden. For e list of ell members of this type, see Garden. Hembers. I.irdrn р«Ы .< < 1... ttid.. Thread Safety Public static (Shared in Visuel Basic) members of this type ere sefe for multithreaded operations Instance members ere not guaranteed to be thread-safe it inherit from GardenC ontent. be visualised using the Gatflan i Use the Add method to add contents, which rr Once contents heve been edded the gerden с Suitable classes for adding include dm Bir.it: : SprinHei Bench MMWM Represents e flower, size lxl. Represents a tree, site 2x2 Represents e sprinkler, size lxl. Represents e statue, sue lxl Represents e bench, sue 5x2. The following code edds e flower to the middle of e garden: Namespace: !.<>'«'>ч:'/_;..;• • Assembly: DocumentedClesses (in OocumentedClesses.dll) Garden Members I OocumentedClaises Namespace | Рис. 31.11. Пример NDoc-документации В случае возможного окончательного исчезновения NDoc стоит обратить внимание на альтернативное средство — Sandcastle. Компания Microsoft использует этот инструмент для генерирования документации по API, поэтому не приходится удивляться, что оно предоставляет все функциональные возможности, которые от него можно ожидать. Оно полностью поддерживает последнюю версию .NET Framework и функциональные возможности языка С#, в том числе, например, обобщения, не поддерживаемые программой NDoc.
Глава 31. XML-документация 1013 Однако поскольку Sandcastle, по сути, представляет собой внутреннее < ре к мю его использование — не самая простая задача. Для него существует нес ко м.к<> (.1 I большинство из которых были созданы его пользователями. Более подробную информацию об этом программном средстве можно пли ти на сайте www.sandcastledocs.com или http://blogs.msdn.com/:ч: I Пользующийся популярностью GUI для Sandcastle можно также загр\.шп> и \ с am a www.codeplex.com/SHFB. Резюме В этой главе мы рассмотрели все аспекты XML-документации — от ее с олдаппя м > возможного применения. В этой главе рассмотрены следующие вопрос ы. □ Синтаксис XML-документации. □ Способы включения внешней XML-документации. □ Способы использования XML-документации в средствах IntelliSense и Objeii Browser среды Visual Studio. □ Использование диаграмм классов для добавления XML-документацпп □ Способы генерирования файлов XML-документации. □ Способы обработки файлов XML-документации в коде С#. □ Способы стилевого оформления XML-документации с помощью XSL1 □ Способы применения таких программных средств, как NDoc и Sanch.isile, д м\ построения справочных файлов из XML-документации. Честно говоря, добавление XML-документации в проекты может окала чы я дос i.i точно трудоемким процессом и придавать исходному коду несколько занукпшый \и\ у Но в крупномасштабных проектах документирование — важная часть нроцес < а разработки, и результат полностью оправдывает затраченные усилия — особенно при ж пользовании средств, разработанных для этой цели. Упражнения 1. Какие из следующих элементов можно использовать в ХМ1.-док)мен гации? • <summary> • <usage> • <member> • <seealso> • <typeparamref> 2. Какие дескрипторы XML-документации верхнего уровня следуем ж пользован» для документирования свойства? 3. XML-документация содержится в файлах исходного кода С#. Корректно дли >к> утверждение? 4. Каков основной недостаток добавления XML-документации иосредс ibom дна граммы классов? 5. Что необходимо сделать, чтобы обеспечить возможность использования др\ • и ми проектами XML-документации, содержащейся в библиотеках клас с он?
Передача данных по сети Глава 21 была посвящена высокоуровневой технологии передачи данных по сети — Web-службам. Вы узнали, как посредством ориентированных на SOAP и .NET протоколов пересылать сообщения от клиента серверу независимым от платформы способом. А в этой главе мы рассмотрим более низкие сетевые уровни, выполняя программирование с помощью классов пространства имен System.Net. Сами Web-службы используют эту технологию. Глава начинается с обзора программирования клиентских и серверных приложений с помощью классов пространств имен System. Net и System. Net .Sockets, которые используют такие протоколы, как HTTP, TCP и UDP Мы рассмотрим как ориентированные на соединение приложения, использующие протокол TCP, так и не зависящие от соединения приложения, применяющие протокол UDP. В этой главе будут рассматриваться следующие темы. □ Обзор сетевой передачи. □ Параметры программирования сетевой передачи. □ Использование WebRequest. □ TcpListener и TcpClient. □ Программирование сокетов. Обзор сетевой передачи Сетевая передача — это обмен данными с приложениями в других системах. Обмен данными осуществляется путем отправки сообщений. Сообщения могут отправляться единственной системе, когда соединение инициируется перед отправкой сообщения, как показано на рис. 32.1, или же они могут отправляться нескольким системам с помощью широковещательной рассылки, как показано на рис. 32.2. При широковещательной рассылке соединения не инициируются; вместо этого сообщения отправляются в саму сеть.
Глава 32. Передача данных по сети 1015 Получатель Отправитель Рис. 32.1. Установка соединения Получатель Получатель Получатель Отправитель Получатель Рис. 32.2. Широковещательная рассылка Нагляднее всего организацию сети можно проиллюстрировать семиуровневой моделью OSI (Open Systems Interconnection — взаимодействие открытых систем). Стек уровней OSI и соответствующие уровни TCP/IP показаны на рис. 32.3. Часто уровни обозначают также простыми последовательными номерами — от нижнего, в котором физическому уровню присвоен номер 1, до верхнего, в котором уровню приложения присвоен номер 7.
1016 Часть V. Дополнительные технологии Прикладной Презентационный Сеансовый Транспортный Сетевой Канала передачи данных Физический HTTP FTP SMTP TCP DNS UDP Протокол Internet Ethernet Рис. 32.3. Модель OSI Самым нижним уровнем OSI является физический уровень. На нем определены физические устройства (такие как сетевые платы и кабели). Уровень канала передачи данных осуществляет доступ к физической сети по физическим адресам. Этот уровень выполняет исправление ошибок, управление потоком и адресацию оборудования. Адрес сетевой платы Ethernet отображается при использовании команды ipconfig /all. Система, представленная следующим примером, имеет аппаратный адрес (MAC address) 00-13-02-38-0D-5C: С:\> ipconfig /all Windows IP Configuration Host Name : farabove Primary Dns Suffix : explorer. local Node Type : Hybrid IP Routing Enabled : No WINS Proxy Enabled : No DNS Suffix Search List : explorer. local kabsi.at Wireless LAN adapter Wireless Network Connection: Connection-specific DNS Suffix . : kabsi.at Description : Intel(R) PRO/Wireless 3945ABG Network Connection Physical address : 00-13-02-38-0D-5C DHCP Enabled : Yes Autoconfiguration Enabled . . . .: Yes Link-local IPv6 Address : fe80:bd:3d27:4106:9f4c%10(Preferred) IPv4 Address : 192.168.0.31(Preferred) Subnet Mask : 255.255.255.0 Lease Obtained : Sunday, July 29, 2007, 3:10:08 PM Lease Expires : Wednesday, August 01, 2007, 3:10:08 PM Default Gateway : 192.168.0.6 DHCP Server : 192.168.0.6 DHCPv6 IAID : 167777026 DNS Servers : 192.168.0.10 195.202.128.2 NetBIOS over Tcpip : Enabled
Глава 32. Передача данных по сети 1017 Сетевой уровень использует логический адрес для адресации систем в глобальной сети (WAN). Протокол Internet (Internet Protocol — IP) — это протокол третьего уровня. На этом уровне IP-адрес используется для адресации других систем. В спецификации IPv4 IP-адрес состоит из четырех байт — например, 192 .14 . 5.12. Транспортный уровень используется для идентификации приложений, обменивающихся данными. Приложения могут идентифицироваться по конечным точкам. Серверное приложение, ожидающее подключения клиентов, обладает известной конечной точкой, к которой должно быть выполнено подключение. Протокол управления передачей (Transmission Control Protocol — TCP) и протокол дейтаграмм пользователя (User Datagram Protocol — UDP) являются протоколами четвертого уровня, которые используют номер порта (конечной точки) для идентификации приложения. Протокол TCP используется для надежных коммуникаций, при которых соединение устанавливается перед отправкой данных, тогда как протокол UDP обеспечивает ненадежные коммуникации, поскольку данные отправляются без гарантии, что они будут получены. Над четвертым уровнем модель OSI определяет сеансовый, презентационный и прикладной уровни. Сеансовый уровень определяет службы для приложения, такие как вход и выход. Сеансовый уровень позволяет устанавливать виртуальное соединение между приложениями. Его можно сравнить с сеансами ASP.NET, освещенными в главе 18. Презентационный уровень связан с форматированием данных — именно на этом уровне происходит шифрование, дешифрация и сжатие. И, наконец, прикладной уровень — самый верхний уровень, предоставляющий приложениям сетевые функциональные возможности, такие как передача файлов, электронная почта, просмотр Web- страниц и т.п. В сочетании с набором протоколов TCP/IP протоколы прикладного уровня охватывают уровни 4-7 модели OSI. Примерами этих протоколов служат протокол передачи гипертекста (Hypertext Transfer Protocol — HTTP), протокол передачи файлов (File Transfer Protocol — FTP) и простой протокол передачи почты (Simple Mail Transfer Protocol — SMTP). На транспортном уровне конечные точки служат для доступа к другим приложениям. Эти прикладные протоколы определяют вид данных, пересылаемых другим системам. Позднее вы увидите, какие данные пересылаются посредством протокола HTTP. Преобразование имен Протокол Internet Protocol требует указания IP-адресов. Поскольку IP-адреса трудны для запоминания (их запоминание еще труднее при использовании спецификации IPv6, в которой IP-адреса состоят из 128 битов, а не из 32), в приложениях используется имя хоста. Однако, как уже было указано, для обмена данными между системами необходим IP-адрес. Для преобразования имени хоста в IP-адрес используются серверы DNS (Domain Name System — система доменных имен). Windows предлагает утилиту командной строки nslookup, которая может выполнять поиск имен (преобразование имен хостов в IP-адреса) или обратный поиск (преобразование IP-адресов в имена хостов). Обратные поиски представляют особый интерес, поскольку, анализируя журнальные файлы на предмет размещения IP-адресов клиентской системы, можно определить происхождение клиентской системы. .NET позволяет выполнять поиск имен с помощью класса Dns из пространства имен System.Net. Класс Dns позволяет преобразовывать имена хостов в их IP-адреса или IP-адреса в их имена хостов. Выполним это на примере простого проекта.
1018 Часть V. Дополнительные технологии Практическое занятие Использование DNS 1. Создайте новый проект консольного приложения С# по имени DnsLookup в каталоге С:\BegVCSharp\Chapter32. 2. Импортируйте пространство имен System.Net. 3. Вызовите метод GetHostEntry () класса Dns, как показано в ниже, после проверки аргументов метода Main (): static void Main(string[] args) { if (args.Length != 1) { Console.WriteLine ("Использование: DnsLookup имя_хоста/IP-адрес") ; return; } IPHostEntry ipHostEntry = Dns. GetHostEntry (args [0]) ; 4. В код метода Main () после вызова метода GetHostEntry () добавьте следующий код для записи информации о преобразованном хосте на консоль: Console.WriteLine("Хост: {0}", ipHostEntry.HostName); if (ipHostEntry.Aliases.Length> 0) { Console.WriteLine("\пПсевдонимы:"); foreach (string alias in ipHostEntry.Aliases) { Console.WriteLine(alias); } } Console.WriteLine("\nAflpec(a):"); foreach (IPAddress address in ipHostEntry.AddressList) { Console.WriteLine("Адрес: {0}", address.ToString()); } } 5. Скомпилируйте приложение и запустите программу. Передайте имя хоста в качестве аргумента командной строки — например, www.microsoft. com. Чтобы запустить программу из среды Visual Studio, аргументы командной строки можно установить в конфигурации отладки, как показано на рис. 32.4. Можно также запустить окно командной строки, изменить текущий каталог на каталог исполняемого файла и запустить приложение командой dnslookup www.microsoft.com. При этом вы должны получить вывод, подобный следующему: Хост: lbl.www.ms.akadns.net Адрес(а): Адрес: 207.46.193.254 Адрес: 207.46.19.190 Адрес: 207.46.19.254 Адрес: 207.46.192.254
Глава 32. Передача данных по сети 1019 Dnsloofcup Program и Appfcattan Mi £pnft<M*on Actrve (Debug) '^] Platform Active (Any CPU) 3 *E~* 0S*prolect Secirty Pubkh О Start e&tamal program: О Start Ъктш Ш ЦЦ.: Start Options Command Ine arguments: ***** microsoft com WortingoVectory: П U» remote machine [7] Enable ynmanaged code debugging Enabte 5QC Server debuggng Рис. 32.4. Установка аргумента командной строки Описание полученных результатов Класс Dns запрашивает DNS-сервер для преобразования имени хоста в IP-адрес и для обратного поиска с целью получения имени хоста по IP-адресу. Для этого класс Dns использует метод GetHostEntry (), в аргументах которого можно передать имя хоста, и который возвратит объект IPHostEntry. Для выполнения обратных поисков можно вызвать метод GetHostByAddress (). Во всех случаях класс Dns возвращает IPHostEntry. Этот класс содержит информацию о хосте. Класс IPHostEntry имеет три свойства: HostName возвращает имя хоста, Aliases — список всех псевдонимов, a AddressList — массив элементов IPAddress. Метод ToString () класса IPAddress возвращает Internet-адрес в стандартной форме записи. Унифицированный идентификатор ресурса Ресурсы, доступные по сети, описываются идентификатором URI (Uniform Resource Identifier — унифицированный идентификатор ресурса). Вы используете идентификаторы URI при каждом вводе Web-адресов наподобие http: //www. thinktecture. com в Web-браузере. Класс Uri инкапсулирует URI и обладает свойствами и методами, предназначенными для анализа, сравнения и объединения идентификаторов URI. Объект Uri можно создать, передавая строку URI конструктору: Uri uri = new Uri("http://www.wrox.com/go/p2p"); При переходе к другим страницам одного и того же сайта можно использовать базовый идентификатор URI и на его основе создавать идентификаторы URI, содержащие каталоги: Uri baseUri = new Uri("http://msdn.microsoft.com"); Uri uri = new Uri(baseUn, "downloads"); Доступ к частям URI можно получить, используя свойства класса Uri. В табл. 32.1 приведено несколько примеров информации, которую можно получить из объекта Uri, инициализированного URI http://www.wrox.com/marketbasket. cgi?isbn=0470124725.
1020 Часть V. Дополнительные технологии Таблица 32.1. Примеры значений свойств объекта Uri Свойство объекта Uri Результат Scheme http Host www.wrox.com Port 80 LocalPath /marketbasket.cgi Query ?isbn=0470124725 Протоколы TCP и UDP Теперь, когда вы знаете, как получить IP-адрес из имени хоста, можно приступить к рассмотрению функционирования протоколов TCP и UDR Оба эти протокола являются протоколами транспортного уровня, как было показано на рис. 32.3. Оба они используют номер порта для идентификации приложения, которое будет принимать данные. Протокол TCP ориентирован на соединение, в то время как UDP не зависит от соединения. Протокол TCP требует создания серверным приложением сокета с известным номером порта, чтобы клиентское приложение могло подключиться к серверу. Клиент создает сокет с общедоступным номером порта. Когда клиент подключается к серверу, он передает серверу свой номер порта, чтобы серверу был известен путь к клиенту. После того как соединение установлено, клиент получает возможность отправлять данные, принимаемые сервером, после чего сервер может отправлять данные, принимаемые клиентом. Естественно, отправка и прием данных могут происходить и в обратном порядке. При использовании протокола QOTD (quote of the day — цитата дня) (сервер QOTD — часть службы Simple TCP/IP Services (Простая служба TCP/IP) компонента Windows) сервер просто отправляет определенные данные после подключения клиента, а затем закрывает соединение. Протокол UDP подобен TCP в том, что сервер должен создать сокет с известным номером порта, а клиент использует общедоступный номер порта. Различие между ними в том, что клиент не инициирует соединение. Вместо этого клиент может отправлять данные, не устанавливая вначале подключение. При отсутствии соединения пет никакой гарантии, что данные вообще принимаются, но передача в целом происходит быстрее. Большое преимущество протокола UDP состоит в том, что с его помощью можно осуществлять широковещательные рассылки — рассылку информации всем системам в локальной сети (LAN) посредством широковещательного адреса. Широковещательные адреса — это IP-адреса, в которых все разряды хостчасти IP-adpeca установлены в 1. Прикладные протоколы В этом разделе рассмотрены прикладные протоколы, использующие TCP или UDP. HTTP — это прикладной протокол, расположенный поверх TCP. Протокол HTTP определяет сообщение, пересылаемое по сети. При использовании протокола HTTP для запроса данных с Web-сервера вначале открывается TCP-соединение, после чего отравляется HTTP-запрос. Пример HTTP- запроса, инициированного браузером, выглядит подобно показанному ниже:
Глава 32. Передача данных по сети 1021 GET /default.aspx HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, app1lcatlon/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-xbap, application/x-Shockwave-flash, * / • Accept-Language: de-AT,en-US;q=0.7 Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; .NET CLR 2.0.50727; Media Center PC 5.0; Tablet PC 2.0; .NET CLR 1.1.4322; .NET CLR 3.5.20706; .NET CLR 3.0.590) Host: localhost:80 Connection: Keep-Alive Этот пример запроса содержит команду GET. Некоторые из наиболее часто используемых команд HTTP — GET, POST и HEAD. Команда GET служит для запроса файла с сервера. Команда POST также запрашивает файл с сервера, но, в отличие от команды GET, при ее использовании вслед за заголовком HTTP отправляются дополнительные данные. При использовании команды HEAD сервер возвращает только информацию заголовка файла, чтобы клиент мог определить, отличается ли данный файл от данных, уже хранящихся в кэше. Команда GET запрашивает файл с сервера. В приведенном примере сервер должен возвратить файл /default .aspx. Последняя часть первой строки определяет версию протокола HTTP. Если и клиент, и сервер поддерживают версию 1.1, именно эта версия используется для обмена данными. В противном случае применяется версия HTTP 1.0. Версия HTTP 1.1 позволяет сохранять то же соединение (см. информацию HTTP-заголовка Connection: Keep-Alive). За первой строкой запроса с помощью команды GET следует информация HTTP- заголовка. Вместе с запросом браузер отправляет информацию о себе. В приведенном примере присутствует информация Accept, в которой отправляются MIME-типы поддерживаемых программ. Заголовок Accept-Language определяет языки, сконфигурированные в браузере. Сервер может использовать эту информацию для возвращения клиенту различной информации в зависимости от поддерживаемых файлов и языков. С помощью заголовка User-Agent браузер отправляет информацию о клиентском приложении, использованном для запроса страницы с сервера. В данном случае применяется браузер Internet Explorer 7.0 в сочетании с операционной системой Windows Vista. Идентификатор этой операционной системы — Windows NT 6.0. Сервер может также выяснить, установлена ли в клиентской системе исполняющая среда .NET. В данном случае поддерживаемыми версиями .NET являются .NET 1.1, .NET 2.0, .NET 3.0 и .NET 3.5. После того, как сервер принимает GET-запрос, он возвращает ответное сообщение. Пример ответного сообщения показан ниже. В случае успешного запроса первая строка ответа содержит состояние ОК и используемую версию протокола HTTP. За ними следует информацию заголовка HTTP, которая включает в себя элементы Server, Date и Content-Type. За заголовком следует информация о длине содержимого. Заголовок и содержимое разделены двумя пустыми строками. НТТР/1.1 200 ОК Server: Microsoft-IIS/7.0 Date: Sun, 29 Jul 2007 20:14:59 GMT X-Powered-By: ASP.NET X-AspNet-Version: 2.0.50727 Cache-Control: private Content-Type: text/html; charset=utf-8 Content-Length: 991
1022 Часть V. Дополнительные технологии <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <HTML> <HEAD> <title>Demo</title> HTTP-запрос легко имитировать с помощью утилиты telnet, что и будет сделано в следующем практическом занятии. Практическое занятие Имитация HTTP-Запроса 1. Запустите клиента Telnet, введя telnet. exe в командной строке. Отобразится приглашение Microsoft Telnetx В среде Windows Vista клиент Telnet по умолчанию не устанавливается. Если в данной системе он отсутствует, выберите пункт меню Control PanelsPrograms^Turn Windows Features On or Off (Панель управления1^ Программы ^Включение или отключение компонентов Windows), а затем установите флажок Telnet Client (Клиент Telnet), чтобы инсталлировать его. 2. Чтобы видеть команды, вводимые и отправляемые серверу, в сеансе Telnet введите команду set localecho. 3. Создайте соединение с Web-сервером в локальной сети. Если Web-сервер присутствует в локальной сети, введите команду open localhost 80. Значение 80 — это номер порта, по умолчанию используемый Web-сервером. 4. Отправьте запрос Web-серверу, введя команду GET / НТТР/1.1, а затем два раза нажав клавишу <Enter>. Аргумент / вызывает возврат сервером страницы, заданной по умолчанию — например, default .htm. Вместо / можно запросить конкретные имена файлов. Отправка двух символов новой строки обозначает конец передачи. После этого вы получите ответ сервера, подобный показанному ранее. Варианты программирования сетевой передачи Пространства имен System.Net и System.Net. Sockets предлагают несколько вариантов для программирования сетевой передачи. Они показаны на рис. 32.5. Простейший способ программирования сетевой передачи предусматривает применение класса WebClient. Для получения файлов с Web-сервера или для передачи файлов на FTP-сервер требуется единственный метод этого класса. Однако функциональные возможности этого класса ограничены. Версия .NET 3.5 позволяет его использовать только с протоколами HTTP и FTP и для доступа к файлам. Конечно, можно подключать пользовательские классы для поддержки других протоколов. Сам класс WebClient использует классы WebRequest и WebResponse. Эти классы предоставляют больше функциональных возможностей, но более сложны в применении. Если требуется создать сервер, для этого нельзя использовать классы WebClient или WebRequest. Для этой цели нужно применять класс TcpListener из пространства имен System. Net .Sockets. Класс TcpListener можно использовать для создания сервера для протокола TCP, a TcpClient предназначен для создания клиентских приложений. Эти классы не ограничены только протоколами HTTP и TCP — можно пользоваться любым протоколом на основе TCP. При необходимости применения протокола UDP для создания UDP-серверов и клиентов используют классы UdpListener и UdpClient (которые подобны классам TcpListener и TcpClient).
Глава 32. Передача данных по сети 1023 о о. WebClient WebRequest, WebResponse TcpClient TcpListener System. Net. Sockets Клиент Сервер Рис. 32.5. Варианты программирования сетевой передачи Если требуется независимость от протокола или большая степень контроля над протоколами TCP и UDP, можно выполнить программирование сокетов средствами .NET. Классы для программирования сокетов определены в пространстве имен System.Net.Sockets. WebClient Мы начнем с рассмотрения самого простого в использовании класса — WebClient. Этот класс представляет собой компонент, который можно перетащить из панели инструментов в приложение Windows Forms. Его можно использовать с клиентскими приложениями для получения доступа к HTTP- или FTP-серверам или для доступа к файлам в файловой системе. Основные свойства и методы класса WebClient описаны в табл. 32.2. Таблица 32.2. Основные свойства и методы класса WebClient Свойства класса WebClient Описание BaseAddress Credentials UseDefaultCredentials Encoding Headers Proxy Свойство BaseAddress определяет базовый адрес запроса, выполняемого объектом WebClient Свойство Credentials позволяет передавать идентификационную информацию — например, с использованием класса NetworkCredential Если нужно использовать идентификационные сведения зарегистрированного в данный момент пользователя, установите значение свойства UseDefaultCredentials равным true Свойство Encoding можно использовать для установки кодирования строк, предназначенных для выгрузки и загрузки Свойство Headers позволяет определять коллекцию webHeaderCollection, содержащую информацию заголовка, характерную для используемого протокола По умолчанию для доступа в Internet используется прокси-сервер, сконфигурированный в Internet Explorer. При необходимости работы с другим прокси-сервером, с помощью свойства Proxy можно определить объект WebProxy
1024 Часть V. Дополнительные технологии Окончание табл. 32.2 Свойства класса WebClient Описание Aiders Если нужно отправить строку запроса Web-серверу, это можно выполнить, устанавливая объект NameValueCollection с помощью свойства QueryString После отправки запроса информацию заголовка ответа можно прочитать внутри класса WebHeaderCollection, связанного с классом ResponseHeaders Юл( с WebClient предоставляет простые методы выгрузки и загрузки файлов, опи- i .iiihnc и габл. 32.3. Таблица 32.3. Методы выгрузки и загрузки файлов класса WebClient Методы класса WebClient Описание -<='* }d ta () Метод DownioadData () позволяет передавать строку Web-адреса, дописываемую к базовому адресу BaseAddress, а метод возвращает массив байтов, содержащий данные, полученные с сервера *1 ;_ring() Метод DownloadString () аналогичен методу DownioadData (); единственное различие между ними состоит в том, что возвращенные данные хранятся внутри строки г <i 1и i 1 е () Если данные, возвращенные с сервера, должны храниться внутри файла, метод DownloadFile () выполняет все необходимые действия. Аргументы, устанавливаемые им — Web-адрес и имя файла f j' И) Метод OpenRead () также загружает данные с сервера, но загруженные данные могут считываться из потока, возвращенного этим методом 1 с О Метод uploadFile () можно использовать для выгрузки файлов на FTP- или Web-сервер. Этот метод допускает также передачу HTTP-метода, который должен использоваться при выгрузке j () В то время как метод UploadFile () позволяет выполнять выгрузку файлов из локальной файловой системы, метод uploadData () отправляет серверу массив байтов ! г г q () Метод UploadString () можно использовать для выгрузки строки на сервер 1 / < 1'JH О Метод UploadValues () можно использовать для выгрузки объекта NameValueCollection на сервер •'•»•" * () Метод OpenWnte () отправляет содержимое серверу, используя при этом поток В ( к'д\1ощем практическом занятии мы загрузим файл из Web с помощью класса практическое занятие Использование класса WebClient 1 (дмдайте новый проект консольного приложения по имени WebClientDemo в КсИ.ыогс C:\BegVCSharp\Chapter32.
Глава 32. Передача данных по сети 1025 2. Импортируйте пространство имен System.Net. 3. Добавьте следующий код в метод Main (): static void Main() { WebClient client = new WebClientO; client.BaseAddress = "http://www.microsoft.com"; string data = client.DownloadString("Office"); Console.WriteLine(data); Console.ReadLine(); } 4. Скомпилируйте и запустите приложение. Описание полученных результатов После создания экземпляра класса WebClient значение свойства BaseAddress устанавливается равным http: //www.microsoft. com. Оно представляет левую часть адреса, используемого во всех последующих запросах. Вызов метода DownloadString ("Of f ice") запрашивает страницу http://www. microsoft.com/Office. Все возвращенные данные сохраняются в переменной data и выводятся на консоль. Классы WebRequest и WebResponse Для обмена данными с Web- или FTP-сервером вместо класса WebClient можно применять класс WebRequest. Сам класс WebClient скрытым образом использует класс WebRequest. Класс WebRequest всегда используется в сочетании с классом WebResponse. Вначале, конфигурируя объект WebRequest, нужно определить запрос, который будет отправлен серверу. Затем можно вызвать метод GetResponse, который отправляет запрос серверу и возвращает его ответ в классе WebResponse. Классы WebRequest и WebResponse являются абстрактными и поэтому не используются непосредственно. Как показано на рис. 32.6, они являются базовыми классами, а пространство имен System.Net предоставляет конкретные реализации протоколов HTTP, FTP и файловых протоколов. WebRequest i GetResponse() ^\ WebResponse HttpWebRequest FtpWebRequest i HttpWebResponse FtpWebResponse FileWebRequest FileWebResponse Puc. 32.6. Диаграмма классов WebRequest и WebResponse
1026 Часть V. Дополнительные технологии Конкретные реализации этих базовых классов — HttpWebRequest, FtpWebRequest, FileWebRequest, HttpWebResponse, FtpWebResponse и FileWebResponse. Протокол HTTP используется классами HttpWebXXX, классы FtpWebXXX применяют протокол FTP, а классы FileWebXXX обеспечивают доступ к файловой системе. Создание FTP-клиента с помощью класса FtpWebRequest — простая задача. В следующем практическом занятии мы создадим Windows-приложение, которое загружает файлы с FTP-сервера. Получение файла с FTP-сервера 1. Создайте новое WPF-приложение по имени FtpClient в каталоге С: \BegVCSharp\ Chapter32. 2. Переименуйте файл Windowl.xaml в FtpClientWindow.xaml. Переименуйте класс Windowl в FtpClientWindow. В файле App.xaml измените ссылку на FtpClientWindow.xaml. 3. В главное окно добавьте элементы управления, показанные на рис. 32.7. л Рис. 32.7. Главное окно приложения FtpClien t Элементы управления этой формы и настройки их свойств приведены в табл. 32.4. Таблица 32.4. Значения свойств элементов управления Тип элемента управления Имя Свойство FtpClientWindow ListBox Label Label Label TextBox TextBox PasswordBox Button Button Button Checkbox listFiles labelServer labelUserName labelPassword textServer textUserName passwordBox buttonOpen buttonOpenDirectory buttonGetFile checkBoxBinary Title="FTP Client" ItemsSource="{Binding}" Content="Server:" Content="Username:" Content="Password:" Text="ftp://" Text="Anonymous" Content="Open" Content="Open Directory" Content="Get File" Content="Binary Mode"
Глава 32. Передача данных по сети 1027 XAML-код определения окна имеет следующий вид: <Window x:Class="FtpClient.FtpClientWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FTP Client" Height=00" Width="800"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width=50" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height=0" /> <RowDefinition Height=0" /> <RowDefinition Height=0" /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefimtions> <Label HorizontalAlignment="Right" Grid.Column=" Grid.Row=" Name="labelServer" Margin=,6,6,6"> Server: </Label> <Label HorizontalAlignment="Right" Grid.Column=" Grid'.Row="l" Name="labelUserName" Margin=,6,6,6"> Username: </Label> <Label HorizontalAlignment="Right" Grid.Column=" Grid.Row=" Name="labelPassword" Margin=,6,6,6"> Password: </Label> <TextBox Grid.Column="l" Grid.Row=" Name="textServer" Text="ftp://" Margin=,6,6,6" /> <TextBox Grid.Column="l" Grid.Row="l" Name="textUserName" Text="Anonymous" Margin=,6,6,6" /> <PasswordBox Grid.Column="l" Grid.Row=" Name="passwordBox" Margin=,6,6,6" /> <ListBox ItemsSource="{Binding}" Margin=2,12,12,12" x:Name="listFiles" Grid.Column=" Grid.Row=" Grid.ColumnSpan=" Grid.RowSpan=" /> <Button Grid.Column=" Grid.Row=" x:Name="buttonOpen" Margin=,6,6,6"> Open </Button> <Button Grid.Column=" Grid.Row="l" x:Name="buttonOpenDirectory" IsEnabled="False" Margin=,6,6, 6"> Open Directory </Button> <Button Grid.Column=" Grid.Row=" x:Name="buttonGetFile" IsEnabled="False" Margin=,6,6,6"> Get File </Button> <CheckBox HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column=" Grid.Row=" x:Name="checkBoxBinary" Margin="8,8,8,8"> Binary Mode </CheckBox> </Grid> </Window> 4. Используя редактор свойств Property Editor, установите значение свойства IsEnabled кнопок buttonGetFile и buttonOpenDirectory равным false. Эти кнопки активны только при выбранном файле. 5. В файле кода импортируйте пространства имен System.Net, System. 10 и Microsoft.Win32. 6. Добавьте переменную serverDirectory в качестве приватного члена класса FtpClientWindow: public partial class FtpClientWindow : Window { private string serverDirectory;
1028 Часть V. Дополнительные технологии 7. Добавьте вспомогательный метод Fi 11 Direct or yList в класс FtpClientWindow. Этот метод заполняет listBox всеми данными, находящимися внутри аргумента stream: private void FillDirectoryList(Stream stream) { StreamReader reader = new StreamReader(stream); string content = reader.ReadToEnd(); string [] files = content.Split(f\n'); this.DataContext = files; reader.Close(); } 8. Для создания начального соединения с сервером добавьте обработчик события Click по имени buttonOpen_Click к кнопке buttonOpen. Добавьте реализацию обработчика, которая показана ниже: private void buttonOpen_Click(object sender, RoutedEventArgs e) { Cursor currentCursor = this.Cursor; FtpWebResponse response = null; Stream stream = null; try { this.Cursor = Cursors.Wait; // Создание объекта FtpWebRequest. FtpWebRequest request = (FtpWebRequest)WebRequest.Create(textServer.Text); request.Credentials = new NetworkCredential(textUserName.Text, passwordBox.Password); request.Method = WebRequestMethods.Ftp.ListDirectory; // Отправка запроса серверу. response = (FtpWebResponse)request.GetResponse(); // Считывание ответа и заполнение поля списка, stream = response.GetResponseStream(); FillDirectoryList(stream); serverDirectory = null; buttonOpenDirectory.IsEnabled = false; buttonGetFile.IsEnabled = false; } catch (WebException ex) { MessageBox.Show(ex.Message, "Error FTP Client", MessageBoxButton.OK, MessageBoxImage.Error); } catch (IOException ex) { MessageBox.Show(ex.Message, "Error FTP Client", MessageBoxButton.OK, MessageBoxImage.Error); } finally { if (response != null) response.Close(); if (stream != null) stream.Close(); this.Cursor = currentCursor; } }
Глава 32. Передача данных по сети 1029 9. Чтобы открыть конкретный каталог на сервере, добавьте обработчик button OpenDirectory_Click события Click к кнопке buttonOpenDirectory. Добавьте код в этот обработчик, как показано в следующем примере: private void buttonOpenDirectory_Click(object sender, RoutedEventArgs e) { Cursor currentCursor = this.Cursor; FtpWebResponse response = null; Stream stream = null; try { this.Cursor = Cursors.Wait; string subDirectory = listFiles.SelectedValue.ToString().Trim(); if (serverDirectory != null) { serverDirectory += "/" + subDirectory; } else { serverDirectory = subDirectory; } Uri baseUri = new Uri(textServer.Text); Uri uri = new Uri(baseUri, serverDirectory); FtpWebRequest request = (FtpWebRequest)WebRequest.Create(uri) ; request.Credentials = new NetworkCredential(textUserName.Text, passwordBox.Password); request.Method = WebRequestMethods.Ftp.ListDirectory; response = (FtpWebResponse)request.GetResponse(); stream = response.GetResponseStream(); FillDirectoryList(stream); } catch (WebException ex) { MessageBox.Show(ex.Message, "Error FTP Client", MessageBoxButton.OK, MessageBoxImage.Error); } finally { if (response != null) response.Close () ; if (stream != null) stream.Close (); this.Cursor = currentCursor; } } 10. Для загрузки файла с сервера добавьте обработчик buttonGetFileClick события Click к кнопке buttonGetFile. Поместите следующий код в этот обработчик события:
1030 Часть V. Дополнительные технологии private void buttonGetFile_Click(object sender, RoutedEventArgs e) { Cursor currentCursor = this.Cursor; FtpWebResponse response = null; Stream inStream = null; Stream outStream = null; try { this.Cursor = Cursors.Wait; Uri baseUri = new Uri(textServer.Text); string filename = listFiles.SelectedValue.ToString().Trim() ; string fullFilename = serverDirectory + @"/" + filename; Uri uri = new Uri(baseUri, fullFilename); FtpWebRequest request = (FtpWebRequest)WebRequest.Create(uri); request.Credentials = new NetworkCredential(textUserName.Text, passwordBox.Password); request.Method = WebRequestMethods.Ftp.DownloadFile; request.UseBinary = checkBoxBinary.IsChecked ?? false; response = (FtpWebResponse)request.GetResponse(); inStream = response.GetResponseStreamO ; SaveFileDialog saveFileDialog = new SaveFileDialog(); saveFileDialog.FileName = filename; if (saveFileDialog.ShowDialog() == true) { outStream = File.OpenWrite(saveFileDialog.FileName); byte[] buffer = new byte[8192]; int size = 0; while ((size = inStream.Read(buffer, 0, 8192))> 0) { outStream.Write(buffer, 0, size); } catch (WebException ex) { MessageBox.Show(ex.Message, "Error FTP Client", MessageBoxButton.OK, MessageBoxImage.Error); finally { if (inStream != null) inStream.Close(); if (outStream != null) outStream.Close (); if (response != null) response.Close (); this.Cursor = currentCursor;
Глава 32. Передача данных по сети 1031 11. Чтобы активизировать кнопки Open Directory (Открыть каталог) и Get File (Получить файл), необходимо добавить обработчик события SelectionChanged поля списка listFiles. Присвойте этому обработчику события имя listFiles_ SelectionChanged. private void listFiles_SelectionChanged(object sender, RoutedEventArgs e) { buttonGetFile.IsEnabled = true; buttonOpenDirectory.IsEnabled = true; 12. После компиляции приложение можно испытать. В результате запуска приложение отобразит экран, подобный показанному на рис 32.8. Введите имя FTP-cep- вера, к которому нужно подключиться (например, ftp: //ftp.microsoft.com) и щелкните на кнопке Open (Открыть). FTP-серверы, которые поддерживают анонимных пользователей, принимают имя пользователя Anonymous. Некоторые FTP-серверы, допускающие анонимных пользователей, выполняют проверку пароля, представленного в формате допустимого адреса электронной почты. Это поведение реализуется не для обеспечения безопасности, а для записи запросов файлов в журнальный файл. ■ FTP СПИН Server: иилмлм: Password: II Up:// Anonymous b»i«JHHl <^р*я ftreciory <^[»4* ВМегуМове Рис. 32.8. Работа приложения FtpClient После успешной установки соединения файлы и каталоги FTP-сервера отображаются в поле списка, как показано на рис. 32.9. Теперь можно открывать другие каталоги, щелкая на кнопке Open Directory, или загружать файлы, щелкая на кнопке Get File. server Userneroe: Password MSC1 peropeys i^^^^M PSS RttJOt Services ftp: ttprnicrosoft com Anonymous 1 (Z °~ Open Directory Get Fill Вишу Mode j i Ij Puc. 32.9. Файлы и каталоги FTP-сервера
1032 Часть V. Дополнительные технологии Описание полученных результатов При отправке запроса FTP-серверу необходимо создать объект FtpWebRequest. Объект FtpWebRequest создается фабричным методом WebRequest .Create (). Этот метод создает объект FtpWebRequest, HttpWebRequest либо FileWebRequest, в зависимости от строки URL-адреса, переданной методу. FtpWebRequest request = (FtpWebRequest)WebRequest.Create(textServer.Text); После того как объект FtpWebRequest создан, его нужно сконфигурировать, устанавливая его свойства. Свойство Credentials позволяет устанавливать имя пользователя и пароль, служащие для доступа к серверу. Свойство Method определяет FTP- запрос, который должен быть отправлен серверу. Протокол FTP принимает команды, которые очень похожи на команды протокола HTTP. Протокол HTTP использует такие команды, как GET, HEAD и POST. Команды протокола FTP определены в документе RFC 959 (www.ietf.orq/rfc/rfc0959.txt): RETR предназначена для загрузки файлов, ST0R — для выгрузки файлов, MKD — для создания каталога, a LIST — для получения файлов из каталога. При использовании каркаса .NET знание всех этих команд FTP не обязательно, поскольку класс WebRequestMethods содержит их. WebRequestMethods .Ftp.ListDirectory создает LIST-запрос, который должен быть отправлен серверу, a WebRequestMethods . Ftp. DownloadFiles используется для загрузки файла с сервера. request.Credentials = new NetworkCredential(textUsername.Text, passwordBox.Password); request.Method = WebRequestMethods.Ftp.ListDirectory; Когда запрос определен, он может быть отправлен серверу. Метод GetResponse () отправляет запрос серверу и дожидается получения ответа: response = (FtpWebResponse)request.GetResponse(); Чтобы можно было получить доступ к данным ответа, метод GetResponseStream () возвращает поток. Поток, созданный в ответ на запрос WebRequestMethods . Ftp. ListDirectory, содержит все имена файлов из запрошенного каталога сервера. Поток, созданный в ответ на запрос WebRequestMethods.Ftp.DownloadFile, содержит содержимое запрошенного файла: stream = response.GetResponseStream(); Возвращенный поток, содержащий листинг файла каталога сервера, обрабатывается методом FillDirectoryList (). Чтение потока осуществляется с использованием StreamReader. Метод ReadToEndO возвращает строку всех имен файлов, возвращенных сервером. Имена файлов разделяются символом новой строки, который используется для создания массива строк, содержащего все имена файлов. Этот массив строк устанавливается в качестве контекста данных окна, поэтому файлы отображаются элементом управления ListBox: private void FillDirectoryList(Stream stream) { StreamReader reader = new StreamReader(stream); string content = reader.ReadToEnd(); stringf] files = content.Split('\n'); this.DataContext = files; reader.Close (); }
Глава 32. Передача данных по сети 1033 Поток, полученный в результате запроса на загрузку файла, обрабатывается методом OnDownloadFile (). Метод открывает диалоговое окно SaveFileDialog, чтобы пользователь мог указать каталог для сохранения файла. Свойство FileName объекта SaveFileDialog возвращает каталог и имя файла, выбранные пользователем. Метод OpenWrite () класса File возвращает поток, указывающий возможное место хранения файла, полученного с сервера. Поток, возвращенный FTP-сервером, считывается в цикле while и записывается в поток локального файла: Диалоговые окна подробно рассматриваются в главе 17. inStream = response.GetResponseStreamO ; SaveFileDialog SaveFileDialog = new SaveFileDialog (); SaveFileDialog.FileName = fileName; if (SaveFileDialog.ShowDialogO == true) { outStream = File.OpenWrite(SaveFileDialog.FileName); byte[] buffer = new byte[8192]; int size = 0; while ((size = inStream.Read(buffer, 0, 8192)) > 0) { outStream.Write(buffer, 0, size); } } Теперь вы узнали, как классы .NET можно применять совместно с общеизвестными прикладными протоколами. Если хотите создать собственный протокол приложения на основе TCP, для этого можно использовать классы TcpListener и TcpClient. Классы TcpListener и TcpClient Классы пространства имен System.Net. Sockets предлагают гораздо больше возможностей по программированию сетевой передачи. Классы этих пространств имен позволяют создавать собственный сервер и определять пользовательский протокол для передачи информации от клиента серверу и от сервера клиенту. С сервером можно использовать класс TcpListener. С помощью конструктора класса можно определять номер порта, который сервер должен прослушивать. Метод Start () запускает прослушивание. Однако для осуществления обмена данными с клиентом сервер должен вызвать метод AcceptTcpClient (). Этот метод выполняет блокировку до момента подключения клиента. Возвращаемое значение этого метода—объект TcpClient, содержащий информацию о подключении клиента. Используя этот объект, сервер может принимать данные, отправляемые клиентом, и отправлять данные обратно клиенту. Сам клиент также использует класс TcpClient. Клиент может инициировать подключение к серверу с помощью метода Connect (), после чего он может отправлять и принимать данные посредством потока, связанного с объектом TcpClient. В двух последующих практических занятиях мы создадим простые приложения сервера и клиента с помощью классов TcpListener и TcpClient. Сервер отправляет клиенту список файлов изображений, а клиент может выбирать файл из этого списка для запроса конкретного файла с сервера. Это приложение позволяет клиенту выполнять два запроса: запрос LIST возвращает список файлов, а запрос PICTURE :имя_файла возвращает изображение в виде потока байтов.
1034 Часть V. Дополнительные технологии Практическое занятие | Со3дание ТСР-Сврвера" Начнем с создания серверного приложения. 1. С помощью Visual Studio создайте новый проект консольного приложения в каталоге С: \BegVCSharp\Chapter32. Назовите приложение PictureServer. 2. Выберите свойства проекта. Откройте вкладку Settings (Настройки) и создайте файл настроек, используемых по умолчанию, щелкнув на ссылке This Project Does Not Contain A Default Settings File. Click Here To Create One. (Этот проект не содержит файла настроек, используемых по умолчанию. Щелкните на этой ссылке, чтобы его создать). 3. В файле Settings . settings добавьте свойство PictureDirectory типа string и свойство Port типа int, как показано на рис. 32.10. Установите значение PictureDirectory равным имени каталога своей системы, в котором хранятся изображения, например, с: \pictures. Установите значение свойства Port равным 8888, чтобы определить номер порта сервера. l^flrtamfla: (OefaJt) |^^^ ► Name Type PfctureOrectory ! string Port rt В string --■.*И*аг* «e- -:otJ Scope Value ЦЛрркМоп У c:\pktures У****к»Уабее v Appkation v ' 1 Рис. 32.10. Добавление свойств в файл Settings. settings 4. С помощью пункта меню Add1^Class (Добавить^Класс) в окне Solution Explorer добавьте в консольное приложение внутренний вспомогательный класс PictureHelper. Статические методы, предоставляемые этим классом, возвращают список всех файлов (метод GetFileList ()) и список имен файлов в массиве байтов (метод GetFileListBytes ()). Для получения имен файлов и чтения файла этот класс использует файловый ввод-вывод. Более подробно файловый ввод-вывод рассматривается в главе 24. using System; using System. Col lections. Generic- using System.Linq; using System.10; using System.Text; namespace PictureServer { internal static class PictureHelper { internal static IEnumerable <string> GetFileList () { // Удаление пути каталога из имени файла. return from file in Directory.6etFiles( Properties.Settings.Default.PictureDirectory) select Path.GetFileName(file); }
Глава 32. Передача данных по сети 1035 internal static byte [] GetFileListBytes() { try { // Список, возвращаемый запросом LIST. IEnumerable <string> files = PictureHelper.GetFileList() ; StringBuilder responseMessage = new StringBuilder () ; foreach (string s in files) { responseMessage.Append(s) ; responseMessage.Append(":"); } return Encoding.ASCII. GetBytes ( responseMessage.ToString()); ) catch (DirectoryNotFoundException ex) { Console. WriteLine(ex.Message); throw; } ) } } 5. Импортируйте пространства имен System.Net, System.Net.Sockets и System. 10 в файл Program.cs. 6. В метод Main () серверного приложения добавьте следующий код: class Program { static void Main() { TcpListener listener = new TcpListener(IPAddress.Any, Properties.Settings.Default.Port); listener.Start(); Console.WriteLine("Server running..."); while (true) { const int buffer Size = 8192; TcpClient client = listener.AcceptTcpClient() ; NetworkStream clientStream = client. GetStream () ; byte[] buffer = new byte [buffer Size] ; int readBytes = 0; readBytes = clientStream.Read(buffer, 0, bufferSize); string request = Encoding. ASCI I. GetString (buffer) . Substring ( 0, readBytes); if (request.StartsWith("LIST", StringComparison.Ordinal)) { // Список, возвращаемый запросом LIST. byte[] responseBuffer = PictureHelper.GetFileListBytes(); clientStream. Write (responseBuf fer, 0, responseBuf fer. Length) ; >
1036 Часть V. Дополнительные технологии else if (request.StartsWith("FILE" , StringCoxnparison.Ordinal)) { // Файл, возвращенный запросом FILE. // Получение имени файла. string[] requestMessage = request.Split(' : ') ; string filename = requestMessage [1] ; byte[] data = File.ReadAllBytes(Path.Combine( Properties.Settings.Default.PictureDirectory, filename)); // Отправка изображения клиенту. clientStream.Write(data, 0, data.Length); } clientStream.Close(); > } } Описание полученных результатов Чтобы создать серверное приложение, которое дожидается подключения клиентов, использующих протокол TCP, можно применять класс Тер Listener. Для создания объекта TcpListener нужно определить номер порта сервера. В данном случае номер порта считывается из файла конфигурации посредством объекта Properties. Settings. Default. Port. Затем приложение вызывает метод Start () для запуска прослушивания входящих запросов: TcpListener listener = new TcpListener(IPAddress.Any, Properties.Settings.Default.Port); listener.Start (); После запуска слушателя сервер с помощью метода AcceptTcpClient () дожидается подключения клиента. Подключение к клиенту определено в объекте TcpClient, возвращенном методом AcceptTcpClient (): TcpClient client = listener.AcceptTcpClient (); После того как подключение инициировано, клиент отправляет запрос серверу. Запрос читается в поток. Метод client. GetStream () возвращает объект NetworkStream. Данные из этого сетевого потока считываются в буфер массива байтов, а затем преобразуются в строку, которая сохраняется в переменной request: NetworkStream clientStream = client.GetStream(); byte[] buffer = new byte[bufferSize]; int readBytes = 0; readBytes = clientStream.Read(buffer, 0, bufferSize); string request = Encoding.ASCII.GetString(buffer).Substring@, readBytes); В зависимости от строки запроса клиенту возвращается либо список файлов изображений, либо байты изображения. Проверка строки запроса выполняется методом StartsWith: if (request.StartsWith("LIST", StringComparison.Ordinal)) Сервер отправляет данные обратно клиенту, записывая возвращаемые данные в сетевой поток: byte[] responseBuffer = PictureHelper.GetFileListBytes (); clientStream.Write(responseBuffer, 0, responseBuffer.Length);
Глава 32. Передача данных по сети 1037 Создание ТСР-клиента Клиентское приложение — это приложение Windows Forms, которое отображает файлы изображений, доступные на сервере. Когда пользователь выбирает файл изображения, изображение будет показано в клиентском приложении. 1. Создайте новый WPF-проект по имени PictureClient в каталоге С: \BegVCSharp\ Chapter32. 2. Удалите файл Windowl. xaml и добавьте новое окно WPF по имени PictureClient Window.xaml. В файле App.xaml измените ссылку на PictureClientWindow.xaml. 3. Добавьте в настройки свойств приложения номер порта сервера, как это было выполнено в серверном приложении. Свойство имени сервера называется Server, а свойство номера порта — ServerPort. Установите значение Server в соответствии с именем сервера, а номер порта — в соответствии с номером порта сервера. В этом примере используется номер порта сервера равный 8888. 4. В главное окно добавьте элементы управления, показанные на рис. 32.11. GttPicttmUst Get Picture —ii ■—ri Рис. 32.11. Главное окно приложения PictureClient Основные элементы управления этого диалогового окна перечислены в табл. 32.5. Таблица 32.5. Значения свойств элементов управления Тип элемента управления Имя Свойство PictureBox Button ListBox Button pictureBox buttonListPictures listFiles buttonGetPicture Content="List Pictures" ItemsSource="{Binding}" Content="Get Picture" Код файла PictureClientWindow.xaml показан ниже: <Window x:Class="PictureClient.PictureClientWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Picture Client" Height=00" Width=00">
1038 Часть V. Дополнительные технологии <Grid> <Grid.RowDefinitions> <RowDefinition Height=0" /> <RowDefinition /> <RowDefinition Height=0" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Image x:Name=MpictureBox" Grid.Row=" Grid.Column=" Grid.RowSpan=" Margin=,6,6,6" /> <Button Grid.Row=" Grid.Column="l" Margin=,5,5,5" x:Name="buttonGetPictureList"> Get Picture List </Button> <Button Grid.Row=" Grid.Column=Ml" Margin=,5,5,5" x:Name="buttonGetPicture"> Get Picture </Button> <ListBox ItemsSource=" {Binding}" Grid.Row="l,! Grid.Column="l" Margin=,5,5,5" x:Name="listFiles" /> </Grid> </Window> 5. Импортируйте пространства имен System.Net, System.Net. Sockets и System. 10 в файл PictureClientWindow.xaml.cs. 6. Добавьте в класс PicutreClientWindow константу buf ferSize: public partial class PictureClientWindow : Window { private const int bufferSize = 8192; 7. Добавьте вспомогательный метод ConnectToServer () с реализацией, показанной ниже: private TcpClient ConnectToServer () { // Подключение к серверу. TcpClient client = new TcpClient(); IPHostEntry host = Dns.GetHostEntry( Properties.Settings.Default.Server); var address= (from h in host.AddressList where h.AddressFamily == AddressFamily.InterNetwork select h).First (); client.Connect(address.ToString (), Properties.Settings.Default.ServerPort); return client; } 8. Добавьте к кнопке buttonListPictures обработчик события Click, содержащий следующий код: private void buttonGetPictureList_Click(object sender, RoutedEventArgs e) { TcpClient client = ConnectToServer () ; // Отправка запроса серверу. NetworkStream clientStream = client. Getstrearn () ; string request ■ "LIST"; byte[] requestBuffer = Encoding.ASCII.GetBytes(request) ; clientStream.Write(requestBuffer, 0, requestBuffer.Length);
Глава 32. Передача данных по сети 1039 // Считывание ответа с сервера. byte[] responseBuffer = new byte [bufferSize] ; Memory Stream memStream = new MemoryS tream () ; int bytes Re ad = 0; do { bytesRead = clients tream. Read (responseBuffer, 0, buf ferSize) ; memStream. Write (responseBuffer, 0, bytesRead); } while (bytesRead > 0) ; clients tream. Close () ; client.Close(); byte[] buffer = memStream.GetBuffer() ; string response = Encoding.ASCII.GetString(buffer) ; this. Da taCon text = response. Split (':'); } 9. Добавьте к кнопке buttonGetPicture обработчик события Click, содержащий следующий код: private void buttonGetPicture_Click(object sender, RoutedEventArgs e) { TcpClient client = ConnectToServer () ; NetworkStream clientStream = client. Gets tream () ; string request = "FILE:" + this.listFiles.Selectedltern.ToStringO ; byte[] requestBuffer = Encoding. ASCI I. GetBytes (request) ; clientstream.Write(requestBuffer, 0, requestBuffer.Length); byte[] responseBuffer = new byte [bufferSize] ; MemoryS tream memStream = new MemoryS tream () ; int bytesRead = 0 ; do { bytesRead = clientStream. Read (responseBuffer, 0, bufferSize) ; memStream. Write (responseBuffer, 0, bytesRead); } while (bytesRead > 0) ; clientStream.Close(); client.Close(); Bitmap Image bitmaplmage = new Bitmaplmage() ; memS tream.Seek@, SeekOrigin.Begin); bitmaplmage.Beginlnit(); bitmaplmage. StreamSource = memStream; bitmapImage.Endlnit(); pictureBox. Source = bitmaplmage; } 10. Теперь можно запустить серверное приложение, а затем — клиентское приложение (рис. 32.12). Щелчок на кнопке List Pictures (Вывести список изображений) ведет к отображению списка всех изображений в каталоге изображений (сконфигурированном на сервере). Выбор изображения и щелчок на кнопке Get Picture (Получить изображение) вызывает передачу изображения клиенту и его отображению в окне изображения. При включенном брандмауэре Windows Vista система попросит разблокировать программу. Чтобы сервер прослушивал конкретный порт, это действие нужно разрешить. Настройки брандмауэра позволяют также конфигурировать исключения для конкретных номеров портов, используемых во время разработки.
1040 Часть V. Дополнительные технологии ■ PtetureCM»nt GttPIOwlM PICTOIM.JPG PtCT0185.JPG PtCT01S6.JPG PICT0187.JPG PtCT01tt.JPG P«CT01WJPG PICT0190JPG PJCT0191.JPG PtCT0192.JPG PICT0193.JPG PICT0194.JPG PtCT0195.JPG PICTOIMJPG HCTOW.JPG PtCTOIMJPG PtCT01»JPG P«CT0200.JPG PICT0201.JPG PtCT0202.JPG PKrT0203.JPG PKTT0204JPG PKIT0205.JPG Рис. 32.12. Работа приложения PictureClient Описание полученных результатов Класс TcpClient используется клиентом для подключения к серверу. После создания объекта TcpClient метод Connect () инициирует подключение к серверу. Метод Connect () требует указания IP-адреса и номера порта. IP-адрес считывается из IPHostEntry переменной host. Объект IPHostEntry, содержащий все IP-адреса сервера, возвращается методом GetHostEntry () класса Dns. Имя сервера считывается из файла конфигурации приложения посредством объекта Properties. Settings. Default.Server: TcpClient client = new TcpClient (); IPHostEntry host = Dns.GetHostEntry( Properties.Settings.Default.Server); client.Connect(host.AddressList[0], Properties.Settings.Default.ServerPort) ; Теперь посредством объекта NetworkStream данные могут быть отправлены серверу. Объект NetworkStream доступен посредством метода GetStreamO объекта TcpClient. Теперь предназначенные для записи данные могут быть отправлены серверу методом объекта NetworkStream. Поскольку метод Write () требует использования массива байтов, строка "LIST" преобразуется классом Encoding в массив байтов. Encoding.ASCII возвращает ASCII-объект Encoding. С помощью этого объекта метод GetBytes () преобразует строку в массив байтов: NetworkStream clientStream = client.GetStream(); string request = "LIST"; byte[] requestBuffer = Encoding.ASCII.GetBytes(request); clientStream.Write(requestBuffer, 0, requestBuffer.Length); Данные, возвращенные с сервера, считываться методом Read () объекта clientStream. Поскольку объем данных, которые должны быть получены с сервера, не известен, для считывания всех поступающих данных используется цикл while. Считанные данные дописываются в конец объекта MemoryStream, размеры которого автоматически изменяются: byte[] responseBuffer = MemoryStream memStream int bytesRead = 0; new byte[bufferSize]; = new MemoryStream();
Глава 32. Передача данных по сети 1041 do { bytesRead = clientStream.Read(responseBuffer, 0, bufferSize); memStream.Write(responseBuffer, 0, bytesRead); } while (bytesRead > 0); Изображение считывается так же, как и данные файла. Поток памяти, содержащий данные изображения, преобразуется в изображение классом Bitmap Image WPE Затем изображение присваивается свойству Source объекта PictureBox, чтобы оно отображалось в форме: Bitmaplmage bitmaplmage = new Bitmaplmage(); memStream.Seek@, SeekOrigin.Begin); bitmaplmage.Beginlnit() ; bitmaplmage.StreamSource = memStream; bitmaplmage.Endlnit(); pictureBox.Source = bitmaplmage; Резюме В настоящей главе мы рассмотрели использование классов пространства имен System.Net и System.Net. Sockets для создания сетевых приложений. Класс WebClient можно использовать для доступа к Web-серверам. Простой созданный нами FTP-клиент иллюстрирует применение классов FtpWebRequest и FtpWebResponse, которые предоставляют больше возможностей, чем класс WebClient. Было также показано, что кроме класса FtpWebRequest можно использовать классы HttpWebRequest и FileWebRequest. Поскольку создание сервера с помощью классов FtpWebRequest и FtpWebResponse невозможно, для этого требуются классы из пространства имен System.Net. Sockets. Вы научились создавать TCP-сервер с помощью класса TcpListener и клиентское приложение с помощью класса TcpClient. В этой главе рассмотрены следующие вопросы. □ Использование класса Dns для поиска имен. □ Какие данные клиент отправляет в HTTP-запросе. □ Как выполнять HTTP-запросы с помощью класса WebClient. □ Как работать с классом WebRequest и, в частности, как получать файлы с FTP- сервера с помощью класса FtpWebRequest. □ Как применять пользовательский протокол совместно с классами пространства имен System.Net .Sockets. Упражнения 1. Расширьте возможности приложения FTP-клиента, использующего классы FtpWebRequest и FtpWebResponse, чтобы оно не только загружало файлы с FTP- сервера, но и позволяло выгружать файлы на сервер. Добавьте на форму еще одну кнопку, в качестве свойства Text которой установлен текст Upload File (Выгрузить файл), используйте диалоговое окно OpenFileDialog, чтобы запросить пользователя о выгружаемом файле, а затем отправьте запрос серверу. Для выгрузки файлов класс WebRequestMethods предоставляет член Ftp.UploadFile. 2. Измените приложения PictureServer и PictureClient, использующие классы TcpListener и TcpClient, чтобы можно было выгружать файлы изображений с клиента на сервер.
Введение в GDI+ Ранее термин GDI+ был кратко представлен при рассмотрении печати в среде .NET Framework. Эта глава служит введением в программирование с помощью классов GDI+ (Graphics Device Interface — интерфейс графических устройств) — т.е. технологии рисования .NET Framework. Картографические приложения, игры, системы автоматизированного проектирования/автоматизированного производства (CAD/CAM), программы рисования, программы составления диаграмм и многие другие типы приложений требуют от разработчиков вставки кода обработки графики в приложения Windows Forms. Создание нестандартных элементов управления также требугт использования кода обработки графики. Эта новейшая библиотека классов Microsoft сделала создание кода обработки графики проще, чем когда-либо ранее. Написание кода обработки графики — одна из наиболее увлекательных задач программирования. Изменение кода и немедленное наблюдение результатов этого изменения в видимой форме — чрезвычайно благодарное занятие. Создаете ли вы нестандартное графическое окно, представляющее какую-то информацию приложения новым способом, или создаете нестандартный элемент управления, который делает приложение более стильным и расширяет возможности его применения — в любом случае приложение будет благожелательно встречено широкой публикой. В начале в этой главе рассмотрены методики рисования с помощью GDI+, проиллюстрированные на примере создания нескольких простых графических программ. Затем мы в общих чертах рассмотрим некоторые более сложные возможности GDI+ вроде отсечения. После краткого ознакомления с каждой из этих тем мы рассмотрим классы, которые можно использовать для реализации функциональных возможностей. Знание того, что может быть выполнено, и понимание иерархии используемых при этом классов — уже половина успеха. В этой главе будут рассматриваться следующие темы. □ Поверхности рисования, инкапсулированные в классе Graphics. □ Цвета, определенные структурой Color. □ Рисование линий и форм. □ Рисование текста и изображений. □ Рисование в изображениях (двойная буферизация).
Глава 33. Введение в GDI+ 1043 Обзор графического рисования Первое, что следует запомнить относительно кода обработки графики — то, что система Windows не помнит, как выглядит открытое окно, если оно заслонено другими окнами. Когда заслоненное окно становится видимым, Windows по существу говорит приложению: "Ваше окно (или какая-то его часть) становится видимым. Пожалуйста, нарисуйте его". Программист должен позаботиться только о прорисовке содержимого своего окна. Windows сама заботится о границе окна, строке заголовка и всех остальных особенностях окна. При создании окна для рисования, как правило, объявляют класс, производный от System. Windows. Forms. Form. При создании нестандартного элемента управления объявляют класс, производный от System. Windows. Forms. UserControl. В любом случае это ведет к переопределению виртуальной функции OnPaint (). Система Windows вызывает эту функцию каждый раз, когда требуется выполнить повторную прорисовку любой части окна. При наступлении этого события класс PaintEventArgs передается в качестве аргумента. PaintEventArgs содержит два важных элемента информации: объект Graphics и ClipRectangle. Класс Graphics мы рассмотрим первым, а отсечение будет рассмотрено в конце главы. Класс Graphics Класс Graphics инкапсулирует поверхность рисования GDI+. Существуют три основных типа поверхностей рисования: □ окна и элементы управления на экране; □ страницы, отправляемые на принтер; □ битовые карты и изображения в памяти. Класс Graphics предоставляет функции для рисования на любой из этих поверхностей рисования. В числе прочих возможностей его можно использовать для рисования дуг, кривых, кривых Безье, эллипсов, изображений, линий, прямоугольников и текста. Объект Graphics для окна можно получить двумя различными способами. Первый — переопределение метода OnPaint (). Класс Form наследует метод OnPaint () из класса Control, и этот метод является обработчиком события Paint, которое генерируется при каждом перерисовывании элемента управления. Объект Graphics можно получить из класса PaintEventArgs, который передается событием: protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; // Выполнить здесь рисование. } В других случаях может требоваться выполнение рисования непосредственно в окне, не дожидаясь генерации события Paint. Такая ситуация может возникать при создании кода для выбора графического объекта в окне (подобно выбору пиктограмм в окне проводника Windows) или перетаскивании объекта мышью. Объект Graphics можно получить, вызывая в форме метод CreateGraphics (), который является еще одним методом, унаследованным Form от класса Control:
1044 Часть V. Дополнительные технологии protected void Forml_Click (object sender, System.EventArgs e) { Graphics g = this.CreateGraphics (); // Выполнить здесь рисование. g. Dispose (); // это важно } Построение приложения, которое обрабатывает перетаскивание — достаточно сложная задача, выходящая за рамки настоящей главы. В любом случае, эта методика рисования менее распространена. Почти все рисование будет выполняться в ответ на действия метода OnPaint (). Удаление объектов Все мы знакомы с поведением Windows при недостаче ресурсов. Система начинает работать очень медленно и иногда приложения прорисовываются некорректно. Правильно ведущие себя приложения освобождают свои ресурсы по завершении работы с ними. При разработке приложений, использующих .NET Framework, приходится иметь дело с несколькими типами данных, для которых важно вызывать метод Dispose () — в противном случае некоторые ресурсы не будут освобождены. Эти классы реализуют интерфейс IDisposable, и Graphics — один из них. При получении объекта Graphics от метода OnPaint (), мы не создавали его, поэтому вызов метода Dispose () — не наша забота. Но при вызове метода CreateGraphics () вызов метода Dispose () становится нашей обязанностью. Вызов метода Dispose () важен при получении объекта Graphics посредством вызова метода CreateGraphics(). Метод Dispose () автоматически вызывается в деструкторе различных классов, которые реализуют интерфейс IDisposable. Может показаться, что это освобождает от обязанности вызывать метод Dispose (), однако это не так. Это связано с тем, что только сборщик мусора (garbage collector — GC) регулярно вызывает деструктор, а мы не можем гарантировать, когда именно GC будет запущен. В частности, в операционной системе Windows 9X, располагающей очень большим объемом памяти, GC может запускаться очень редко, и все ресурсы вполне могут быть исчерпаны, прежде чем он будет запущен. Хотя нехватка памяти ведет к запуску GC, нехватка ресурсов не оказывает такого действия. Windows 2000 и Windows XP значительно менее чувствительны к нехватке ресурсов и, согласно их спецификациям, эти две операционные системы не имеют никаких определенных ограничений на использование указанных типов ресурсов. Однако нередко приходится сталкиваться с непредвиденным поведением Windows 2000 при наличии слишком большого количества открытых приложений, когда закрытие некоторых из них быстро восстанавливает правильную работу системы. В любом случае лучше вручную корректно и вовремя удалять любые объекты, потребляющие много ресурсов. В то же время существует более простой способ работы с объектами, которые нужно удалять надлежащим образом. Можно использовать конструкцию using, которая автоматически вызывает метод Dispose (), когда объект выходит из области видимости. Следующий код демонстрирует правильное применение ключевого слова using в этом контексте: using (Graphics g = this.CreateGraphics() ) { g.DrawLine(Pens.Black, new Point@, 0), new PointC, 5) ) ; }
Глава 33. Введение в GDI+ 1045 В соответствии с документацией приведенный код полностью эквивалентен следующему: Graphics g = this.CreateGraphics(); try { g.DrawLine(Pens.Black, newPoint@, 0), newPointC, 5)); } finally { if (g != null) ((IDisposable)g).Dispose (); } He путайте это применение ключевого слова using с директивой using, которая создает псевдоним пространства имен или разрешает использование типов в пространстве имен, чтобы не нужно было полностью квалифицировать использование типа. Приведенный пример демонстрирует совершенно особое применение ключевого слова using — если хотите, блок кода, обозначенный ключевым словом using, можно считать блоком using. В примерах этой главы обработка обращений к методу Dispose () выполняется с использованием обоих стилей. В одних случаях вызов метода Dispose () выполняется непосредственно, в других — через блок using. Последний метод значительно более понятен, как видно из приведенных фрагментов кода, но ни один из них не дает особых преимуществ. Прежде чем приступить к исследованию первого примера, необходимо рассмотреть еще два аспекта рисования графических изображений — систему координат и цвета. Система координат При разработке программы, которая прорисовывает сложные, замысловатые изображения, очень важно, чтобы код рисовал то, и только то, что требуется. Единственный неправильно помещенный пиксель может оказать отрицательное влияние на визуальную привлекательность графического изображения, поэтому важно четко представлять, какие пиксели рисуются при вызове операций рисования. Это наиболее важно при создании нестандартных элементов управления, которые содержат множество прямоугольников и горизонтальных и вертикальных линий. Линия, которая короче или длиннее, чем требуется, всего на один пиксель, очень бросается в глаза. Однако это несколько менее заметно в случае кривых, диагональных линий и других графических элементов. GDI+ поддерживает систему координат, построенную на основе линий воображаемой сетки, которые проходят через центры пикселей. Эти линии нумеруются, начиная с нуля: пересечение линий сетки в верхнем левом пикселе любого координатного пространства — точка X = О, Y = 0. Более кратко точку можно обозначать как точку 1,2, что эквивалентно обозначению X = 1, Y = 2. Каждое окно, в котором выполняется рисование, имеет собственное пространство координат. При создании нестандартного элемента управления, который может использоваться в других окнах, сам этот элемент обладает собственным пространством координат. Другими словами, при рисовании в этом нестандартном элементе управления верхним левым пикселем является точка 0,0. При этом не нужно беспокоиться о том, где нестандартный элемент управления размещается в содержащем его окне. При рисовании линий GDI+ центрирует нарисованные пиксели на указанной линии сетки. При рисовании горизонтальной линии, имеющей целочисленные коорди-
1046 Часть V. Дополнительные технологии наты, можно считать, что половина каждого пикселя размещается над линией воображаемой сетки, а половина — под ней. Рисование горизонтальной линии толщиной в один пиксель от точки 1,1 до точки 4,1 приведет к окрашиванию пикселей, показанных на рис. 33.1. 0 - 1 - 2 - 3 - 4 - 5 - ( ) I 2 3 4 5 6 7 Рис. 33.1. Рисование горизонтальной линии толщиной 6 один пиксель от точки 1,1 до точки 4,1 Рисование вертикальной линии толщиной в один пиксель от точки 1,1 до точки 1,4 приведет к окрашиванию пикселей, показанных на рис. 33.2. 0 - 1 - 2 - 3 - 4 - 5 - ( ) I > \ 4 \ с ( Г Рис. 33.2. Рисование вертикальной линии толщиной в один пиксель от точки 1,1 до точки 1,4 При рисовании диагональной линии от точки 1,0 до точки 4,3 пиксели будут окрашены так, как показано на рис. 33.3.
Глава 33. Введение в GDI+ 1047 0 - 1 - 2 - 3 1 4 - 5 J ( ) 1 ■ ■ ■ 1 2 1 1 3 1 1 4 ■ 5 6 7 Рис. 33.3. Рисование диагональной линии от точки 1,0 до точки 4,3 Прямоугольник, верхний левый угол которого расположен в точке 1,0, а размеры сторон составляют 5 и 4, будет прорисован, как показано на рис. 33.4. и * 1 " z ■ з 4 5 - ( 1 12 3 4 5 6 н ш ш ш ш ш 1 ■ 1 ■ i н ■ 1 ■ 1 н ■ 1 ■ 1 н ■ 1 ■ 1 1 г Это расстояние равно 5 пикселям Рис. 33.4. Рисование прямоугольника Обратите внимание на интересный нюанс. Указание ширины, равной 5 пикселям, привело к прорисовке 6 пикселей в горизонтальном направлении. Однако если подсчитать количество линий сетки, проходящих через пиксели, ширина прямоугольника составляет только 5 пикселей, причем нарисованная линия выступает на пол пикселя наружу и на полпикселя внутрь указанной линии сетки. Но и это еще не все. При рисовании с использованием функции сглаживания другие пиксели будут окрашены частично, создавая впечатление плавной линии и частично устраняя ступенчатость диагональных линий.
1048 Часть V. Дополнительные технологии Линия, нарисованная без сглаживания, изображена на рис. 33.5. Рис. 33.5. Линия, нарисованная без сглаживания Эта же линия, нарисованная со сглаживанием, показана на рис. 33.6. Рис. 33.6. Линия, нарисованная со сглаживанием При просмотре с высоким разрешением эта линия будет выглядеть значительно более плавной, лишенной ступенчатого эффекта. Понимание взаимосвязи между координатами, переданными функциям рисования, и результирующим эффектом на поверхности рисования позволяет легко представить себе, какие именно пиксели будут затронуты данным обращением к функции рисования. Для указания координат при рисовании часто придется использовать три структуры: Point (точка), Size (размер) и Rectangle (прямоугольник). Point Интерфейс GDI+ использует Point для представления точки, имеющей целочисленные координаты. Она представляет собой точку на двумерной плоскости — т.е. спецификацию одиночного пикселя. Многие функции GDI+, такие как DrawLine (), принимают Point в качестве аргумента. Объявление и конструирование структуры Point выполняется следующим образом: Point р = new Point (l, 1); Для получения и установки координат X и Y структуры Point используют общедоступные свойства X и Y. Size GDI+ использует структуру Size для представления размера в пикселях. Структура Size содержит как ширину, так и высоту. Объявление и конструирование структуры Size выполняется следующим образом: Size s = new Size E, 5); Для получения и установки высоты и ширины структуры Size используют общедоступные свойства Height и Width.
Глава 33. Введение в GDI+ 1049 Rectangle Интерфейс GDI+ использует эту структуру во множестве различных мест для указания координат прямоугольника. Структура Point определяет верхний левый угол прямоугольника, а структура Size — его размеры. Существуют два конструктора структуры Rectangle. Один принимает в качестве аргументов позицию X, позицию Y, ширину и высоту. Второй принимает структуры Point и Size. Ниже приведены два примера объявления и конструирования структуры Rectangle: Rectangle rl = new Rectangle A, 2, 5, 6); Point p = new Point A, 2); Size s = new Size E, 6); Rectangle r2 = new Rectangle (p, s) ; Для получения и установки всех аспектов расположения и размеров структуры Rectangle предусмотрены соответствующие общедоступные свойства. Кроме того, существуют также другие полезные свойства и методы для решения таких задач, как определение того, пересекается ли прямоугольник с другим прямоугольником, получение пересечения и объединения двух прямоугольников. GraphicsPath В интерфейсе GDI+ доступны два более важных типа данных, которые используются в качестве аргументов различных функций рисования. Класс GraphicsPath представляет серию связанных линий и кривых. При конструировании пути к нему можно добавлять линии, кривые Безье, дуги, сектора, многоугольники, прямоугольники и другие элементы. По завершении конструирования сложного пути его можно рисовать посредством одной операции: вызова метода DrawPath (). Путь можно заполнить (выполнить заливку), вызывая метод FillPath (). Конструирование объекта GraphicsPath осуществляется с использованием массива точек и объекта PathTypes. Объект PathTypes — это массив типа byte, в котором каждый элемент соответствует элементу массива точек и представляет дополнительную информацию о способе конструирования пути, проходящего через каждую конкретную точку. Информация о пути через точку может быть получена с помощью перечисления PathPointType. Например, если точка — начало пути, тип пути для этой точки — PathPointType .Start. Если точка — соединение между двумя линиями, тип пути для этой точки — PathPointType. Line. Если точка применяется для конструирования кривой Безье из точек, расположенных до и после данной, типом пути является PathPointType.Bezier. В следующем практическом занятии мы создадим объект GraphicsPath и нарисуем его в окне. Практическое занятие СоЗДЭНИе Графического ПуТИ Чтобы создать и нарисовать объект GraphicsPath, выполните следующие действия. 1. Создайте новое Windows-приложение по имени DrawingPaths в каталоге С:\ BegVCSharp\Chapter33. 2. В начальную часть кода добавьте следующую директиву using для System. Drawing.Drawing2D:
1050 Часть V. Дополнительные технологии using System; using System.Collections .Generic- using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Drawing.Drawing2D; 3. В тело кода объекта Forml введите следующий код: protected override void OnPaint (PaintEventArgs e) { GraphicsPath path; path = new GraphicsPath(new Point[]{ new Point A0, 10), new PointA50, 10) , new Point B00, 150), new Point A0, 150), new Point B00, 160) }, new byte[] { (byte)PathPointType.Start, (byte)PathPointType.Line, (byte)PathPointType.Line, (byte)PathPointType.Line, (byte)PathPointType.Line }); e.Graphics.DrawPath(Pens.Black, path); } 4. Запустите приложение. Результирующий путь должен выглядеть, как показано на рис. 33.7. Рис. 33.7. Результирующий путь Описание полученных результатов Код создания этого пути достаточно сложен. Конструктор объекта GraphicsPath принимает два аргумента. Первый аргумент — массив Point. В данном случае мы используем синтаксис С# для объявления и инициализации массива в одном и том же месте и создания нового объекта Point по мере перемещения: new Point [ ] { new Point A0, new PointA50, new Point B00, new Point A0, new Point B00, 10), 10), 150), 150), 160)
Глава 33. Введение в GDI+ 1051 Второй аргумент — массив байтов, который также создается в нужном месте: new byte[]{ (byte)PathPointType.Start, (byte)PathPointType.Line, (byte)PathPointType.Line, (byte)PathPointType.Line, (byte)PathPointType.Line } И, наконец, мы вызываем метод DrawPath (): е.Graphics.DrawPath(Pens.Black, path); Области Класс Region (область) — сложная графическая форма, состоящая из прямоугольников и путей. После создания объекта Region эту область можно нарисовать, используя метод FillRegion (). В следующем практическом занятии мы создадим объект Region и нарисуем его в окне. Практическое занятие Создание области Следующий код создает область и добавляет в нее объекты Rectangle и GraphicsPath, а затем закрашивает эту область синим цветом. 1. Создайте новое Windows-приложение по имени DrawingRegions в каталоге С:\BegVCSharp\Chapter33. 2. В начальную часть кода добавьте следующую директиву using для System. Drawing.Drawing2D: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System. Drawing. Drawing2D ; 3. В тело Forml поместите следующий код: protected override void OnPaint (PaintEventArgs e) { Rectangle rl = new Rectangle A0, 10, 50, 50); Rectangle r2 = new Rectangle D0, 40, 50, 50); Region r = new Region(rl); r.Union(r2); GraphicsPath path = new GraphicsPath(new Point[] { new Point D5, 45) , new Point A45, 55), new Point B00, 150), new Point G5, 150), new Point D5, 45) }, new byte [] { (byte)PathPointType.Start, (byte)PathPointType.Bezier,
1052 Часть V. Дополнительные технологии (byte)PathPointType.Bezier, (byte)PathPointType.Bezier, (byte)PathPointType.Line }); r.Union(path); e.Graphics.FillRegion(Brushes.Blue, r) ; } 4. Если теперь запустить этот код, результат должен быть подобен показанному на рис. 33.8. Рис. 33.8. Рисование области Описание полученных результатов Код создания области также достаточно сложен, однако наиболее сложная его часть — создание любых путей, которые будут помещены внутрь области, а эта задача уже была рассмотрена в предыдущем примере. Конструирование областей состоит из конструирования прямоугольников и путей до того, как будет вызван метод Union (). Если бы требовалось использовать пересечение прямоугольника и пути, вместо метода Union () можно было бы применить метод Intersection(). Для общего ознакомления с GDI+ дополнительная информация о путях и областях не требуется, поэтому мы не будем их рассматривать более подробно. Цвета Многие операции рисования в среде GDI+ требуют указания цвета. При рисовании линии или прямоугольника необходимо задать нужный цвет. В интерфейсе GDI+ цвета инкапсулированы в структуре Color. Цвет можно создать, передавая функции структуры Color значения красного, зеленого и синего компонентов, но это почти никогда не требуется. Структура Color содержит около 150 свойств, которые позволяют получать широкое множество заранее определенных цветов. Забудьте о красном, зеленом, синем, желтом и черном цветах — если нужно выполнить рисование цветом LightGoldenrodYellow или LavenderBlush, для этого существует заранее определенный цвет. Объявление переменной типа Color и ее инициализация цветом из структуры Color выполняется следующим образом: Color redColor = Color.Red; Color anotherColor = Color.LightGoldenrodYellow; Мы почти готовы к созданию какого-либо рисунка, но вначале следует привести несколько замечаний. Еще один способ представления цвета — разбиение его на три компонента: оттенок, насыщенность и яркость. Структура Color содержит вспомогательные методы для выполнения этой задачи, а именно — GetHue (), GetSaturation () и GetBrightness ().
Глава 33. Введение в GDI+ 1053 На этом этапе можно создать диалоговое окно выбора цвета, которое можно использовать для выяснения взаимосвязи между цветами, определенными посредством красной, зеленой и синей составляющей, и цветами, определенными посредством оттенка, насыщенности и яркости. Для этого выполните перечисленные ниже действия. 1. В новом Windows-приложении перетащите элемент управления ColorDialog в свою форму и после вызова метода InitializeComponent () добавьте вызов метода this.colorDialogl.ShowDialog(): public Forml() { InitializeComponent (); this.colorDialogl.ShowDialog(); При помещении формы в приложение диалоговое окно выбора цвета также будет помещено в него. 2. Запустите приложение и щелкните на кнопке Define Custom Colors (Определить пользовательские цвета). Откроется диалоговое окно, в котором с помощью мыши можно выбирать цвета и видеть RGB-значения выбранного цвета. Диалоговое окно отображает также значения оттенка, насыщенности и светлоты цвета (светлота соответствует яркости). Можно также непосредственно ввести RGB-значения и наблюдать результирующий цвет. В GDI+ цвета имеют четвертый компонент — альфа-канал. Используя этот компонент, можно устанавливать прозрачность цвета, что позволяет создавать эффекты усиления/ ослабления, подобные используемым в меню Windows 2000 и Windows ХР/Vista. Описание применения альфа-компонента выходит за рамки настоящей книги. Рисование линий с использованием класса Реп Первый пример, который мы рассмотрим — рисование линий. В следующем практическом занятии мы нарисуем линии с использованием класса Реп (перо), который позволяет определять цвет, толщину и стиль линии, отображаемой кодом. Свойства цвета и толщины очевидны, а стиль линии показывает, является ли линия сплошной или состоит из штрихов и точек. Класс Реп определен в пространстве имен System. Drawing. практическое занятие Использование класса Реп Чтобы нарисовать в окне ряд линий с применением класса Реп, выполните следующие действия. 1. Создайте новое Windows-приложение по имени DrawingLines в каталоге С:\ BegVCSharp\Chapter33. 2. В тело кода объекта Forml введите следующий код: protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; using (Pen blackPen = new Pen (Color .Black, 1)) {
1054 Часть V. Дополнительные технологии if (ClientRectangle.Height/10 > 0) { for (int у = 0; у < ClientRectangle.Height; у += ClientRectangle.Height / 10) { g.DrawLme (blackPen, new Point @, 0), new Point(ClientRectangle.Width, y) ) ; } } } } 3. Нажмите клавишу <F5>, чтобы скомпилировать и запустить код. В результате будет создано окно, показанное на рис. 33.9. Рис. 33.9. Рисование линий с использованием класса Реп Описание полученных результатов Класс Graphics был представлен ранее в этой главе. Первое, что мы выполняем в методе OnPaint () — получение объекта Graphics из параметра PaintEventArgs: Graphics g = е.Graphics; Поскольку мы передаем ссылку на объект Graphics и не создаем его, нам не нужно (и не следует) вручную вызывать для него метод Dispose (). Однако поскольку в этом примере применяется объект Реп, который теоретически может потреблять много ресурсов, остальная часть кода помещена в блок using, как было описано ранее, что обеспечит уничтожение объекта, как только это станет возможным. При создании пера конструктору передается цвет и ширина пера. В этом примере цвет является черным, а ширина равной единице. Строка кода, создающего перо, выглядит следующим образом: using (Pen blackPen = new Pen (Color .Black, 1)) Каждое окно, в котором можно выполнять рисование, имеет клиентскую область, представляющую собой прямоугольник, который существует внутри границы и точно определяет область, где можно выполнять рисование. Клиентскую область можно получить из общедоступного, предназначенного только для чтения свойства ClientRectangle формы (наследуемой от класса Control). Оно содержит размеры (т.е. ширину и высоту) клиентской области окна, в котором выполняется рисование.
Глава 33. Введение в GDI+ 1055 Следующий код создает цикл от нуля до высоты клиентской области (заданной свойством ClientRectangle. Height) с шагом 10. Обратите внимание, что цикл начинается с проверки того, что значение ClientRectangle .Height/10 больше нуля — в противном случае цикл выполнялся бы бесконечно, если бы размер формы был меньше определенной высоты. (Поскольку значение ClientRectangle.Height/10 представляет собой приращение цикла, если оно равно нулю, цикл будет выполняться бесконечно.) if (ClientRectangle.Height/10 > 0) { for (int у = 0; у < ClientRectangle.Height; у += ClientRectangle.Height / 10) Теперь можно нарисовать линии — при рисовании каждой линии мы передаем только что созданный объект Реп вместе с начальной и конечной точками линии: g.DrawLine(blackPen, newPoint@, 0), new Point(ClientRectangle.Width, y) ); Для объектов Pen всегда вызывайте метод Dispose (). Как и при использовании объектов Graphics, для объектов Реп важно либо вызывать метод Dispose () по завершении работы с ними, либо использовать блок using. В противном случае приложение может истощать ресурсы Windows. В приведенном примере мы создали объект Реп, но существует и более простой способ его получения. Класс Pens содержит свойства для получения около 150 перьев — по одному для каждого из упомянутых предварительно определенных цветов. Следующая версия примера работает идентично предыдущей, но вместо создания объекта Реп, мы получаем его из класса Pens: protected override void OnPaint(PaintEventArgs e) { if (ClientRectangle.Height/10 > 0) { for (int у = 0; у < ClientRectangle.Height; у += ClientRectangle.Height / 10) { e.Graphics.DrawLine(Pens.Black , new Point@, 0) , new Point (ClientRectangle. Width, y)) ; } } } В данном случае мы не создавали объекта Реп, поэтому нет нужды вызывать метод Dispose (). Класс Реп предоставляет много других функциональных возможностей. Можно было бы создать перо для рисования штриховой линии или же перо, толщина которого больше одного пикселя. Свойство Alignment класса Реп позволяет определять, должно ли перо отображаться слева или справа (либо выше или ниже) указанной линии. Свойства StartCap и EndCap позволяют задавать стрелку, ромб, квадрат или закругление в качестве окончания линии. Свойства CustomStartCap и CustomEndCap позволяют даже создавать нестандартные формы начала и окончания линия. После ознакомления с изображениями вы научитесь указывать объект Brush с помощью объекта Реп, чтобы можно было рисовать линии, используя растровое изображение, а не чистый цвет.
1056 Часть V. Дополнительные технологии Рисование форм с использованием класса Brush Следующий пример демонстрирует использование класса Brush (кисть) для рисования таких форм, как прямоугольники, эллипсы, сектора и многоугольники. Класс Brush — абстрактный базовый класс. Для создания экземпляров объекта Brush применяются такие производные от Brush классы, как SolidBrush, TextureBrush и LinearGradientBrush. Классы Brush и SolidBrush определены в пространстве имен System. Drawing. Однако классы TextureBrush и LinearGradientBrush принадлежат пространству имен System. Drawing. Drawing2D. Классы кистей предоставляют перечисленные ниже возможности. □ SolidBrush выполняет заливку формы чистым цветом. □ TextureBrush выполняет заливку формы растровым изображением. При создании этой кисти указывают также ограничивающий прямоугольник и режим укладки. Ограничивающий прямоугольник указывает, какая часть растрового изображения применяется для кисти — вовсе не обязательно использовать все растровое изображение, если это не требуется. Режим укладки обладает рядом опций, включая Tile (Плитка), которая вызывает укладку текстуры в виде плитки, и TileFlipX, TileFlipY и TileFlipXY, которые вызывают укладку изображения в виде плитки с переворотом при укладке последующих плиток. TextureBrush позволяет создавать очень интересные и впечатляющие эффекты. □ LinearGradientBrush инкапсулирует кисть, которая рисует градиентный переход между двумя цветами по указанным углом. Угол задается в градусах. Нулевой угол означает, что переход между цветами будет выполняться слева направо. Угол в 90 градусов означает, что переход между цветами будет выполняться сверху вниз. Еще одна заслуживающая упоминания кисть — PathGradientBrush, создающая сложный эффект затенения, при котором затенение прорисовывается от центра пути к его краю. Эта кисть напоминает раскрашивание контурных карт цветными карандашами, с выделением границ между областями или странами более темными оттенками. Для объектов Brush всегда вызывайте метод Dispose (), Как и при использовании объектов Graphics и Реп, для объектов Brush важно либо вызывать метод Dispose () по завершении работы с ними, либо использовать блок using. В противном случае приложение может истощать ресурсы Windows. В следующем практическом занятии мы с помощью кистей выполним заливку ряда форм. шшшшяявшшшшш Практическое занятие Использование КИСТИ Чтобы увидеть кисти в действии, выполните следующие действия. 1. Создайте новое Windows-приложение по имени UsingBrushes в каталоге С:\ BegVCSharp\Chapter33. 2. В начальную часть кода добавьте следующую директиву using для класса LinearGradientBrush из пространства имен System. Drawing. Drawing2D:
Глава 33. Введение в GDI+ 1057 using System; using System.Collections .Generic- using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System. Drawing. Drawing2D ; 3. В конструктор класса Forml после вызова метода InitializeComponent () добавьте вызов метода SetStyle(): public Forml () { InitializeComponent (); SetStyle(ControlStyles.Opaque, true); } 4. Добавьте в свой класс метод OnPaint (): protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.FillRectangle(Brushes.White, ClientRectangle); g.FillRectangle(Brushes.Red, new Rectangle A0, 10, 50, 50)); Brush linearGradientBrush = new LinearGradientBrush ( new RectangleA0, 60, 50, 50), Color.Blue, Color.White, 45); g.FillRectangle(linearGradientBrush, new Rectangle A0, 60, 50, 50)); // Вызов метода Dispose() вручную. linearGradientBrush.Dispose (); g.FillEllipse(Brushes.Aquamarine, new Rectangle F0, 20, 50, 30)); .FillPie(Brushes.Chartreuse, new Rectangle F0, 60, 50, 50), 90, 210), .FillPolygon(Brushes.BlueViolet, new Point [ ] { new new new new new }); Point A10, PointA50, Point A60, Point A20, Point A20, 10), 10), 40) , 20) , 60), 5. После компиляции и запуска этой программы ее результат будет подобен показанному на рис. 33.10. Рис. 33.10. Использование кистей
1058 Часть V. Дополнительные технологии Описание полученных результатов Прежде всего, обратите внимание на вызов метода SetStyle () в конструкторе формы. Он является методом класса Form: SetStyle(ControlStyles.Opaque, true); Этот метод изменяет поведение класса Form, чтобы он не выполнял автоматическую прорисовку фона окна. Если вставить эту строку, но не выполнить рисование фона самостоятельно, любые элементы, расположенные под окном во время его создания, будут проглядывать сквозь него, что нежелательно. Поэтому следующим действием будет прорисовка нужного фона в клиентской области окна. Подобно классу Pens класс Brushes содержит свойства для получения приблизительно 150 кистей — по одной для каждого заранее определенного цвета. Именно этот класс применяется для получения большинства кистей рассматриваемого примера, за исключением кисти LinearGradientBrush, которую мы создадим самостоятельно. Первый вызов метода FillRectangle () выполняет прорисовку фона клиентской области окна: g.FillRectangle(Brushes.White, ClientRectangle); Параметры, используемые при создании кисти LinearGradientBrush — прямоугольник, указывающий ее размеры, два цвета, служащие для градиентного перехода, и угол (в данном случае — 45 градусов): Brush linearGradientBrush = new LinearGradientBrush( new RectangleA0, 60, 50, 50), Color.Blue, Color.White, 45); g.FillRectangle(linearGradientBrush, new RectangleA0, 60, 50, 50)); linearGradientBrush.Dispose(); При указании прямоугольника для кисти применялся прямоугольник с шириной и высотой 50, совпадающий по размерам с использованным при определении кисти. В результате область кисти полностью соответствует прямоугольнику. Попробуйте изменить ширину и высоту прямоугольника, определенного при создании кисти, на 10 и посмотрите, к чему это приведет. Поэкспериментируйте также с различными значениями угла, чтобы увидеть изменения в действии. Прорисовка текста с использованием класса Font Следующий пример использует класс Font (шрифт) для прорисовки текста. Класс Font инкапсулирует три основных характеристики шрифта: семейство шрифта, его размер и стиль. Класс Font определен в пространстве имен System.Drawing. В соответствии с документацией .NET, семейство шрифта "обобщает группу гарнитур шрифтов, обладающих аналогичным базовым начертанием". Это определение — несколько усложненное утверждение того, что семейства шрифтов представляют собой такие группы схожих шрифтов, как Courier, Arial или Times New Roman. Свойство Size представляет размер гарнитуры шрифта. Однако в среде .NET Framework этот размер — не обязательно размер в пунктах. Он может быть размером в пунктах, но при желании свойство GraphicsUnit, определяющее единицу измерения размера шрифта, можно изменить с помощью свойства Unit. Вспомните, что один пункт равен */72 дюйма — т.е. высота шрифта размером в 10 пунктов составляет 10/72 дюйма. Посредством перечисления GraphicsUnit в качестве единицы изменения шрифтов можно указывать одну из следующих:
Глава 33. Введение в GDI+ 1059 □ пункт A/тг дюйма) □ дисплей 0/75 дюйма) □ документ (Узоо дюйма) □ дюйм □ миллиметр □ пиксель Как видите, мы располагаем беспрецедентной гибкостью в указании нужного размера шрифта. Одно из возможных применений этого многообразия возможностей — написание подпрограммы прорисовки текста, которая приемлемым образом должна работать на дисплеях с очень высоким разрешением, на дисплеях с низким разрешением и на принтерах. При прорисовке текста с применением конкретного шрифта на конкретной поверхности рисования часто требуется знать ширину пикселей указанной строки текста. Понятно, почему различные шрифты будут оказывать влияние на ширину пикселей строки — более мелкий шрифт будет приводить к подсвечиванию меньшего количества пикселей. Однако столь же важно знать особенности поверхности рисования, поскольку "пиксельное" разрешение различных поверхностей рисования различно. Как правило, разрешение экрана составляет 72 пикселя на дюйм. Принтеры поддерживают разрешения 300 пикселей на дюйм, 600 пикселей на дюйм и выше. Для вычисления ширины текста при использовании данного шрифта служит метод MeasureString () объекта Graphics. Свойство Style определяет, является ли шрифт курсивным, полужирным, зачеркнутым или подчеркнутым. Для объектов Font всегда вызывайте метод Dispose (). Для создаваемых объектов Font важно вызывать метод Dispose () либо использовать блок using: в противном случае приложение может истощать ресурсы Windows. При прорисовке текста объект Rectangle используют для указания координат границ рисуемого текста. Как правило, высота этого прямоугольника должна быть равной высоте шрифта или кратной ей. Исключение их этого правила требуется только при прорисовке ряда специальных эффектов с применением усеченного шрифта. Класс StringFormat инкапсулирует информацию о размещении текста, в том числе о выравнивании и межстрочном интервале. Следующий пример демонстрирует выравнивание текста по правому краю и по центру с использованием класса StringFormat. В этом практическом занятии мы создадим объект Font и с его помощью выполним прорисовку текста. Праггич«ское занятно ПрориСОВКЭ текста Чтобы на практике проверить прорисовку текста рядом способов, выполните следующие действия. 1. Создайте новое Windows-приложение по имени DrawText в каталоге С:\ BegVCSharp\Chapter33. 2. В конструктор класса Forml после вызова метода InitializeComponent () добавьте вызов метода SetStyleO. Потребуется также изменить границы окна, чтобы обеспечить достаточно места для отображения нужного текста. Измененный конструктор выглядит следующим образом:
1060 Часть V. Дополнительные технологии public Forml() { InitializeComponent(); SetStyle(ControlStyles.Opaque, true); Bounds = new Rectangle @, 0, 500, 300) ; } 3. Добавьте в свой класс метод OnPaint (): protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; int у = 0; g.FillRectangle(Brushes.White, ClientRectangle); // Прорисовка текста, выровненного по левому краю. Rectangle rect = new Rectangle @, у, 400, Font.Height); g.DrawRectangle(Pens.Blue, rect); g.DrawString("This text is left justified.", Font, Brushes.Black, rect); у += Font.Height + 20; // Прорисовка текста, выровненного по правому краю. Font aFont = new Font("Arial", 16, FontStyle.Bold I FontStyle.Italic); rect = new Rectangle @, y, 400, aFont.Height); g.DrawRectangle(Pens.Blue, rect); StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Far; g.DrawString("This text is right justified.", aFont, Brushes.Blue, rect, sf) ; у += aFont.Height + 20; // Вызов метода Dispose () вручную. aFont.Dispose (); // Прорисовка текста, выровненного по центру. Font cFont = new Font("Courier New", 12, FontStyle.Underline); rect = new Rectangle @, y, 400, cFont.Height); g.DrawRectangle(Pens.Blue, rect); sf = new StringFormat(); sf.Alignment = StringAlignment.Center; g.DrawString("This text is centered and underlined.", cFont, Brushes.Red, rect, sf) ; у += cFont.Height + 20; // Вызов метода Dispose () вручную. cFont.Dispose (); // Прорисовка многострочного текста. Font trFont = new Font("Times New Roman", 12); rect = new Rectangle @, y, 400, trFont.Height * 3) ; g.DrawRectangle(Pens.Blue, rect); String longString = "This text is much longer, and drawn "; longString += "into a rectangle that is higher than "; longString += "one line, so that it will wrap. It is "; longString += "very easy to wrap text using GDI+."; g. DrawString(longString, trFont, Brushes.Black, rect); // Вызов метода Dispose () вручную. trFont.Dispose ();
Глава 33. Введение в GDI+ 1061 4. Если теперь скомпилировать и запустить этот код, будет создано окно, показанное на рис. 33.11. This text is right justified. This text is centered and underlined. IThis text is much longer, and drawn into a rectangle that is higher than one line, so that it will wrap It is very easy to |wrap text using GDI+ Рис. 33.11. Выравнивание текста no правому краю и по центру с использованием класса StringForma t Описание полученных результатов Этот пример содержит несколько наиболее часто выполняемых операций прорисовки текста. Как обычно, для удобства мы присваиваем локальной переменной ссылку на объект Graphics. Фон окна будет окрашен белым цветом. При прорисовке текста мы вычисляем ограничивающий прямоугольник текста. Для получения высоты шрифта служит свойство Height. В иллюстративных целях, чтобы ограничивающий прямоугольник текста был отчетливо виден, для него выбран синий цвет. При прорисовке текста мы передаем сам текст, кисть и ограничивающий прямоугольник: // Прорисовка текста, выровненного по левому краю. Rectangle rect = new Rectangle@, у, 400, Font.Height); g.DrawRectangle(Pens.Blue, rect); g.DrawString("This text is left justified.", Font, Brushes.Black, rect); При этом нужно указать только прямоугольник, в котором будет отображаться текст. Базовая линия шрифта — это воображаемая линия, на которой расположено большинство символов шрифта. GDI+ и сам шрифт определяют расположение базовой линии — мы никак не можем влиять на него. При прорисовке текста мы передаем кисть функции DrawString (). В этом примере мы передаем только кисти с чистым цветом. Но с такой же легкостью можно было бы передать другие типы кистей, такие как градиентная кисть. После рассмотрения работы с изображениями в следующем разделе мы рассмотрим прорисовку текста кистью TextureBrush. При первой прорисовке текста в этом примере применялся шрифт, заданный для формы по умолчанию. Этот шрифт указан в свойстве Font, унаследованном из класса Control: g.DrawString("This is a left justified string.", Font, Brushes.Black, rect); Для следующей прорисовки текста в этом примере создается новый экземпляр объекта Font: Font aFont = new Font("Arial", 16, FontStyle.Bold I FontStyle.Italic); Этот пример демонстрирует не только создание нового экземпляра шрифта, но и придание ему нужного стиля. В данном случае использован полужирный и курсивный стили.
1062 Часть V. Дополнительные технологии Пример демонстрирует также создание объекта StringFormat, который позволяет прорисовывать текст, выровненный по правому краю и по центру. В GDI+ выравнивание текста по правому краю носит название Far (По дальнему краю), а по левому — Near (По ближнему краю). StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Far; И, наконец, мы выполняем прорисовку фрагмента многострочного текста. С использованием интерфейса GDI+ это чрезвычайно просто. Достаточно указать прямоугольник, ширина которого меньше длины строки (выраженной в пикселях), а высота достаточна для прорисовки нескольких строк. В данном случае высота прямоугольника в три раза превышает высоту шрифта. Прорисовка с использованием изображений Изображения находят много применений в интерфейсе GDI+. Конечно, можно рисовать изображения в окнах приложения, но можно также создать кисть (TextureBrush), содержащую изображение, и рисовать формы, которые затем будут залиты этим изображением. На основе кисти TextureBrush можно создать перо и рисовать линии, используя выбранное изображение. Кисть TextureBrush можно передать при прорисовке текста, в результате чего текст будет прорисовываться с выбранным изображением. Класс Image определен в пространстве имен System.Drawing. Иногда изображение, которое нужно создать, очень сложно и причудливо, и его прорисовка занимает значительное время даже на современных быстрых компьютерах. Наблюдение за тем, как графическое изображение "выползает" на экран по мере прорисовки — не самое приятное занятие. Примеры таких типов приложений — картографические приложения и сложные приложения CAD/САМ. Рассматриваемая технология вместо прорисовки непосредственно в окне использует прорисовку в изображении, по завершении которой выполняется прорисовка изображения в окне. Эта технология называется двойной буферизацией. Некоторые другие графические технологии применяют прорисовку по слоям, когда вначале выполняется прорисовка фона, затем прорисовка объектов поверх него и, наконец, прорисовка текста поверх объектов. Если эта прорисовка осуществляется непосредственно на экране, пользователи будут наблюдать эффект мерцания. Двойная буферизация устраняет его. Пример использования двойной буферизации будет рассмотрен немного позже. Сам класс Image является абстрактным. Он имеет два производных класса: Bitmap и Metafile. Класс Bitmap представляет изображение общего назначения, обладающее свойствами высоты и ширины. В примерах этого раздела применяется класс Bitmap. Мы загрузим изображение Bitmap из файла и выполним его рисование. На его основе мы также создадим кисть, воспользуемся ею, чтобы создать перо для рисования линий, и затем применим кисть для прорисовки текста. Класс Metafile рассматривается в конце главы при кратком ознакомлении с расширенными возможностями интерфейса GDI+. Для объектов Image всегда вызывайте метод Dispose (). Помните, что для создаваемых объектов Image важно вызывать метод Dispose () либо применять блоки using. В противном случае приложение может истощать ресурсы Windows.
Глава 33. Введение в GDI+ 1063 Существует несколько возможных источников растровых изображений. Растровое изображение можно загрузить из файла, создать его из другого существующего изображения или же создать в виде пустого растрового изображения, поверх которого можно выполнять рисование. Изображение в файле может храниться в формате JPEG, GIF или BMP. Следующее практическое занятие демонстрирует чтение изображения из файла его рисование в окне. Практическое занятие Чтение И рИСОВЭНИе ИЗОбражеНИЯ Следующий очень простой пример считывает изображение из файла и выполняет его рисование в окне. 1. Создайте новое Windows-приложение по имени Drawlmage в каталоге С:\ BegVCSharp\Chapter33. 2. В своем классе Forml объявите приватную переменную для хранения изображения после его чтения из файла. После объявления переменной компонентов добавьте следующее объявление объекта the Image: partial class Forml : Form { private Image theImage; 3. Измените конструктор, как показано ниже: public Forml() { InitializeComponent (); SetStyle(Controlstyles.Opaque, true); thelmage = new Bitmap("Person.bmp") ; } 4. Теперь добавьте в свой класс метод OnPaintEvent (): protected override void OnPaint (PamtEventArgs e) { Graphics g = e.Graphics; g.Drawlmage(thelmage, ClientRectangle); } 5. Освободите объект Image, хранящийся в переменной-члене класса. Измените метод Dispose () класса формы. Обратите внимание, что в среде Visual Studio 2008 метод Dispose () определен в файле Forml .Designer.cs. Простейший способ открытия файла — щелчок на кнопке Show All Files (Показать все файлы) в панели инструментов в верхней части окна Solution Explorer. Эта функция имеет следующий вид: protected override void Dispose ( bool disposing ) { if (disposing) { thelmage.Dispose(); } if (disposing & & (components != null)) { components.Dispose() ; } base.Dispose(disposing);
1064 Часть V. Дополнительные технологии Прежде чем запускать этот пример, необходимо получить ВМР-файл Person.bmp и поместить его в каталог DrawImage\bin\Debug. Файл Person.bmp можно найти в загружаемом коде; также можно воспользоваться другим имеющимся в наличии растровым изображением — но в этом случае не забудьте изменить строку thelmage = new Bitmap("Person.bmp"); добавленную на шаге 3, чтобы указанное в ней имя файла соответствовало нужному растровому изображению. Если теперь запустить скомпилировать и запустить этот код, он создаст изображение, показанное на рис. 33.12. Рис. 33.12. Чтение и рисование изображения Описание полученных результатов В конструкторе выполняется создание экземпляра объекта Bitmap и его присваивание объявленной переменной Image. Затем в методе OnPaint () мы выполняем рисование изображения. При рисовании изображения мы передаем Rectangle в качестве одного из аргументов метода Drawlmage (). Если размеры изображения не соответствуют размерам прямоугольника, переданного методу Drawlmage (), GDI+ автоматически изменяет размеры изображения, чтобы оно помещалось в указанном прямоугольнике. Чтобы предотвратить изменение размеров изображения интерфейсом GDI+, передайте методу Drawlmage () размеры изображения, полученные из свойств Width и Height. Рисование с использованием текстурной кисти Теперь создадим кисть TextureBrush на основе только что использованного изображения и рассмотрим три различных примера ее применения: □ рисование эллипса; □ создание пера; □ прорисовка текста. Следующее практическое упражнение мы начнем с кода, созданного в предыдущем примере, и внесем в него несколько изменений.
Глава 33. Введение в GDI+ 1065 Практическое занятие РиСОВЭНИе ЭЛЛИПСЭ С ИСПОЛЬЗОВЭНИеМ изображения Чтобы выполнить прорисовку фрагмента текста с использованием кисти TextureBrush, выполните следующие действия. 1. Начав с кода, созданного в предыдущем примере Drawlmage, добавьте в класс Forml еще одно объявление переменной Image: partial class Forml : Form { private Image thelmage; private Image smallImage; 2. В конструкторе формы на основе объекта thelmage создайте объект small Image. При этом укажите прямоугольник, высота и ширина которого составляют половину от размеров объекта thelmage. В результате будет создана меньшая версия исходного изображения: public Forml () { InitializeComponent (); SetStyle(ControlStyles.Opaque, true); thelmage = new Bitmap("Person.bmp"); small Image = new Bitmap (thelmage, new Size (thelmage.Width / 2, thelmage.Height / 2)) ; } 3. Замените метод OnPaint () следующей версией: protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.FillRectangle(Brushes.White, ClientRectangle); Brush tBrush = new TextureBrush(smalllmage, new Rectangle @, 0, smalllmage.Width, smalllmage.Height)); g.FillEllipse (tBrush, ClientRectangle); tBrush.Dispose(); } 4. Избавьтесь от двух изображений, хранящихся в переменных-членах класса. Измените метод Dispose () класса (в файле Forml .Designer. cs) следующим образом: protected override void Dispose ( bool disposing ) { if (disposing) thelmage.Dispose(); smalllmage.Dispose(); f (disposing & & (components != null)) components.Dispose(); base.Dispose(disposing);
1066 Часть V. Дополнительные технологии 5. При запуске этого приложения окно должно выглядеть подобно показанному на рис. 33.13. х********** mmmm mm******* ************* и*********** *********** Рис. 33.13. Рисование эллипса с использованием изображения Описание полученных результатов При создании объекта TextureBrush мы передаем конструктору прямоугольник, определяющий, какая часть изображения будет использоваться для создания кисти. В данном случае мы указываем, что будет использовано все изображение. При выполнении любой прорисовки с применением кисти TextureBrush используется растровое изображение, а не чистый цвет. Большая часть кода этого примера уже была пояснена в данной главе. Различие состоит в том, что мы вызываем метод FillEllipse () класса Graphics, передавая ему только что созданную текстурную кисть, и ClientRectangle выполняет прорисовку эллипса в окне. g.FillEllipse(tBrush, ClientRectangle); В следующем практическом упражнении мы создадим объект TextureBrush, а затем на его основе создадим перо. Практическое занятие СоЗДЭНИе пера НЭ ОСНОВе изображения Теперь, когда объект TextureBrush создан, с его помощью можно создать перо. 1. Начните с примера Drawlmage, измененного в предыдущем практическом упражнении, и измените метод OnPaint () следующим образом: protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g. FillRectangle(Brushes.White, ClientRectangle); Brush tBrush = new TextureBrush(smalllmage, new Rectangle@, 0, smalllmage.Width, smalllmage.Height)); Pen tPen = new Pen (tBrush, 40) ; g.DrawRectangle(tPen,0,0, ClientRectangle.Width, ClientRectangle.Height); tPen.Dispose(); tBrush.Dispose();
Глава 33. Введение в GDI+ 1067 2. При запуске этого кода результат должен выглядеть подобно показанному на рис. 33.14. Рис. 33.14. Создание пера на основе изображения В следующем практическом упражнении мы выполним прорисовку текста, используя созданный объект TextureBrush. Практическое занятие Прорисовка текста с использованием изображения Теперь можно выполнить прорисовку текста, используя объект TextureBrush. 1. Продолжая работу с кодом, созданным в предыдущем практическом занятии, измените метод OnPaint следующим образом: protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.FillRectangle(Brushes.White, ClientRectangle); Brush tBrush = new TextureBrush(smalllmage, new Rectangle @, 0, smalllmage.Width, smalllmage.Height)); Font trFont = new Font ("Times New Roman", 32, FontStyle.Bold | FontStyle.Italic); g.DrawString("Hello from Beginning Visual C#", trFont, tBrush, ClientRectangle); tBrush.Dispose(); trFont.Dispose(); 2. В этом примере будет использовано другое растровое изображение — поэтому в конструкторе формы измените строку, определяющую источник изображения the Image, следующим образом: thelmage = new Bitmap("Tile.bmp"); Файл Tile .bmp также можно найти в загружаемом коде. 3. Если теперь запустить этот код, результат должен быть подобен показанному на рис. 33.15.
1068 Часть V. Дополнительные технологии Hello from Beginning Visual C# Рис. 33.15. Прорисовка текста с использованием изображения Вызов метода Drawstring () аналогичен предыдущим случаям использования этого метода. В качестве аргументов он принимает текст, шрифт, созданную текстурную кисть и ограничивающий прямоугольник: g.DrawString("Hello from Beginning Visual C#", trFont, tBrush, ClientRectangle); Двойная буферизация Вспомните, что ситуация, когда прорисовка занимает слишком длительное время и пользователь вынужден долго дожидаться, пока нарисованное изображение появится на экране, может оказаться неприемлемой. Как уже было отмечено ранее, решением этой проблемы является выполнение рисования в изображение, а по завершении всех операций прорисовки — прорисовка всего изображения в окне. В следующем практическом занятии вы увидите преимущества двойной буферизации. Воздерживаясь от прорисовки на экране до момента полной готовности изображения, можно повысить производительность приложения. практическое занятие Двойная буферизации" Чтобы создать более производительное приложение, выполните следующие действия. 1. Создайте новое Windows-приложение по имени DoubleBuf f er и добавьте в него следующий метод OnPaint (), выполняющий рисование множества линий, окрашенных в случайные цвета: protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; Random r = new Random () ; g.FillRectangle(Brushes.White, ClientRectangle); for (int x = 0; x < ClientRectangle.Width; x++) { for (int у = 0; у < ClientRectangle.Height; у += 10) { Color с = Color.FromArgb(r.Next B55), r.NextB55), r.Next B55));
Глава 33. Введение в GDI+ 1069 using (Pen p = new Pen (с, 1)) { g.DrawLine(p, new Point @, 0), new Point(x, y)); } } } } 2. При выполнении этого кода прорисовка выполняется на наших глазах (конечно, если компьютер не является слишком быстрым). По завершении прорисовки окно выглядит, как показано на рис. 33.16. Рис. 33.16. Рисование без двойной буферизации 3. Теперь добавьте в приложение функцию двойной буферизации — если заменить метод OnPaint () следующей версией, графические элементы будут прорисовываться все сразу, по истечении одной-двух секунд: protected override void OnPaint(PaintEventArgs e) { Graphics displayGraphics = e.Graphics; Random r = new Random () ; Image i = new Bitmap (ClientRectangle.Width, ClientRectangle. Height) ; Graphics g = Graphics.Fromlmage(i) ; g.FillRectangle(Brushes.White, ClientRectangle); for (int x = 0; x < ClientRectangle.Width; x++) { for (int у = 0; у < ClientRectangle.Height; у += 10) { Color с = Color.FromArgb (r.Next B55), r.NextB55), r.NextB55)); Pen p = new Pen(c, 1); g.DrawLine(p, new Point @, 0), new Point(x, y)); p.Dispose (); } } displayGraphics.Drawlmage(i, ClientRectangle); i.Dispose(); }
1070 Часть V. Дополнительные технологии Описание полученных результатов Часть кода, отвечающая за рисование линий, понятна — метод DrawLine () рассматривался ранее в этой главе. Единственный элемент в этой части кода, заслуживающий отдельного упоминания — статический метод FromArgb () структуры Color, который создает структуру Color из трех переданных целочисленных значений, соответствующих красному, зеленому и синему компонентам цвета. В коде двойной буферизации (шаг 3) следующая строка создает новое изображение, высота и ширина которого равны размерам прямоугольника ClientRectangle: Image i = new Bitmap(ClientRectangle.Width, ClientRectangle.Height); Затем следующая строка получает объект Graphics из изображения: Graphics g = Graphics.Fromlmage(i); Все операции прорисовки аналогичны выполняемым в предыдущем коде, за исключением того, что теперь они выполняются в изображение, а не непосредственно в окно. И, наконец, в самом конце, код выполняет рисование изображения в окне: displayGraphics.Drawlmage(i, ClientRectangle); Поскольку вначале линии рисуются в невидимое изображение, приходится подождать некоторое время, прежде чем что-либо появится на экране. Расширенные возможности GDI+ Мы лишь вскользь коснулись многих возможностей, предоставляемых интерфейсом GDI+. Существует множество других возможных его применений — значительно больше, чем можно рассмотреть в одной главе. Однако, чтобы придать этой главе завершенный вид, опишу несколько областей применения этих расширенных возможностей. Отсечение Отсечение важно в трех ситуациях. Во-первых, при вызове метода OnPaint () в сочетании с объектом Graphics событие передает прямоугольник отсечения. В простых подпрограммах рисования этому прямоугольнику отсечения можно не уделять особого внимания, но в очень сложных подпрограммах прорисовки, занимающих длительное время, время рисования можно уменьшить, проверяя этот прямоугольник до начала рисования. Ограничивающий прямоугольник любого графического изображения или фигуры, которые нужно нарисовать, известен. Если этот ограничивающий прямоугольник не пересекается с прямоугольником отсечения, операцию прорисовки можно пропустить. На рис. 33.17 показано окно, содержащее гистограмму, которое частично заслонено другим окном. После закрытия калькулятора и прорисовки границы окна операционной системой Windows окно гистограммы выглядит, как показано на рис. 33.18. На этом этапе метод OnPaint () должен быть вызван применительно к окну гистограммы, причем прямоугольник отсечения должен быть установлен в соответствии с областью, открывающейся в результате закрытия окна и показанной на рис. 33.18 черным цветом. Теперь гистограмма должна была бы выполнить прорисовку тех участков своего окна, которые ранее располагались под перекрывающим его окном. Столбцы Cars и Trains перерисовки не требуют, и фактически, даже если бы метод OnPaint () попытался выполнить рисование в других областях окна, кроме открытой, он не смог бы этого сделать. Любые выполненные им операции рисования игнорировались бы.
Глава 33. Введение в GDI+ 1071 Backspace | СЕ С мс | III!! MR | 4 J 5 J Ь J | X I MS J 1 J 2 J 3 J J 1/x J 4 _J_J_J_J_J Рис. 33.17. Окно с гистограммой заслонено другим окном Рис. 33.18. Окно с гистограммой после закрытия другого окна Окну гистограммы известен ограничивающий прямоугольник столбца Cars, и оно может определить, пересекается ли он с открытым участком окна. Определив, что эти прямоугольники не пересекаются, программа прорисовки не выполняет перерисовку той части дисплея, которая содержит столбец Cars. Иногда, когда требуется нарисовать только часть фигуры или графического изображения, удобнее нарисовать всю фигуру и выполнить отсечение изображения до того, которое должно отображаться. Например, может требоваться рисование только фрагмента имеющегося изображения. Вместо того чтобы создавать новое изображение, представляющее фрагмент исходного, можно соответствующим образом установить прямоугольник отсечения, а затем выполнить рисование изображения так, чтобы только нужный его фрагмент проглядывал сквозь область отсечения. Именно эта методика применяется при создании прямоугольной области выделения. Последовательно
1072 Часть V. Дополнительные технологии изменяя позицию рисования текста и одновременно устанавливая область отсечения, можно создавать эффект горизонтального перемещения текста. И, наконец, существует методика, посредством которой можно создавать смотровое отверстие в графическом изображении большего размера. Пользователь может перемещать это смотровое отверстие, перетаскивая его мышью или манипулируя линейками прокрутки. Смотровое отверстие можно перемещать также программно в соответствии с другими действиями, выполняемыми пользователем. В этом случае установка области отсечения и прорисовка только того, что должно "проглядывать" сквозь него — вполне подходящая технология. Для получения дополнительной информации обратитесь к описанию свойства Clip класса Graphics в документации каркаса .NET. Пространство имен System.Drawing.Drawing2D Классы этого пространства имен предоставляют расширенные функциональные возможности работы с двумерными и векторными графическими элементами. Эти классы можно использовать для построения сложных приложений рисования и обработки изображений. Векторная графика — это технология, при которой программист вообще не обращается к пикселям. Вместо этого он записывает множество векторов, указывая такие операции, как "рисование от одной точки к другой ", "рисование прямоугольника в определенной позиции" и т.п. Затем разработчик может применять масштабирование, поворот и другие трансформации. В случае применения трансформаций все операции визуализируются в окне одновременно. Это пространство имен включает перечисленные ниже классы. □ Усовершенствованные кисти. Мы уже рассмотрели кисть LinearGradientBrush и вскользь затронули кисть PathGradientBrush. Существует также кисть HatchBrush, которая выполняет прорисовку в стиле штриховки, используя цвета переднего плана и фона. □ Класс Matrix. Этот класс определяет геометрические преобразования. Он позволяет выполнять преобразования применительно к операциям рисования. Например, используя класс Matrix, можно нарисовать скошенный овал. □ Класс GraphicsPath. Мы уже вскользь затрагивали этот класс. С его помощью можно определить сложный путь и выполнить рисование сразу всего пути. Пространство имен System.Drawing. Imaging Классы этого пространства имен предоставляют расширенную поддержку работы с изображениями, такую как работа с метафайлами. Метафайл описывает последовательность графических операций, которые могут быть записаны и впоследствии воспроизведены, и пространство имен System. Drawing. Imaging содержит ряд классов, которые позволяют расширить возможности GDI+ для обеспечения поддержки других форматов изображений. Резюме В этой главе был описан ряд классов пространства имен System. Drawing. Мы выяснили, как класс Graphics инкапсулирует поверхность рисования, и рассмотрели механизм прорисовки, в котором событие OnPaint вызывается каждый раз, когда окно требует перерисовки.
Глава 33. Введение в GDI+ 1073 Мы рассмотрели цвета и системы координат и структуры Point, Size и Rectangle, используемые для указания позиций и размеров поверхностей рисования. Затем мы рассмотрели ряд примеров рисования линий, форм, текста и изображений. В этой главе рассмотрены следующие вопросы. □ Класс Graphics. □ Структура Color. □ Рисование линий посредством использования класса Реп. □ Рисование форм посредством использования класса Brush. □ Прорисовка текста посредством использования класса Font. □ Прорисовка изображений посредством использования класса Bitmap. □ Выполнение рисования в изображения (двойная буферизация). Мы выяснили также, что очень важно вызывать метод Dispose () применительно к следующим классам по завершении работы с ними: □ Graphics □ Pen □ Brush □ Font □ Image И, наконец, были кратко описаны дополнительные возможности, предоставляемые каркасом .NET Framework. Упражнения 1. Создайте небольшое приложение, которое предпринимает попытки неправильного освобождения объекта Реп, полученного из класса Pens. Обратите внимание на результирующее сообщение об ошибке. 2. Создайте приложение, которое отображает трехцветный светофор. Цвет светофора должен меняться, когда пользователь щелкает на нем. 3. Создайте приложение, принимающее RGB-значения. Соответствующий цвет должен отображаться в прямоугольнике. Обеспечьте отображение HSB-значе- ний для данного цвета.
Windows Presentation Foundation В этой книге до сих пор речь шла только о приложениях двух основных видов, а именно — о настольных приложениях, которые пользователи запускают напрямую, и о Web-приложениях, к которым пользователи получают доступ через браузер. Эти приложения создаются с помощью двух разных компонентов .NET Framework — Windows Forms и ASP.NET — и имеют как свои преимущества, так и недостатки. В частности, настольные приложения обладают большей степенью гибкости и реактивности, а Web-приложения предусматривают возможность получения к ним удаленного доступа одновременно многими пользователями. Однако в современном мире компьютерных технологий границы между приложениями все больше и больше стираются. С появлением Web-служб и служб WCF (о которых более подробно будет рассказываться в главе 35), настольные приложения и Web-приложения могут функционировать более распределенным образом, обмениваясь данными как по локальной, так и по глобальной сети. Вдобавок к клиентским Web- приложениям (т.е. браузерам вроде Internet Explorer и Firefox) можно уже больше не относиться как к так называемым "тонким" клиентам, не обладающим больше никакими функциональными возможностями, кроме как простой возможности отображения информации на экране. Новейшие браузеры и компьютеры, на которых они работают, способны на гораздо большее. В последние годы наблюдался постепенный переход к обеспечению однородного впечатления пользователей от работы со всеми приложениями. Web-приложения теперь, как правило, предусматривают применение технологий вроде JavaScript, Flash, Java и т.д. и все больше ведут себя подобно настольным приложениям. Чтобы убедиться в этом, достаточно просто взглянуть на возможности такого Web-приложения, как Google Docs. Что касается настольных приложений, то они, напротив, стали гораздо более "подключаемыми" с возможностями, начиная от самых простых (вроде автоматического обновления, получения справочной информации в онлайновом режиме и т.п.) и заканчивая очень сложными (вроде использования онлайновых источников данных и участия в одноранговых сетях). Схематично этот процесс показан на рис. 34.1.
Глава 34. Windows Presentation Foundation 1075 Рис. 34.1. Обеспечение однородности Windows Presentation Foundation (WPF) является как раз одной из объединяющий технологий, которые позволяют писать приложения, ликвидирующие разрыв между настольной системой и Internet. WPF-приложение, как будет показано позже в этой главе, может функционировать как в виде настольного приложения, так и в виде Web- приложения внутри браузера. Еще существует сокращенная версия WPF, которая называется Silverlight и которой можно пользоваться для добавления динамического содержимого в Web-приложения. В настоящей главе речь пойдет о технологии WPF и том, как ее применять для создания приложений следующего поколения. В этой главе будут рассматриваться следующие темы. □ Что собой представляет WPF Структура базового WPF-приложения. Основные понятия WPE □ □ □ Программирование с использованием WPF. Что собой представляет WPF WPF (ранее известная как Avalon) представляет собой технологию, которая позволяет писать не зависящие от платформы приложения с четким разделением между дизайном и функциональными возможностями. В ее основе лежат заимствованные и расширенные понятия и классы из многих предыдущих технологий, подобных Windows Forms, ASP.NET, XML, методик привязки данных, GDI+ и т.д. Те, у кого имеется опыт сборки Web-приложения с помощью .NET Framework, при первом же взгляде на код WPF-приложения смогут сразу же увидеть множество таких сходств. В частности, в WPF-приложениях используется разметка плюс модель на основе отделенного кода, подобная той, что применяется в ASP.NET. Однако на более глубоком уровне в WPF имеется ровно столько же отличий, сколько и сходств, которые вместе обеспечивают совершенно новое впечатление от применения этой технологии как для разработчиков, так и для пользователей.
1076 Часть V. Дополнительные технологии Одной из ключевых концепций в разработке WPF-приложений является практически полное разделение дизайна и функциональных возможностей. Это разделение позволяет дизайнерам и разработчикам кода на языке С# работать над проектами вместе с такой степенью свободы, для получения которой ранее требовалось применять либо усовершенствованные концепции проектирования, либо сторонние средства. Эта возможность придется по душе всем — и небольшим командам с разработчиками- любителями, и многочисленным командам разработчиков и дизайнеров, работающих вместе над масштабными проектами. В следующих разделах вы увидите, какие преимущества технология WPF предоставляет дизайнерам и разработчикам и каким образом она позволяет им работать вместе. Возможности, которые WPF предлагает для дизайнеров Для проектирования пользовательского интерфейса в WPF применяется язык XAML (Extensible Application Markup Language — расширяемый язык разметки приложений). Он похож на язык разметки, применяемый в ASP.NET, тем, что тоже подразумевает использование XML-синтаксиса и позволяет добавлять элементы управления в пользовательский интерфейс декларативным, иерархическим образом. Другими словами, он позволяет добавлять элементы управления в виде XML-элементов и задавать их свойства с помощью XML-атрибутов. Еще он позволяет создавать элементы управления, содержащие другие элементы управления, что играет важную роль как для компоновки, так и для функциональных возможностей приложения. Язык XAML, однако, гораздо мощнее ASP.NET и не ограничивается только возможностями HTML при визуализации данных для отображения пользователю. Он разрабатывался с учетом всех существующих сегодня мощных графических карт и потому позволяет пользоваться всеми усовершенствованными возможностями, которые эти графические карты предлагают, посредством DirectX 7 и последующих версий. Некоторые из этих возможностей перечислены ниже. Q Координаты с плавающей запятой и векторная графика для обеспечения компоновки, которую можно масштабировать, вращать и трансформировать любым другим образом без потери качества. □ Двухмерные и трехмерные возможности для усовершенствованной визуализации. □ Усовершенствованные средства для обработки и визуализации шрифтов. □ Сплошные, градиентные и текстурные заливки с необязательными эффектами прозрачности для объектов пользовательского интерфейса. □ Технология раскадровки анимации, которую можно применять во всех видах ситуаций, в том числе и в генерируемых пользователем событиях, вроде выполнения щелчков на кнопках. □ Многократно используемые ресурсы, которые можно применять для динамической стилизации элементов управления. Многие из этих функциональных возможностей рассчитаны специально на применение в операционных системах семейства Microsoft Vista, у которых имеются дополнительные графические возможности, доступные через интерфейс Aero. Однако WPF-приложения могут также функционировать и в других операционных системах, например, в Windows XP. Система визуализации, встроенная в .NET Framework 3.5, способна визуализировать XAML-данные (с соответствующей утратой производительности) в случае, если графической карте это по какой-то причине не удается. В состав сред VS и VCE входят различные возможности для создания и стилизации XAML-кода, но для дизайнеров главный интерес представляет средство под названием
Глава 34. Windows Presentation Foundation 1077 Microsoft Expression Blend (ЕВ). Это пакет инструментов для проектирования и компоновки, которым дизайнеры могут пользоваться для создания XAML-файлов, которые разработчики далее могут использовать непосредственно при создании приложений. На самом деле в ЕВ предлагаются те же самые файлы решений и проектов, что и в VS и VCE, поэтому проект можно как создавать, так и затем редактировать в любой из этих сред. В ЕВ все, что требуется для редактирования файла с отделенным кодом — это дважды щелкнуть на нем окне Files (Файлы), которое является эквивалентом окна Solution Explorer в VS и VCE. На рис. 34.2 показано, как выглядит интерфейс ЕВ. Рис. 34.2. Интерфейс ЕВ Узнать больше и загрузить демонстрационную версию ЕВ (а также версию ЕВ2, которая на момент написания настоящей книги находилась на стадии разработки) можно по адресу microsoft.com/expression/products/overview.aspx?key=blend. Однако при этом не следует забывать о том, что для написания WPF-приложений и редактирования XAML наличие ЕВ обязательным вовсе не является. На рис. 34.8 показан тот же самый проект, что и на рис. 34.2, но только загруженный в VCE. К сожалению, отличия между файлами решений в VS и VCE версий 2005 и 2008 не позволяют работать с текущей версией ЕВ. Тем не менее, существует конфигурационная программа, с помощью которой можно конфигурировать пакет ЕВ так, чтобы он работал с VS 2008 и VCE2 008. Загрузить эту программу можно по следующему адресу: http://blogs.msdn.com/expression/archive/2007/05/29/working-with- visualstudio-code-name-orcas-and-expression-blend.aspx При запуске этой программы (BlendConfiguration. exe) нужно выбрать опцию, предусматривающую конфигурирование ЕВ под Visual Studio с кодовым именем "Orcas". Надеемся, что в будущих выпусках ЕВ и ЕВ2 эта проблема будет устранена.
1078 Часть V. Дополнительные технологии 3 Solution VYPfSorttcti |1 project) ► _^ R*f«r«ncti . . App «КП1 jj AMtmBlytnfO о • - Windowl_x»ml M В КАК* • » ■ i -Щ <L*b«:l.H4iwlecTi •»n*«otie> <Тг»паХое««*соир> <Гс-л1*Гг*пл1ог»г« • -- l • t»"l"/> <3k«»Tr чпэЮгр U -• " -.-"/> <Rot«t«Tc*&ato» • „i*»"-l7.iil"/> <Tr*n»l«*Tc«iuJtprn. :—2.8625739509916*12" (••M.7MC </Tr*n»fortnOcoup> <,'l«usei . Pvrvlet гсю.-forn.-- Рис. 54.5. Интерфейс VCE В VCE по умолчанию отображается XAML-код (о котором пока не стоит беспокоиться) и его визуализированная, предназначенная для предварительного просмотра версия в двух отдельных панелях основного окна. Окно редактора свойств выглядит несколько менее интуитивно, и при выборе в нем объектов проявляются некоторые слегка необычные особенности этого редактора. Например, на рис. 34.3 выбран элемент управления Label, но область, выделенная в окне редактора свойств, соответствует ему не совсем точно. При запуске приложения из обеих сред результат будет выглядеть одинаково, как показано на рис. 34.4. Рис. 34.4. Результат запуска приложения из ЕВ и VCE
Глава 34. Windows Presentation Foundation 1079 Возможности, которые WPF предлагает для разработчиков приложений на С# Как уже отмечалось в предыдущем разделе, разработчики могут создавать проекты и решения, пригодные для работы в VS или VCE, а дизайнеры — редактировать эти же проекты и решения в Expression Blend. В отличие от дизайнеров, однако, большую часть времени разработчикам доводится проводить за работой в VS или VCE. В WPF, как уже упоминалось во вводной части этого раздела, применяется модель отделенного кода во многом так же, как и в ASP.NET. Например, добавить обработчик событий для элемента управления Button можно путем добавления к представляющему его XML-элементу атрибута Click. Этот атрибут указывает на имя обработчика событий в файле отделенного кода соответствующей XAML-страницы, который может быть написан на С#. Обратите внимание на то, что в WPF-приложениях можно еще манипулировать элементами управления подобно тому, как это делается в приложениях Windows Forms, примененяя программные приемы для компоновки пользовательских интерфейсов. С помощью отделенного кода можно создавать экземпляр элемента управления, устанавливать свойства, присоединять обработчики событий и добавлять элемент управления в окно. Он позволяет, по сути, полностью обходить код XAML. Тме не менее, обычно он все-таки будет занимать гораздо больше места, чем соответствующий декларативный XAML-код и приводить к утрате разделения между дизайном и функциональными возможностями. Поэтому, хотя в некоторых ситуациях и бывает необходимо делать отдельные вещи программным путем, вообще для компоновки элементов управления в пользовательском интерфейсе лучше применять XAML. В настоящей главе материал подается больше с расчетом именно на разработчиков приложений на С#. Теме WPF посвящены целые книги, а здесь вы сможете получить основные навыки, необходимые для быстрого начала работы с этой технологией, а также краткие сведения о предоставляемых ею возможностях. Структура базового WPF-приложения WPF является довольно интуитивно понятной технологией в плане использования, и потому наилучшим способом познакомиться с ней будет сразу же окунуться в нее и начать экспериментировать. Многие из приемов будут выглядеть уже знакомо. В следующем практическом занятии демонстрируется пример создания простого WPF-приложения, а в идущем после него разделе "Описание полученных результатов" выполнятся анализ кода и результатов для предоставления более четкой картины касательно того, как все это работает вместе. актическое занятие СоЗДЭНИе бЭЗОВОГО WPF-прИЛОЖенИЯ 1. Создайте новое WPF-приложение по имени Ch34Ex01 и сохраните его в каталоге С:\BegVCSharp\Chapter34. 2. Измените код в его файле Windowl. xaml следующим образом: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml" x:Class="Ch34Ex01.Windowl" Title="Color Spinner" Height=70" Width=70">
1080 Часть V. Дополнительные технологии <Window.Resources> <Storyboard x:Key="Spin"> <DoubleAnimationUsingKeyFrames BeginTime=0:00:00" Storyboard.TargetName="ellipsel" Storyboard.TargetProperty= "(UIElement.RenderTransform).(TransformGroup.Children) [0]. (RotateTransform.Angle) " RepeatBehavior="Forever"> <SplineDoubleKeyFrame KeyTime=0:00:10" Value=60"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime=0:00:00" Storyboard.TargetName="ellipse2" Storyboard.TargetProperty= "(UIElement.RenderTransform).(TransformGroup.Children) [0]. (RotateTransform.Angle)" RepeatBehavior="Forever"> <SplmeDoubleKeyFrame KeyTime=0:00:10" Value="-360"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime=0:00:00" Storyboard.TargetName="ellipse3" Storyboard.TargetProperty= "(UIElement.RenderTransform).(TransformGroup.Children)[0].(RotateTransform.Angle) " RepeatBehavior="Forever"> <SplineDoubleKeyFrame KeyTime=0:00:05" Value=60"/ > </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime=0:00:00" Storyboard.TargetName="ellipse4" Storyboard.TargetProperty= "(UIElement.RenderTransform).(TransformGroup.Children)[0].(RotateTransform.Angle)" RepeatBehavior="Forever"> <SplineDoubleKeyFrame KeyTime=0:00:05" Value="-360"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </Wi^ndow. Resources> <Window.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard Storyboard="{StaticResource Spin}" x:Name="Spin_BeginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="goButton"> <ResumeStoryboard BeginstoryboardName="Spin_BeginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="stopButton"> <PauseStoryboard BeginstoryboardName="Spin_BeginStoryboard" /> </EventTrigger> </Window.Triggers> <Window.Background> <LinearGradientBrush EndPoint=.5,1" StartPoint=.5, 0"> <GradientStop Color="#FFFFFFFF" Offset=" /> <GradientStop Color="#FFFFC45A" Offset="l" /> </LinearGradientBrush> </Window.Васkground> <Grid> <Ellipse Margin=0,50,0,0" Name="ellipse5" Stroke="Black" Height=50" HorizontalAlignment="Left" VerticalAlignment="Top" Width=50"> <Ellipse.BitmapEffect> <BlurBitmapEffect Radius=0" /> </Ellipse.BitmapEffect> <Ellipse.Fill>
Глава 34. Windows Presentation Foundation 1081 <RadialGradientBrush> <GradientStop Color=M#FF000000" 0ffset="l" /> <GradientStop Color="#FFFFFFFF" Offset=.306" /> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> <Ellipse Margin=5,85,0,0" Name="ellipsel" Stroke="{x:Null}" Height="80" HorizontalAlignment="Left" VerticalAlignment="Top" Width=20" Fill="Red" Opacity=.5" RenderTransformOrigin=.92,0.5" > <Ellipse.BitmapEffect> <BevelBitmapEffect /> </Ellipse.BitmapEffect> <Ellipse.RenderTransform> <TransformGroup> <RotateTransform Angle=" /> </TransformGroup> </Ellipse.RenderTransform> </Ellipse> <Ellipse Margin="85,15,0,0" Name="ellipse2" Stroke="{x:Null}" Height=20" HorizontalAlignment="Left" VerticalAlignment="Top" Width="80" Fill="Blue" Opacity=.5" RenderTransformOrigin=.5,0.92"> <Ellipse.BitmapEffect> <BevelBitmapEffect/> </Ellipse.BitmapEffect> <Ellipse.RenderTransform> <TransformGroup> <RotateTransform Angle=" /> </TransformGroup> </Ellipse.RenderTransform> </Ellipse> <Ellipse Margin=15,85,0,0" Name="ellipse3" Stroke="{x:Null}" Height="80" HorizontalAlignment="Left" VerticalAlignment="Top" Width=20" Opacity=.5" Fill="Yellow" RenderTransformOrigin=.08,0.5"> <Ellipse.BitmapEffect> <BevelBitmapEffect /> </Ellipse.BitmapEffect> <Ellipse.RenderTransform> <TransformGroup> <RotateTransform Angle=" /> </TransformGroup> </Ellipse.RenderTransform> </Ellipse> <Ellipse Margin="85,115,0,0" Name="ellipse4" Stroke="{x:Null}" Height=20" HorizontalAlignment="Left" VerticalAlignment=,,Top" Width="80" Opacity=.5" Fill="Green" RenderTransformOrigin=.5,0.08"> <Ellipse.BitmapEffect> <BevelBitmapEffect /> </Ellipse.BitmapEffect> <Ellipse.RenderTransform> _ <TransformGroup> <RotateTransform Angle=" /> </TransformGroup> </Ellipse.RenderTransform> </Ellipse> <Button Height=3" HorizontalAlignment="Left" Margin=0,0,0,56" Name="goButton" VerticalAlignment="Bottom" Width=5" Content="Go" /> <Button Height=3" HorizontalAlignment="Left" Margin=52,0,0,56" Name="stopButton" VerticalAlignment="Bottom" Width=5" Content="Stop" />
1082 Часть V. Дополнительные технологии <Button Height=3" HorizontalAlignment="Left" Margm="85, 0, 86,16м Name="toggleButton" VerticalAlignment="Bottom" Width=5" Content="Toggle"> </Grid> </Window> 3. Дважды щелкните на кнопке Toggle (Переключить) в режиме конструктора (который показан на рис. 34.5 и при котором представление XAML отображается в свернутом виде). Window l.x* ml ш , Stop d De*ign В XAML Button (toggleButtonl шва Рис. 34.5. Кнопка Toggle 4. Измените код в файле Windowl.xaml.cs следующим образом (как оператор using, так и новый код в обработчике toggleButtonClick (), которые были добавлены автоматически после выполнения двойного щелчка на кнопке): using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows. Media. Animation ; namespace Ch34Ex01 { /// <summary> /// Логика взаимодействия для Windowl.xaml /// </summary>
Глава 34. Windows Presentation Foundation 1083 public partial class Windowl : Window { public Windowl () { InitializeComponent (); } private void toggleButton_Click(object sender, RoutedEventArgs e) { Storyboard spinStoryboard = Resources ["Spin"] as Storyboard; if (spinStoryboard != null) { if (spinStoryboard.GetlsPaused(this)) { spinStoryboard.Resume(this); } else { spinStoryboard.Pause(this); 5. Запустите приложение и попробуйте повключать, поостанавливать и поперек- лючать анимацию. На рис. 34.6 показан пример. Рис. 34.6. Пример работы с анимацией 6. Создайте новое WPF-приложение типа браузера (WPF Browser) по имени Ch34Ex01Web и сохраните его в каталоге С: \BegVCSharp\Chapter34. 7. Измените значение атрибута Title элемента <Раде> в Pagel.xaml на Color Spinner Web. 8. Откройте файл Windowl. xaml из приложения Ch34Ex01 и скопируйте весь код из элемента <Window> в этом файле в элемент <Раде> в Pagel .xaml. 9. Замените элементы <Window. Resources>, <Window. Triggers> и <Window. Background> на элементы <Page.Resources>, <Page.Triggers> и <Page.Background> соответствующим образом (не забудьте также изменить начальные и конечные дескрипторы для этих элементов).
1084 Часть V. Дополнительные технологии 10. Удалите пять элементов <Ellipse.BitmapEffect> и их содержимое из Pagel .xaml. 11. Скопируйте обработчик событий toggleButtonClick () и оператор using для пространства имен System. Windows.Media. Animation из файла Windowl. xaml. cs в файл Pagel. xaml. cs. 12. Запустите приложение Ch34Ex01Web. На рис. 34.7 показан результат, который должен получиться. | Ш C:\8^VCSherpNCh*pttr34\Cr\34€Jrt1V^bSChMEx01\^b0>^\O*bog\Ch34Ex01WtbJCb*p... I I fil ШШ ] ' \Ш CABegVCSh«rp\ChapterM\Ch34EitlW ' | *t | X I I bve Seorch fi • Г~^~Л ГтП i% Computer | Protected Mode: Off Рис. 34.7. Приложение Ch34Ex01 Web в действии Описание полученных результатов В этом примере мы создали простое приложение, умеющее вращать цветные овалы и позволяющее запускать и останавливать этот процесс. К сожалению, черно-белые экранные снимки не передают получаемого эффекта полностью, но при самостоятельном выполнении кода это приложение должно оставлять хотя бы частично приятное с эстетической точки зрения впечатление. Нам пришлось добавить довольно много кода для достижения такого результата, но если присмотреться к нему, то будет нетрудно заметить, что большая его часть повторяется, без чего нельзя было обойтись, поскольку анимацию нужно было обеспечивать для целых четырех эллипсов. Еще можно заметить, что в файле отделенного кода мы добавили совсем немного кода С#, а именно — код лишь для одной из трех кнопок. Это было сделано для того, чтобы проиллюстрировать два следующих важных момента. □ Дизайнеры могут создавать привлекательные пользовательские интерфейсы со сложными графическими возможностями, анимационными эффектами и способностью взаимодействия с пользователем с помощью не более чем одного XAML-кода. □ При необходимости над пользовательским интерфейсом на базе XAML-кода можно получать полный контроль из отделенного кода.
Глава 34. Windows Presentation Foundation 1085 Еще мы показали, как использовать тот же код, что и в настольном приложении, в Web-приложении. Несколько изменений (каких именно — мы расскажем позже) все- таки потребовалось, но основные функциональные возможности выглядели в обеих средах одинаково. В коде приведенного приложения мы продемонстрировали многие из возможностей WPF для ознакомления с некоторыми ключевыми приемами. Начнем мы с анализа настольного приложения, а потом разберемся с теми изменениями, которые потребовалось внести для создания его Web-версии. В первую очередь давайте взглянем на XAML-файл настольного приложения Windowl. xaml и самый высокоуровневый элемент в нем: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Ch34Ex01.Windowl" Title="Color Spinner" Height=70" Width=70"> </Window> Элемент <Window>, используется для определения окна. Приложение могло бы состоять из нескольких окон, каждое из которых тогда бы содержалось в отдельном XAML-файле. Это, однако, вовсе не означает, что в XAML-файле всегда определяется только окно: в XAML-файлах могут также содержаться пользовательские элементы управления, кисти, другие ресурсы, Web-страницы и многое другое. В проекте Ch34Ex01, например, присутствует даже такой XAML-файл, в котором содержится определение самого приложения. Называется этот файл Арр. xaml. О нем и определении приложения более подробно будет рассказываться позже в этой главе. Что касается файла Windowl .xaml, то в нем нужно обратить внимание на то, что в элементе <Window> содержатся кое-какие довольно понятные атрибуты. Первые два отвечают за объявление пространств имен: один за объявление глобального пространства имен, которое должно использоваться для XML, а другой за объявление пространства имен х. Оба они являются существенно важными для работы WPF и определяют словарь для синтаксиса XAML. Далее идет атрибут Class, взятый из пространства имен х. Этот атрибут связывает элемент <Window> в XAML-файле с определением частичного класса в файле отделенного кода, который в данном случае называется Ch34Ex01 .Window. Это напоминает способ, которым в ASP.NET используется класс для страницы, и позволяет файлу отделенного кода иметь точно такую же модель кода, как и у XAML-файла, вместе со всеми содержащимися в нем внутри XAML-элементов определениями элементов управления и т.д. Обратите внимание на то, что атрибут х: Class может применяться только на уровне корневого элемента в XAML-файле. Три других атрибута— Title, Height и Width — задают текст, отображаемый в заголовке окна, и размеры (в пикселях), которые должны использоваться для визуализации окна. Эти атрибуты отображаются на свойства класса System.Windows .Window, производным от которого и является класс Ch34Ex01 .Window. У класса System.Windows .Window есть еще несколько других свойств, которые позволяют определять дополнительные детали. Многие из них сложнее тех трех, что были перечислены выше — т.е., например, они не имеют вид простых строк или чисел. Синтаксис XAML позволяет использовать вложенные элементы для указания значения для этих свойств. Синтаксис, который в XAML нужно использовать для определения объектов, свойств и содержимого, более подробно будет описан позже в этой главе, в разделе ''Синтаксис ХАМЕ.
1086 Часть V. Дополнительные технологии Например, свойство Background определяется в рассматриваемом коде с помощью вложенного элемента <Window.Background> следующим образом: <Window.Background> <LinearGradientBrush EndPoint=.5,1" StartPoint=.5, 0"> <GradientStop Color="#FFFFFFFF" Offset=" /> <GradientStop Color=M#FFFFC45A" Offset="l" /> </LinearGradientBrush> </Window.Background> В этом коде свойство Background устанавливается в экземпляр класса Linear GradientBrush. В WPF кисти применяются почти так же, как в технологии GDI+, о которой рассказывалось в главе 33. В данном случае для кисти задается градиент с переходом от белого цвета к персиковому в направлении сверху вниз. Есть еще два других "сложных" свойства, которые определяются в этом коде во вложенных элементах: а именно— в элементе <Window.Resources>, где задаются анимационные эффекты, и элементе <Window.Triggers>, где задаются триггеры, отвечающие за управление этими анимационными эффектами. Оба этих свойства — Resource и Triggers — способны на гораздо большее, чем они делают здесь, и потому позже в этой главе будут рассматриваться более подробно. Прежде чем переходить к анализу реализации этих свойств, не помешает забежать наперед и взглянуть на элемент <Grid>. В нем определяется экземпляр элемента управления System. Windows. Controls. Grid, который является одним из тех нескольких элементов управления, которые можно использовать для компоновки интерфейса в WPF-приложении, и позволяет размещать вложенные элементы управления путем задания координат относительно любого из четырех углов прямоугольника. Другие элементы управления позволяют делать это другими способами. Обо всех элементах управления, которые можно использовать для компоновки, более подробно будет рассказываться позже в этой главе, в разделе "Компоновка элементов управления". В анализируемом коде элемент <Grid> содержит пять элементов <Ellipse> (представляющих экземпляры элементов управления System.Windows.Shapes.Ellipse) и три элемента <Button> (представляющих экземпляры элементов System.Windows. Controls .Button). В них определяются, соответственно, эллипсы, которые должны использоваться для отображения вращающихся графических объектов в приложении, и кнопки, которые должны предоставляться для управления приложением. Первый элемент <Ellipse> выглядит следующим образом: <Ellipse Margin=0,50,0,0" Name="ellipse5" Stroke="Black" Height=50" HorizontalAlignment="Left" VerticalAlignment="Top" Width=50"> <Ellipse.BitmapEffect> <BlurBitmapEffect Radius=0" /> </Ellipse.BitmapEffect> <Ellipse.Fill> <RadialGradientBrush> <GradientStop Color=M#FF000000" 0ffset="l" /> <GradientStop Color="#FFFFFFFF" Offset=.306" /> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> Здесь видно, что в нем определяется экземпляр класса System.Windows .Shapes. Ellipse, применяемый для отображения фигуры-эллипса, и устанавливаются значения для нескольких свойств этого экземпляра, которые перечислены ниже. □ Name. Идентификатор, который должен использоваться для данного элемента управления.
Глава 34. Windows Presentation Foundation 1087 □ Margin. Задает месторасположение определенной в элементе управления Ellipse фигуры внутри содержащей его сетки путем указания полей вокруг фигуры. Эти поля предоставляются в рассматриваемом коде в пикселях. То, каким именно образом данное свойство будет отображаться на фактическое месторасположение фигуры, зависит от значений свойств HorizontalAlignment и VerticalAlignment. □ HorizontalAlignment и VerticalAlignment. Используются для указания того, какие из краев определенного в Grid прямоугольника должны применяться для размещения фигуры. Например, значения Left и Bottom будут приводить к размещению фигуры относительно левого нижнего края сетки. □ Height и Width. Размеры фигуры. □ Stroke. Кисть, которая должна использоваться для прорисовывания контура определенной в Ellipse фигуры. □ BitmapEf f ect. Специальный эффект, который должен использоваться при отображении элемента управления Ellipse. Для свойства Fill устанавливается кисть RadialGradientBrush. Подобные кисти уже встречались в коде GDI+. В данном случае кисть подразумевает использование градиентной заливки с переходом от белого (почти из центра эллипса) к черному (по краям). Для свойства BitmapEf feet устанавливается эффект BlurBitmapEf f ect. Он является одним из нескольких эффектов, которые разрешено применять к графическим объектам в WPF, и подразумевает размытие фигуры до той степени, которая указана в свойстве BlurBitmapEf feet .Radius. Для Web-приложений этот эффект не доступен, из-за чего и был удален на шаге 11. Это и есть одно из тех нескольких отличий между настольными приложениями и Web-приложениями. Четыре остальных элемента <Ellipse> в коде выглядят очень похоже. В каждом из них определяется один из четырех цветных анимируемых эллипсов. Первый выглядит следующим образом: <Ellipse Margin=5,85,0,0" Name="ellipsel" Stroke="{x:Null}" Height="80M HorizontalAlignment="Left" VerticalAlignment="Top" Width=20" Fill=MRed" Opacity=M0.5" RenderTransformOrigin=.92,0.5"> <Ellipse.BitmapEffect> <BevelBitmapEffect /> </Ellipse.BitmapEffect> <Ellipse.RenderTransform> <TransformGroup> <RotateTransform Angle="/> </TransformGroup> </Ellipse.RenderTransform> </Ellipse> Этот код выглядит почти так же, как и код предыдущего эллипса, но имеет отличия, которые перечислены ниже. □ Для свойства Stroke устанавливается значение {х: null}. В XAML значения, заключенные в фигурные скобки подобно этому, называются расширениями разметки (markup extensions) и используются для предоставления значений для таких свойств, значения которых не могут сокращаться в XAML-синтаксисе до простых строк. В данном случае {x:null} предоставляет для свойства Stroke значение null, означающее, что для него не должна использоваться никакая кисть. □ Задается свойство Opacity со значением 0.5. Это указывает, что данный эллипс должен быть полупрозрачным.
1088 Часть V. Дополнительные технологии □ В свойстве BitmapEf f ect указывается использовать эффект BevelBitmapEf f ect, который и приводит к получению таких скошенных краев, как были показаны на рис. 34.5. □ Задается свойство RenderTransform. В качестве значения для него устанавливается объект TransformGroup с одной единственной трансформацией — RotateTransform. Эта трансформация должна выполняться при анимировании эллипса и имеет одно единственное заданное свойство — Angle. Это свойство представляет угол вращения эллипса (в градусах) и первоначально устанавливается в 0. □ Используется свойство RenderTransformOrigin для установки центральной точки, вокруг которой должно осуществляться вращение эллипса во время трансформации RotateTransform. Два последних свойства имеют отношение к определяемой далее в XAML-коде анимации, за которую отвечает объект System.Windows .Media.Animation. Storyboard. Этот объект определяется в рамках элемента <Window.Resources>, а это означает, что он будет доступен через принадлежащую окну коллекцию Resources. Еще в этом коде определяется атрибут х:Кеу, позволяющий ссылаться на объект Storyboard через коллекцию Resources с помощью ключа: <Window.Resources> <Storyboard x:Key="Spin"> </Storyboard> </Window.Resources> Объект Storyboard содержит четыре объекта DoubleAnimationUsingKeyFrames. Эти объекты позволяют указывать, что свойство, содержащее значение double, должно со временем изменяться, вместе с деталями, конкретизирующими это поведение еще дальше. В данном коде в каждом элементе определяется анимационный эффект, который должен использоваться для одного из цветных эллипсов. Например, для рассматривавшегося выше эллипса ellipsel этот анимационный эффект выглядит так: <DoubleAnimationUsingKeyFrames BeginTime=0:00:00" Storyboard.TargetName="ellipsel" Storyboard.TargetProperty= "(UIElement.RenderTransform).(TransformGroup.Children) [0]. (RotateTransform.Angle) " RepeatBehavior="Forever"> <SplineDoubleKeyFrame KeyTime=0:00:10" Value=60" /> </DoubleAnimationUsingKeyFrames> В общем, здесь просто указывается, что первоначальное значение свойства Angle, принадлежащего описанной ранее трансформации RotateTransform, должно изменяться на значение 360 через 10 секунд и что по завершении это изменение должно повторяться снова. Об анимационных эффектах более подробно речь пойдет позже в этой главе, в разделе "Анимация". После определений эллипсов идет три элемента <Button>, в которых определяются кнопки (обратите внимание на то, что атрибут Click не был показан в коде примера; его добавила IDE-среда после выполнения вами в шаге 3 двойного щелчка на кнопке): <Button Height=3" HorizontalAlignment="Left" Margin=0,0,0,56" Name="goButton" VerticalAlignment="Bottom" Width=5" Content="Go" /> <Button Height=3" HorizontalAlignment="Left" Margin=52,0,0,56" Name="stopButton" VerticalAlignment="Bottom" Width=5" Content="Stop" /> <Button Height=3" HorizontalAlignment="Left" Margin="85,0,86,16" Name="toggleButton" VerticalAlignment="Bottom" Width=5" Content="Toggle" Click="toggleButton_Click" />
Глава 34. Windows Presentation Foundation 1089 В каждом из этих элементов задается имя, позиция и размеры объекта Button с помощью тех же самых свойств, что и в описанных выше элементах <Ellipse>. Еще в них используется свойство Content, отвечающее за то, что должно отображаться в виде содержимого кнопки, и в данном случае — строка текста. На кнопках, однако, можно отображать не только простой текст, при желании на них можно отображать и вложенные фигуры или другое графическое содержимое. Более подробно об этом будет рассказываться позже в этой главе, в разделе "Стилизация элементов управления". В атрибуте Click кнопки toggleButton определяется метод обработчика событий Click. Этот метод выглядит как toggleButtonClick () и фактически представляет собой обработчик маршрутизируемых событий (routed events). Маршрутизируемые события более подробно рассматриваются чуть позже в этой главе, в разделе "Маршрутизируемые события". Пока достаточно знать только то, что такие события срабатывают при выполнении щелчка на кнопке и затем приводят к вызову соответствующего обработчика событий. В коде обработчика событий первым делом получается ссылка на объект Storyboard, содержащий необходимый анимационный эффект. Ранее уже показывалось, что этот объект содержится в свойстве Resources объекта-контейнера Window и использует ключ Spin. Поэтому код, применяемый для его извлечения, не должен вызывать никаких вопросов: private void toggleButton_Click(object sender, RoutedEventArgs e) { Storyboard spinStoryboard = Resources["Spin"] as Storyboard; После получения объекта Storyboard, если только предыдущий код не возвращает значение null, используется метод Storyboard.GetlsPausedO для определения того, находится анимационный эффект в текущий момент в приостановленном состоянии или нет. Если да, тогда вызывается метод Resume (), а если нет — метод Pause (). Эти методы, соответственно, либо возобновляют, либо приостанавливают воспроизведение анимационного эффекта: if (spinStoryboard != null) { if (spinStoryboard.GetlsPaused (this)) { spinStoryboard.Resume(this); } else { spinStoryboard.Pause (this); } } } Обратите внимание на то, что всем этим методам требуется ссылка на объект, в котором содержится объект Storyboard. Объясняется это тем, что объекты Storyboard сами за временем не следят. Вместо этого они используют часы того окна, в котором содержатся. Передача ссылки на это окно (с помощью this) как раз и позволяет им получать доступ к этим часам. Две остальных кнопки — goButton и stopButton — ни с какими методами обработчика событий в файле отделенного кода не связаны. Вместо этого за их функционирование отвечают триггеры. В данном примере этих триггеров определяется целых три, как показано ниже:
1090 Часть V. Дополнительные технологии <Window.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard Storyboard="{StaticResource Spin}" x:Name="Spin_BeginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="goButton"> <ResumeStoryboard BeginstoryboardName="Spin_BeginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="stopButton"> <Pausestoryboard BeginstoryboardName="Spin_BeginStoryboard" /> </EventTrigger> </Window.Triggers> Первым идет триггер, который связывает событие FrameworkElement. Loaded (срабатывающее при загрузке приложения) с действием BeginStoryboard. Это действие запускает воспроизведение анимационного эффекта Spin. Обратите внимание, что для ссылки на анимационный эффект Spin используется синтаксис расширения разметки с кодом {StaticResource Spin}. Такой синтаксис очень часто применяется в WPF-приложениях для ссылки на ресурсы, содержащиеся внутри окна-контейнера. Действию BeginStoryboard присваивается имя SpinBeginStoryboard, которое применяется далее в виде ссылки на него в двух других триггерах. Эти триггеры связывают события Click кнопок goButton и stopButton, соответственно, с действиями ResumeStoryboard и PauseStoryboard, выполняющими именно те операции, на которые указывают их названия. Этот код прекрасно работает в настольном приложении, но при преобразовании его в Web-приложение требует внесения нескольких изменений. На самом деле в примере некоторые из этих деталей были скрыты в результате создания нового WPF-при- ложения типа Browser (Браузер). Например, из-за существования ряда связанных с безопасностью ограничений при выполнении кода в браузере WPF-приложение типа Browser оснащается временным ключом, который можно использовать для подписания приложения. Подобное требуется при желании разрешить приложению выполнять такие действия, выполнение которых в браузерных приложениях в противном случае запрещено, как, например, получение доступа к локальной файловой системе. Также не трудно было заметить, что корневой элемент настольного приложения <Window> в Web-приложении заменяется элементом <Раде>. Это связано с тем, что возможности, предоставляемые браузером, немного отличаются от возможностей, предоставляемых приложением-хостом, которое используется для запуска настольных приложений. Поэтому в WPF для представления этих разных хостов и применяются разные классы. Однако, как было видно в коде примера, для многих вещей в этих разных средах все равно допускается использовать идентичный код. Более подробно об этом будет рассказываться позже в этой главе. На этом анализ данного примера приложения завершен. Здесь было охвачено много основных моментов и вкратце рассмотрено очень много новых концепций, поэтому, пожалуй, не помешает перед продолжением немного "перевести дух". В остальной части настоящей главы будут более детально описаны показанные здесь приемы, требуемый синтаксис и некоторые другие вещи. Основные понятия WPF Надеемся, что примером, приведенным в первой части этой главы, нам удалось вызвать у вас интерес к программированию WPF-приложений. Несмотря на необходимость изучения множества новых понятий, этот пример показал, как создавать динамические приложения очень быстро, комбинируя код XAML и .NET, а также как при
Глава 34. Windows Presentation Foundation 1091 желании оставлять разработку многих компонентов, вроде пользовательского интерфейса приложений, дизайнерам, которым не требуется обладать навыками программирования на С#. Еще пример проиллюстрировал то, как можно создавать настольные и Web-приложения с помощью (более или менее) одинакового кода. Однако прежде чем начинать создавать WPF-приложения самостоятельно, необходимо потратить еще немного времени на изучение основ. В этом разделе освещено несколько тем, которые являются фундаментальными для WPF-приложений, и описан синтаксис, который необходим для их реализации. По ходу здесь также охватывается много дополнительных возможностей, изучить которые более детально можно будет в своих собственных приложениях. В частности, в настоящем разделе рассматриваются следующие вопросы. □ Синтаксис XAML. □ Настольные и Web-приложения. □ Объект Application. □ Основные понятия, связанные с элементами управления — свойства зависимостей, подключаемые свойства, маршрутизируемые события и подключаемые события. □ Компоновка и стилизации элементов управления. □ Триггеры. □ Анимация. □ Статические и динамические ресурсы. Синтаксис XAML В примере в первой части этой главы было представлено много XAML-синтаксиса без всяких формальных объяснений. Описание многих правил и возможностей было опущено для того, чтобы сфокусировать внимание на структуре и функциональных возможностях в общем. В настоящем разделе синтаксис XAML будет рассмотрен немного более подробно, чтобы прояснить, каким образом собираются XAML-файлы. Синтаксис объектных элементов В базовой структуре XAML-файла используется синтаксис объектных элементов для описания иерархии объектов с одним корневым объектом, содержащим все остальные. С помощью такого синтаксиса, как становится понятно из его названия, описывается представляемый XML-элементом объект (или структура). Например, в предыдущем примере вы видели, как для представления объекта System. Windows . Controls . Button использовался элемент <Button>. В корневом элементе XAML-файла всегда применяется синтаксис объектных элементов, хотя, как было видно в предыдущем примере, класс, используемый для корневого объекта, задается не с помощью имени элемента (<Window> или <Раде>), а с помощью атрибута х: Class. Такой синтаксис применяется только в корневом элементе. В случае настольных приложений, корневой элемент должен обязательно наследоваться от System.Windows .Window, а в случае Web-приложений — от System.Windows. Controls.Page. Многие из объектов, которые определяются с помощью синтаксиса объектных элементов, на самом деле являются элементами управления, вроде Button, который использовался в предыдущем примере.
1092 Часть V. Дополнительные технологии Синтаксис атрибутов Нетрудно было заметить, что во многих случаях при использовании элемента для представления объекта (с помощью синтаксиса объектных элементов) также применяются и атрибуты для указания свойств и событий. Например, в приведенном ранее элементе <Button> использовались такие атрибуты: <Button Height=M23" HorizontalAlignment="LeftM Margin="85,0,86,16" Name="toggleButton" VerticalAlignment="Bottom" Width=5" Content="Toggle" Click="toggleButton_Click" /> Здесь все атрибуты, кроме атрибута Click, который назначает объекту toggleButton обработчик маршрутизируемых событий для обработки его событий Click, и атрибута Name, задают значение для соответствующего свойства объекта toggleButton и являются примером синтаксиса атрибутов. Атрибут Name здесь представляет собой особый случай и задает для элемента управления идентификатор, чтобы на него можно было ссылаться из отделенного и другого XAML-кода. Имена атрибутов можно уточнять путем добавления перед ними имени того базового класса, на который они ссылаются, и точки. Например, элемент управления Button наследует свое событие Click от ButtonBase, а это значит, что предыдущий код можно было бы записать и следующим образом: <Button Height=3" HorizontalAlignment="Left" Margin="85/ 0, 86,16" Name="toggleButton" VerticalAlignment="Bottom" Width=5" Content="Toggle" ButtonBase.Click="toggleButton_Click" /> Обратите внимание, что точно такой же синтаксис применяется и для подключаемых свойств, о которых более подробно будет рассказываться позже в этой главе, в разделе "Основные сведения об элементах управления". Синтаксис элементов свойств Во многих случаях для инициализации значения свойства требуется использовать нечто большее, чем простую строку. В примере приложения так было со свойствами Fill, которые устанавливались в различные объекты кисти: <Ellipse ...> <Ellipse.Fill> <RadialGradientBrush> <GradientStop Color="#FF000000" 0ffset="l" /> <GradientStop Color="#FFFFFFFF" Offset=.306" /> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> Здесь значение для свойства устанавливается с помощью дочернего элемента, имя которого соответствует следующему соглашению: [Имя родительского элемента] . [Имя свойства] Этот синтаксис и называется синтаксисом элементов свойств. Синтаксис содержимого Многие элементы управления часто представляют то или иное содержимое, которое затем отображается на экране в соответствии с их шаблоном. Например, содержимое, предоставляемое для элемента управления Button, отображается на поверхности
Глава 34. Windows Presentation Foundation 1093 кнопки. Это может быть как обычный текст, как это и было в примере, так и какой-то графический объект. Синтаксис содержимого позволяет указывать содержимое для элемента управления следующим образом: <Button ...>Go</Button> Этот код предназначен для элемента управления Button и указывает, что тот при визуализации должен отображать на своей поверхности текст Go. В приведенном в качестве примера приложении для этого применялся более простой, хотя и менее гибкий, способ, а именно— атрибут Content. Полную версию синтаксиса содержимого, вроде той, что показана здесь, необходимо использовать только для представления более сложного содержимого, например, для представления графических объектов, о которых упоминалось во вводной части этого раздела. Применение сложного содержимого для элемента управления может приводить к образованию сложных вложенных иерархий XAML-кода. Из-за этого XAML позволяет осуществлять стилизацию элементов управления с помощью других, более усовершенствованных средств. Больше о представлении содержимого и стилизации элементов управления будет рассказываться позже в этой главе, в разделе "Стилизация элементов управления". Смешивание синтаксиса элементов свойств и синтаксиса содержимого Любой элемент управления на XAML-странице, который форматируется с использованием синтаксиса элементов объектов, может включать и синтаксис элементов свойств, и синтаксис содержимого. В тех случаях, когда дело обстоит именно так, необходимо помнить о следующих синтаксических правилах. □ Элементы, используемые для синтаксиса элементов свойств, вовсе необязательно должны следовать строго друг за другом, т.е. допускается, чтобы за элементом, в котором используется синтаксис свойств, шел элемент, в котором применяется синтаксис содержимого, а за ним — снова элемент, в котором используется синтаксис свойств. □ Элементы (и, если возможно, текстовое содержимое), используемые для синтаксиса содержимого, должны строго следовать друг за другом, т.е. не допускается, чтобы за текстом или элементом, в котором применяется синтаксис содержимого, следовал элемент, в котором используется синтаксис свойств, а за ним — снова элемент, в котором используется синтаксис содержимого или текст. Следовательно, этот код является допустимым: <Button . . .> <Button.BitmapEffect> <OuterGlowBitmapEffect GlowColor="Blue" GlowSize=0" /> </Button.BitmapEffect> Go <Button.RenderTransform> <RotateTransform Angle=0" /> </Button.RenderTransform> </Button> Приведенный ниже фрагмент кода, однако, работать не будет, потому что в нем присутствуют два места, где используется синтаксис содержимого, разделяемые элементом, в котором используется синтаксис элементов свойств:
1094 Часть V. Дополнительные технологии <Button ...> Don't <Button.BitmapEffect> <OuterGlowBitmapEffect GlowColor="Red" GlowSize=0" /> </Button.BitmapEffect> Go </Button> Расширения разметки Пример показал, как расширения разметки могут также использоваться для значений свойств— например, значения {x:null}. Везде, где встречаются фигурные скобки ({}), применяются расширения разметки (markup extension). Они могут применяться как в коде с синтаксисом атрибутов, так и в коде с синтаксисом элементов свойств. Все расширения разметки в этой главе относятся конкретно к WPF. Расширения WPF применяются для ссылки на ресурсы и для привязки данных. Настольные приложения и Web-приложения Приведенный ранее в главе пример продемонстрировал, что WPF-приложение может работать и как автономное настольное приложение, и как Web-приложение. В этом примере мы использовали шаблоны проектов WPF Application и WPF Browser Application в качестве отправной точки, а для завершения приложения просто добавили в них соответствующий код XAML и С#. Компиляция шаблона проекта WPF Application привела к получению файла . ехе, а проекта WPF Browser Application — к получению файла . xbap. ХВАР— это акроним от XAML Browser Application (Браузерное приложение на языке XAML), и из-за него Web-приложения, создаваемые с помощью WPF, часто называют ХВАР-приложе- ниями. Большая часть отличий между приложениями этих типов состоит в разнице между файлами проектов (.csproj ). У приложений WPF Browser здесь имеется несколько дополнительных определяемых параметров, в том числе параметров для подписывания, как приложения, так и его манифеста (из упоминавшихся ранее соображений безопасности), и параметров для включения отладки в браузере. Еще есть, как отмечалось ранее, тестовый сертификат, который можно применять для подписывания. В производственной среде этот сертификат должен быть заменен сертификатом своего центра сертификации. Преобразование настольного приложения WPF в Web-приложение WPF может оказаться делом кропотливым, поскольку требует изменения немалого количества параметров, а также (как показывалось в примере) внесения определенных изменений в XAML-код. В число этих изменений входит замена элементов <Window> элементами <Раде> и удаление функциональных возможностей, вроде растровых эффектов. Обычно наилучшим подходом является создание отдельных проектов, как это и было сделано в предыдущем примере. Однако существуют и альтернативные варианты. Пожалуй, наилучшее решение пока что было реализовано Карэн Корби (Karen Corby) и доступно в ее сетевом дневнике по адресу http://scorbs.com/2006/06/04/ vs-template-flexible-application/. Карэн создала гибкий шаблон приложения, который можно использовать вместо стандартных шаблонов WPF Application и WPF Browser Application. Этот шаблон позволяет переключаться между настольной и Web- версией приложения с помощью специального раскрывающегося списка конфигураций в IDE-среде. На него действительно стоит взглянуть тем, кто планирует часто переключаться между типами приложений, хотя на практике разработчики все же предпочитают выбирать только какую-то одну среду и работать в ней.
Глава 34. Windows Presentation Foundation 1095 На рис. 34.8 показано, как с помощью гибкого шаблона Карэн можно переключиться на вывод ХВАР. - Debug Release ГПЯШЕПМ ХВАР Release Configuration Manager.., Рис. 34.8. Переключение в режим вывода ХВАР Единственное, что придется делать вручную, так это отключать те функциональные возможности, которые не доступны в ХВАР-приложениях, работающих в среде с уровнем частичного доверия. Например, в таких приложениях нельзя применять растровые эффекты. Полный перечень функциональных возможностей, которые допускается и не допускается использовать в среде с частичным уровнем доверия, можно найти в документации MSDN под заголовком Windows Presentation Foundation Partial Trust Security (Обеспечение безопасности для сред с уровнем частичного доверия в WPF). Объект приложения В WPF в состав большинства приложений (включая все ХВАР-приложения, а также настольные приложения на базе шаблона WPF Application) входит экземпляр класса, унаследованного от System.Windows .Application. В примере приложения, которое демонстрировалось ранее, за определение этого объекта отвечали файлы Арр. xaml и App.xaml.cs. В частности, файл Арр.xaml приложения Сп34Ех01 выглядел так: Application x:Class="Ch34Ex01.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns :x="http: //schemas .microsoft.com/winfx/2006/xaml11 StartupUri="Windowl.xaml"> Application.Resources> </Application.Resources> </Application> Синтаксис элемента <Application> похож на синтаксис рассмотренного ранее элемента <Window> и подобно ему предусматривает использование атрибута х:Class для связывания кода с определением частичного класса в файле отделенного кода. Определяемый в этом коде объект является входной точкой WPF-приложения. У этого объекта может существовать только один экземпляр, доступ к которому может производиться только через статическое свойство Application.Current. Объект приложения является чрезвычайно полезным по следующим причинам. □ Он представляет многочисленные события, генерируемые на определенных этапах жизненного цикла приложения. К ним относится событие LoadCompleted, которое уже показывалось ранее и которое возбуждается при загрузке и визуализации приложения, событие DispatcherUnhandledException, которое происходит при выдаче необработанного исключения, и многие другие. □ Он содержит методы, которые можно использовать для установки или загрузки cookie-наборов, обнаружения и загрузки ресурсов и т.д. □ Он обладает несколькими свойствами, которые можно применять для получения доступа к, например, ресурсам, находящимся в области действия приложения (о которых более подробно будет рассказываться чуть позже в этой главе, в разделе "Статические и динамические ресурсы"), и окнам в приложении.
1096 Часть V. Дополнительные технологии События, генерируемые объектом приложения, пожалуй, являются наиболее полезной функциональной возможностью в этом списке и, скорее всего, ими придется пользоваться наиболее часто. Основные сведения об элементах управления В WPF предоставляется широкий спектр элементов управления, которые можно использовать для создания приложений. В настоящей главе дается общий обзор технологии WPF и того, что с ее помощью можно делать, поэтому вместо рассмотрения каждого из элементов управления WPF по отдельности мы будем изучать их в действии. Ниже приведен общий список всех элементов управления, которые предлагаются bWPF: Border BuiletDecorator Button CheckBox ComboBox ContextMenu DocumentViewer Expander FlowDocumentPageViewer FlowDocumentReader FlowDocumentScrollViewer л Frame GroupBox Image Label ListBox ListView Menu PasswordBox Popup ProgressBar PrintDialog RadioButton RepeatButton RichTextBox ScrollBar ScrollViewer Separator Slider StatusBar TabControl TextBlock TextBox ToolBar ToolTip TreeView Viewbox В этот перечень не вошли элементы управления WPFy применяемые для компоновки, потому что о них будет отдельно рассказываться позже в этой главе. Некоторые из перечисленных здесь элементов управления имеют очень знакомые имена и на самом деле делают практически то же самое, что и их аналоги в приложениях Windows Forms и ASP.NET. Например, уже показывалось, как элемент управления Button может использоваться для визуализации кнопки. Остальные элементы управления выглядят менее знакомо и потому, чтобы понять, на что они способны, не помешает с ними поэкспериментировать. Эти элементы управления первоначально имеют очень простой внешний вид. Для извлечения из них максимальной пользы, их нужно подвергать стилизации, где, пожалуй, и проявляется истинная мощь WPF, как можно будет увидеть позже в этой главе. Помимо стилизации, существует еще несколько других средств, которые можно применять для элементов управления WPF. В этом разделе будет рассказано о слудеющим из них: □ свойства зависимостей; □ подключаемые свойства; □ маршрутизируемые события. Как и при разработке других настольных и Web-приложений, допускается создавать свои собственные элементы управления. При создании собственного элемента управления можно пользоваться всеми описываемыми здесь средствами. В последующих разделах можно будет увидеть примеры их реализации.
Глава 34. Windows Presentation Foundation 1097 Свойства зависимостей Свойство зависимости (dependency property) — это свойство особого типа, которое используется в WPF повсюду, особенно в элементах управления, и предоставляет функциональность, расширяющую возможности обычных свойств .NET. Чтобы проиллюстрировать это, рассмотрим обычное свойство .NET. При создании классов в .NET свойства, как правило, реализуются с использованием очень простого кода, подобного показанному ниже: private string aStringProperty; public string AStringProperty { get { return aStringProperty; } set { aStringProperty = value; } } Здесь реализуется общедоступное свойство, основанное на приватном поле. Такая простая реализация прекрасно подходит для большинства целей, но не включает никакое особой функциональности, кроме базовой возможности получения доступа к состоянию. Например, при желании добавить свойство AStringProperty в один элемент управления (ControlA) и сделать так, чтобы другой элемент управления (ControlB) реагировал на изменения в этом свойстве, потребовалось бы выполнить следующие шаги. 1. Добавить свойство AStringProperty в элемент управления ControlA с помощью показанного выше кода. 2. Добавить в элемент управления ControlA событие. 3. Добавить в элемент управления ControlA метод для генерации этого события. 4. Добавить код для установки функции set свойства AStringProperty в ControlA таким образом, чтобы она вызывала этот генерирующий событие метод. 5. Добавить в элемент управления ControlB код для подписки на событие, предоставляемое элементом управления ControlA. Рассматривать отдельно требуемый для выполнения всех этих шагов код нет никакой необходимости, поскольку он уже несколько раз встречался ранее в книге. Проблема такого подхода состоит в том, что не существует никаких определенных стандартов, которым можно было бы следовать для достижения этого результата. Разные разработчики могут добавить код, приводящий к получению такого же результата, разными способами. Более того, он требует определять все свойства, об изменениях в которых может понадобиться получать уведомления, во время разработки элементов управления. Для устранения этой проблемы в WPF как раз и предлагается заменять определение простого свойства, вроде того, что использовалось в предыдущем коде, свойством зависимости и затем использовать формализованные и структурированные приемы для предоставления уведомлений обо всех происходящих в нем изменениях. Свойство зависимости представляет собой свойство, которое регистрируется в системе свойств WPF как предусматривающее расширенную функциональность. Эта расширенная
1098 Часть V. Дополнительные технологии функциональность включает возможность автоматической отправки уведомлений об изменении свойства (и не только). В частности, свойства зависимостей характеризуются следующими функциональными возможностями. □ Разработчик может использовать стили для изменения значений свойств зависимостей. □ Разработчик может устанавливать значение свойства зависимости за счет использования ресурсов или привязки данных. □ Разработчик может изменять значения свойств зависимостей в анимации. □ Разработчик может устанавливать свойства зависимостей иерархическим образом в XAML — т.е. значение, которое устанавливается для свойства зависимости в родительском элементе, может использоваться для установки значения по умолчанию для такого же свойства зависимости в его дочерних элементах. □ Разработчик может конфигурировать уведомления об изменениях в значениях свойств зависимостей путем применения четко определенной схемы кодирования. □ Разработчик может конфигурировать целые наборы взаимосвязанных свойств для того, чтобы они все обновлялись в ответ на изменение в любом из них. Этот процесс называется приведением (coercion), потому что измененное свойство приводит значения остальных свойств. □ Разработчик может применять к свойству зависимости метаданные для указания других связанных с поведением характеристик. Например, он может указывать таким образом, что изменение конкретного свойства чревато возникновением необходимости в перестройке пользовательского интерфейса. На практике из-за способа, которым реализуются свойства зависимостей, на первый взгляд можно и не заметить особых отличий от обычных свойств. Однако при создании своих собственных элементов управления сразу же станет заметно, что в случае использования обычных свойств .NET многие функциональные возможности вдруг куда-то исчезают. Из-за того, что свойства зависимостей играют превалирующую роль в WPF, чуть позже в этой главе будет показано, как именно их реализовать. Подключаемые свойства Подключаемое свойство (attached property) — это свойство, которое делается доступным для каждого дочернего объекта в экземпляре того класса, который его определяет. Например, представим, что у нас есть класс под названием Recipe (Рецепт) с дочерними объектами, представляющими различные ингредиенты. Тогда мы могли определить в нем подключаемое свойство и затем использовать его в каждом дочернем объекте. Важно обратить внимание на то, что указывать значения для подключаемых свойств в дочерних объектах не нужно. Главная причина, по которой могло бы возникнуть желание сделать подобное, состоит в том, что XAML-код, который нужно использовать для установки значений для подключаемых свойств, является очень простым для понимания: <Recipe Name="Simple Vegetable Chili"> <TinOfKidneyBeans Recipe.Quantity=" Mashed="true" /> <TinOfChoppedTomatoes Recipe.Quantity=" /> <FreshChili Recipe.Quantity=" Notes="Chopped fine, vary to taste." /> «Dnion Recipe.Quantity="l" Notes="Chopped and fried in olive oil." /> <LBVPort Notes="Just a dash." /> </Recipe>
Глава 34. Windows Presentation Foundation 1099 Показанный здесь синтаксис представляет собой разновидность демонстрировавшегося ранее синтаксиса атрибутов. Для ссылки на подключаемое свойство используется имя родительского элемента, точка и имя самого присоединенного свойства. Подключаемые свойства имеют в WPF несколько разных предназначений. Чуть позже, при рассмотрении того, как можно размещать элементы управления, в разделе "Компоновка элементов управления" будет показано много подключаемых свойств и то, как они могут определяться в элементах управления, выполняющих роль контейнера, для предоставления возможности дочерним элементам определять, к примеру, то, к каким краям должен пристыковываться контейнер. Маршрутизируемые события Из-за своей иерархической природы WPF-приложения часто состоят из элементов управления, содержащих другие элементы управления, которые, в свою очередь содержат еще какие-то элементы управления, и т.д. Маршрутизируемое событие представляет собой механизм, позволяющий делать так, чтобы событие, влияющее на один элемент управления в иерархии, также влияло и на другие элементы в этой же иерархии и при этом не писать никакого сложного кода. Замечательным примером может служить ситуация, когда пользователям предоставляется возможность взаимодействовать с приложениями с помощью мыши, что, несомненно, случается очень часто. При выполнении пользователем щелчка на кнопке в приложении обычно нужно, чтобы приложение как-то реагировало на событие щелчка. Одним из возможных вариантов для обеспечения такого поведения, который уже знаком по разработке приложений Windows Forms и ASP.NET, является добавление обработчика для предоставляемого кнопкой события и указания в нем тех действий, которые должны выполняться в ответ на щелчок. Хотя на первый взгляд этого не видно, но данный подход на самом деле прилично ограничивает возможности разработчика и может приводить к получению, например, в некоторых приложениях Windows Forms, довольно запутанного кода. Объясняется это необходимостью в существовании некоего механизма для определения того, какой элемент управления должен реагировать на щелчок кнопкой мыши генерацией своего события щелчка, а этот элемент управления может и не быть очевиден сразу. Например, если взять приведенный здесь простой пример, то какой элемент управления должен генерировать событие щелчка — сама кнопка или окно (т.е. элемент Window), в котором она содержится? При наличии обработчиков событий и у кнопки, и у элемента Window и необходимости, чтобы событие генерировал только какой-то один из них, пожалуй, будет лучше выбрать кнопку. Однако как же быть, если требуется, чтобы событие генерировали они оба, да еще и в определенном порядке? В случае приложений Windows Forms для получения такого поведения, скорее всего, придется писать специальный (и вероятно довольно сложный) код. В WPF событие щелчка кнопкой мыши реализуется для элементов управления, Button и Window включительно, в виде маршрутизируемого события, что полностью исключает упомянутую проблему. Маршрутизируемые события генерируются всеми объектами в иерархии в определенном порядке, предоставляя разработчику полный контроль над тем, как на них реагировать. Например, рассмотрим элемент управления Window, содержащий элемент управления Grid, который, в свою очередь, содержит элемент управления Rectangle. При выполнении щелчка на элементе управления Rectangle будет происходить следующая последовательность событий. 1. Генерация события нажатия кнопки мыши в Window. 2. Генерация события нажатия кнопки мыши в Grid. 3. Генерация события нажатия кнопки мыши в Rectangle.
1100 Часть V. Дополнительные технологии Возможно, вы ожидаете, что на этом все должно заканчиваться, но это не так, последовательность будет продолжаться и далее будет происходить... 4. Генерация еще одного события нажатия кнопки мыши в Rectangle. 5. Генерация еще одного события нажатия кнопки мыши в Grid. 6. Генерация еще одного события нажатия кнопки мыши в Window. Разработчик может реагировать на событие в любой точке приведенной последовательности, просто добавляя соответствующий метод обработки событий. Еще он также может прерывать эту последовательность в любой точке в рамках обработчика событий, но по умолчанию обработчики сэбытий не предусматривают ее прерывания. Это означает, что разработчик может запускать сразу несколько методов обработки из одного события (в данном случае — одного события нажатия кнопки мыши). Изучая показанную здесь последовательность событий, не помешает познакомиться с некоторыми полезными терминами, которые применяются в WPF в отношении событий. Те события, которые продвигаются вниз по иерархии элементов управления, называются туннельными (tunneling), или нисходящими, а те, которые поднимаются вверх— пузырьковыми (bubbling), или восходящими. Вдобавок, где бы не использовались маршрутизируемые события в WPF, их имена всегда указывают на то, какими они являются — туннельными или пузырьковыми. Имена всех туннельных событий начинаются с префикса Preview. Например, у элемента управления Window есть и событие с именем MouseDown, и событие с именем PreviewMouseDown. Добавлять обработчики можно как только для какого-нибудь одного из них, так и для сразу обоих или, при желании, вообще никакого из них. На рис. 34.9 показана схема, иллюстрирующая предыдущую последовательность событий, а именно — то, какие события будут генерироваться, когда они будут генерироваться и как будет происходить их туннелирование и пузырьковая передача по иерархии элементов. Обработчики маршрутизируемых событий принимают два параметра: источник события и экземпляр объекта RoutedEventArgs или того класса, который наследуется от RoutedEventArgs. Mj Window.PreviewMouseDown Туннелирование rjj Grid.PreviewMouseDown Элемент управления Grid Window.MouseDown (б) Пузырьковая передача Grid.MouseDown (i) Туннелирование (з) Rectangle.PreviewMouseDown Элемент управления Rectangle Пузырьковая передача Rectangle.MouseDown D) Рис. 34.9. Последовательность событий
Глава 34. Windows Presentation Foundation 1101 При реализации обработчика для маршрутизируемого события можно, при желании, устанавливать для свойства Handled объекта RoutedEventArgs значение true. В таком случае не будет происходить никакой дальнейшей обработки, т.е. для этого события больше не будут вызываться какие-либо обработчики событий. Еще RoutedEventArgs предоставляет свойство Source, которое позволяет узнавать, какой элемент управления изначально сгенерировал данное событие. Им будет тот элемент управления, в котором WPF впервые обнаружила это событие, т.е. в сценарии, показанном на рис. 34.9, им будет Rectangle. Это свойство может быть очень полезным, поскольку элементы управления способны определять, на каком из их дочерних элементов был выполнен щелчок (если он был выполнен). Обратите внимание на то, что данный процесс "проверки попадания" является довольно сложным. WPF, например, умеет игнорировать щелчки на прозрачных областях элементов управления без выполнения разработчиком каких-либо действий для включения такой функции. В качестве альтернативного варианта, разработчик может, наоборот, создавать такие прозрачные элементы управления, которые будут реагировать на щелчки кнопкой мыши, т.е. у разработчика имеется довольно приличная степень гибкости. Еще следует отметить, что маршрутизируемые события подходят не только для щелчков кнопкой мыши: они могут применяться для множества различных целей, например, для взаимодействия посредством клавиатуры, привязки данных, таймеров и т.д. Подключаемые события, о которых будет рассказываться позже в этой главе, делают маршрутизируемые события даже более полезными. В следующем практическом занятии реализуется описанная выше ситуация, а также демонстрируются некоторые дополнительные детали, касающиеся маршрутизируемых событий. Практическое занятие РабОТЭ С Маршрутизируемыми СОбыТИЯМИ ?т 1. Создайте новое WPF-приложение по имени Сп34Ех02 и сохраните его в каталоге С:\BegVCSharp\Chapter34. 2. Измените код в его файле Windowl. xaml следующим образом: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Ch34Ex02.Windowl" Title="Routed Events" Height=00" Width="800" MouseDown="Generic_MouseDown" PreviewMouseDown="Generic_MouseDown" MouseUp="Window_MouseUp"> <Grid Name="contentGrid" MouseDown="Generic_MouseDown" PreviewMouseDown="Generic_MouseDown" Background="Azure"> <Rectangle Name="clickMeRectangle" Margin=" 10,10,0,0" Height=3" HorizontalAlignment="Left" VerticalAlignment="Top" Width=0" Stroke="Black" MouseDown= "Generic_MouseDown" PreviewMouseDown= "Generic_MouseDown" Fill="CadetBlue" /> <Button Name="clickMeButton" Margin=,10,10,0" Height=3" HorizontalAlignment="Right" VerticalAlignment="Top" Width=0" MouseDown="Generic_MouseDown" PreviewMouseDown="Generic_MouseDown" Click="clickMeButton_Click">Click Me</Button> <TextBlock Name="outputText" Margin=0,40,10,10" Background^' Corns ilk" /> </Grid> </Window>
1102 Часть V. Дополнительные технологии 3. Измените код в файле Windowl. xaml. cs следующим образом (обратите внимание, что в зависимости о того, какая IDE-среда используется и как вводился код XAML, пустые методы обработчиков событий могут быть добавлены автоматически): public partial class Windowl : Window { private void Generic_MouseDown(object sender, MouseButtonEventArgs e) { outputText.Text = string.Format( "{0}Event {1} raised by control {2}. e.Source={3}\n", outputText.Text, e.RoutedEvent.Name, sender.ToStringO , ((FrameworkElement)e.Source).Name); } private void Window_MouseUp(object sender, MouseButtonEventArgs e) { outputText.Text = string.Format( "{0} An\n", outputText.Text); } private void clickMeButton_Click(object sender, RoutedEventArgs e) { outputText.Text = string.Format( "{0}Button clicked!\n ===\n\n", outputText.Text); ) } 4. Запустите приложение. Затем щелкните один раз на прямоугольнике в верхнем левом углу окна, один раз на светло-голубой области между прямоугольником и кнопкой и один раз на кнопке. На рис. 34.10 показан результат, который должен получиться. « Routed Events [сим»! Event РтШЯШШШЮтт retted by control ChMExOJ Wmdowl t Source-clickMeRec tangle Event PrevtewMouteDown rtrsed by control SyitMn.Wtmtowt.Controto.Grtd. e.Source-ciickMeRectangie Event PrevtewMouteDown raised by control System.Windows Shapes.Rectangle e.Sourca-ctickMeRectangle Event MouseOown retted by control System Windows. Shapes Rectangle e Source-clickMeRectangle Event MouteDown retted by control System Windows Controla.Gnd e Souice-clickMeRectangle Event MouteDown raised by control Ch34Ex02.Wlndow1. a.Source-cltckMeRectengle Event PrevtewMouteDown retted by control ChMExOJ Windowl e.Source-contentGnd Event PrevttwMouteOown retted by control Syetem.windowe.Controte.Grid e.Source-contentGnd Event MouteDown retted by control Syatem.Wtndowt.Controlt.Gnd. a Source-contantGnd Event MouteDown raised by control С h34Ex02. Windowl. e.Source-contentGrtd Event PrevtewMouteDown raited by control С h34Ex02. Windowl. e.Source-clickMeButton Event PrevtewMouteDown raited by control System.Wmdows.Controls Grid. a.Source-cllckMaButton Event PrevtewMouteDown raised by control System Windowt.Controlt.Button: Click Me. e. S Рис. 34.10. Приложение Ch 34Ex 02 в действии Описание полученных результатов Это приложение позволило продемонстрировать обработку маршрутизируемых событий на примере событий MouseDown и PreviewMouseDown, которые предоставляются всеми элементами управления WPF, а еще показать, что происходит при включении кнопки в цепочку событий. XAML-код, который мы использовали, уже выглядел
Глава 34. Windows Presentation Foundation 1103 очень просто, но чтобы проанализировать существенные части (в контексте данного примера), давайте рассмотрим следующий фрагмент кода: <Window x:Class="Ch34Ex02.Windowl" MouseDown="Generic_MouseDown" PreviewMouseDown="Generic_MouseDown" MouseUp="Window_MouseUp"> <Grid Name="contentGrid" MouseDown="Generic_MouseDown" PreviewMouseDown="Generic_MouseDown"> <Rectangle Name="clickMeRectangle" MouseDown="Generic_MouseDown" PreviewMouseDown="Generic_MouseDown" /> <Button Name="clickMeButton" MouseDown="Generic_MouseDown" PreviewMouseDown="Generic_MouseDown" Click="clickMeButton_Click" /> <TextBlock Name="outputText" /> </Grid> </Window> Здесь все свойства, которые не отражаются на функциональных возможностях, были удалены, чтобы сфокусировать внимание лишь на том коде, который имеет отношение к обработке маршрутизируемых событий. Как видно, для событий было сконфигурировано три обработчика, описанные в табл. 34.1. Таблица 34.1. Используемые обработчики событий Обработчик событий Обрабатываемые события Generic_MouseDown() Window.PreviewMouseDown Window.MouseDown Grid.PreviewMouseDown Grid.MouseDown Rectangle.PreviewMouseDown Rectangle.MouseDown Window_MouseUp() Window.MouseUp clickMeButton_Click() Button.Click Методы всех этих обработчиков событий просто выводят информацию в элементе управления TextBlock и тем самым позволяют видеть, что происходит. В выводимый текст входит имя события, имя элемента управления, который его сгенерировал, и имя исходного элемента управления, которым данное событие было сгенерировано впервые, в том виде, в каком оно было получено из RoutedEventArgs. Source. При запуске приложения первый щелчок на элементе управления Rectangle привел к выполнению такой последовательности событий, которая была описана перед практическим занятием. Во время ее выполнения обработчик событий GenericMouseDown () был вызван шесть раз: три раза для туннельных событий PreviewMouseDown и еще три раза для пузырьковых событий MouseDown. В роли источника событий во всех случаях выступил, как и следовало ожидать, тот элемент управления, который прошел проверку попадания, а таковым оказался Rectangle.clickMeRectangle . Обработчик событий Window_MouseUp () тоже был вызван (после всех остальных обработчиков событий) и добавил текст для отделения данной проверки от следующей. Следующий щелчок был выполнен между элементами управления, т.е. фактически это был щелчок на фоновой области элемента управления Grid. На этот раз обработчик событий GenericMouseDown () был вызван четыре раза: два раза для туннельных событий PreviewMouseDown и два раза для пузырьковых событий MouseDown. Источником событий здесь во всех случаях выступил элемент управления Grid.
1104 Часть V. Дополнительные технологии После всех вызовов GenericMouseDown () был опять-таки вызван обработчик событий Window_MouseUp(). Последний щелчок был выполнен на кнопке Button. В этот раз обработчик GenericMouseDown () был вызван только три раза. После этого сработало событие Button. Click, которое привело к вызову clickMeButtonClick (). Но в завершение был снова вызван обработчик событий WindowMouseUp (). В последней цепочке событий произошло следующее: событие Mouse Down было обработано кнопкой и использовано для генерации ее события Click. В рамках базовой реализации обработчика событий MouseDown кнопки для свойства Handled параметра аргументов события RoutedEventArgs было установлено значение true. Это привело к прерыванию потока событий, в результате чего событие MouseDown не было передано обратно вверх по иерархии элементов управления. На случай, если читателю интересно, способ, которым элемент управления Button прерывает маршрутизацию событий, и является той причиной, по которой последовательность событий, возникающая после выполнения щелчка на элементе управления Rectangle, была рассмотрена перед практическим занятием. Подключаемые события В предыдущем примере событие Button.Click было добавлено к элементу управления Button. Обработчиков для события Click элементов управления Grid и Window мы не добавляли, потому что у них нет события Click. Однако иногда может потребоваться, чтобы оно у них было. Например, представим, что имеется окно, содержащее тысячу кнопок, и требуется обработать событие Click каждой из них. В таком случае можно либо добавить тысячу обработчиков, либо поступить проще и добавить один обработчик, общий для всех. Но даже в случае добавления одного обработчика его все равно придется связывать с каждым событием Button.Click. В WPF предлагается альтернативный (и более удобный) способ для разрешения подобных ситуаций, и заключается он в применении подключаемых событий. С помощью системы подключаемых событий обрабатывать события можно даже в тех элементах управления, которые их не предоставляют, т.е. в рассматриваемом примере можно было бы обработать событие Button.Click в содержащем кнопки элементе управления Grid, даже несмотря на то, что у этого элемента управления нет события Click. На самом деле это было бы событие ButtonBase. Click, поскольку ButtonBase представляет собой класс, который определяет событие Click, наследуемое элементом управления Button. Необходимый для этого синтаксис выглядит точно так же, как и синтаксис атрибутов, используемый для подключаемых свойств: <Grid Name="contentGrid" ButtonBase .Click^'contentGndjnick" ...> <Button Name="buttonl" ...>Buttonl</Button> <Button Name="button2" ...>Button2</Button> <Button Name="buttonl000" ...>Buttonl000</Button> </Grid> В обработчике событий мы получаем ссылку на элемент управления Grid в параметре отправителя, а, значит, можем использовать свойство RoutedEventArgs . Source для определения того, на какой кнопке был выполнен щелчок, и реагировать соответствующим образом. Эта событие все равно будет генерироваться только при выполнении щелчка на кнопке, а не на фоновой области элемента управления Grid, поскольку у элемента управления Grid нет события Click, которое он мог бы генерировать.
Глава 34. Windows Presentation Foundation 1105 Компоновка элементов управления Пока что в этой главе для компоновки элементов управления применялся элемент Grid, в основном из-за того, что он представляет собой такой элемент, который поставляется по умолчанию при создании любого нового WPF-приложения. Однако мы еще не рассказали ни о всех возможностях данного класса, ни о других контейнерах, которыми можно пользоваться для получения альтернативных вариантов компоновки. Поэтому в настоящем разделе мы остановимся на компоновке элементов управления более подробно, ведь эта тема является одной из самых фундаментальных в WPF. Все элементы управления, предназначенные для компоновки содержимого, наследуются от абстрактного класса Panel. Этот класс просто определяет контейнер, способный содержать коллекцию объектов, унаследованных от UIElement. Все элементы управления в WPF наследуются от UIElement. Использовать для компоновки элементов управления непосредственно сам класс Panel нельзя, но можно создавать производные от него классы для этого. В качестве альтернативного варианта можно использовать один из следующих наследующихся от Panel элементов управления. □ Canvas. Этот элемент управления позволяет размещать дочерние элементы управления любым желаемым образом. Он не налагает никаких ограничений на место размещения дочерних элементов управления, но и никак не помогает в этом процессе. □ DockPanel. Этот элемент управления позволяет пристыковывать дочерние элементы управления к одному из четырех краев. Последний дочерний элемент занимает оставшееся пространство. □ Grid. Как уже показывалось, этот элемент управления является очень гибким в плане размещения дочерних элементов управления, но кроме этого еще также может разбиваться на строки и столбцы, что позволяет осуществлять в нем выравнивание дочерних элементов управления. □ StackPanel. Этот элемент управления позволяет размещать дочерние элементы в последовательном горизонтальном или вертикальном порядке. □ WrapPanel. Этот элемент управления так же, как и StackPanel, позволяет размещать дочерние элементы управления в последовательном горизонтальном или вертикальном порядке, но виде не одной, а нескольких строк или столбцов в соответствии с доступным пространством. То, как именно можно использовать все эти элементы управления, мы покажем более детально чуть позже. Перед этим необходимо разобраться в следующих важных вопросах. □ Что подразумевается под стопочным порядком размещения элементов управления. □ Как использовать свойства типа Alignment, Margin и Padding для размещения элементов управления и их содержимого. □ Как применять элемент управления Border. Стопочный порядок размещения элементов управления Когда в элементе управления, играющем роль контейнера, содержится несколько дочерних элементов управления, они прорисовываются в определенном стопочном порядке. Это понятие может быть уже знакомо из пакетов рисования графики. Легче всего понять, что означает стопочный порядок — это представить, что каждый элемент управления содержится внутри стеклянной пласти'нки и что в контейнере содер-
1106 Часть V. Дополнительные технологии жится целая стопка таких стеклянных пластинок. Извне тогда этот контейнер будет выглядеть так, как если на него смотреть сверху через все эти слои стеклянных пластинок. Содержащиеся в нем элементы управления будут перекрывать друг друга, а это значит, что то, какие из них будут видны, а какие нет, зависит от порядка стеклянных пластинок. То есть элементы управления, расположенные выше в стопке, будут видны в области перекрытия, а элементы управления, расположенные ниже в стопке, будут, соответственно, либо частично, либо полностью скрываться теми, что находятся над ними. Еще это будет отражаться и на проверке попаданий при выполнении щелчка в окне кнопкой мыши. Целевым элементом управления всегда будет тот, который при рассмотрении перекрывающихся элементов управления будет находиться выше всех в стопке. Стопочный порядок элементов управления определяет порядок, в котором они идут в списке дочерних элементов контейнера. Первый дочерний элемент в контейнере будет размещаться на самом нижнем уровне в стопке, последний — на самом верхнем, а другие, идущие между первым и последним элементы управления — на соответствующих более высоких уровнях. Стопочный порядок размещения имеет в случае некоторых элементов управления компоновкой в WPF кое-какие дополнительные особенности, которые будут рассмотрены позже. Свойства HorizontalAligzuaent, VerticalAlignment, Margin, Padding, Height и Width В более ранних примерах уже показывалось, как, комбинируя свойства Margin, HorizontalAlignment и VerticalAlignment, можно размещать элементы управления в контейнере Grid желаемым образом, а также как с использованием свойств Height и Width задавать их размеры. Все эти свойства, вместе с еще не рассматривавшимся свойством Padding, являются полезными для всех элементов управления компоновкой (или, по крайней мере, большинства из них, как будет показано позже), но разным образом. Некоторые из элементов управления компоновкой могут также устанавливать для этих свойств значения по умолчанию. Во всем этом можно будет убедиться на примерах, которые будут приводиться в последующих разделах, а пока что необходимо разобраться в основных деталях. Свойства HorizontalAlignment и VerticalAlignment отвечают за то, как должен выравниваться элемент управления, но какие значения для них можно устанавливать еще не показывалось. Для свойства HorizontalAlignment, например, можно устанавливать такие значения, как Left, Right, Center и Stretch. Значения Left и Right позволяют размещать элементы управления, соответственно, с левого или с правого края контейнера, значение Center — посередине, а значение Stretch дает возможность изменять ширину элемента управления так, чтобы его края достигали сторон контейнера. Свойство VerticalAlignment выглядит похоже и может принимать значения Top, Bottom, Center и Stretch. Свойства Margin и Padding отвечают за количество пространства, которое должно оставляться пустым, соответственно, снаружи и изнутри краев элементов управления. В приводившихся ранее примерах свойство Margin использовалось для размещения элементов управления относительно, например, левого верхнего угла контейнера Grid. Это работало потому, что в случае установки для свойства HorizontalAlignment значения Left, а для свойства VerticalAlignment — значения Тор, элемент управления пристыковывается прямо к левому верхнему углу, а свойство Margin приводило к добавлению пробелов снаружи этого элемента управления. Свойство Padding имеет похожее предназначение, но только отделяет пробелами содержимое элемента управления от его краев. Оно особенно полезно для элемента управления Border, о котором
Глава 34. Windows Presentation Foundation 1107 будет рассказываться в следующем разделе. И свойство Padding, и свойство Margin можно задавать как с помощью четырех отдельных частей (lef tAmount, topAmount, rightAmount и bottomAmount), так и с помощью одного значения (Thickness). Что касается свойств Height и Width, то ими часто управляют другие свойства. Например, в случае установки для свойства HorizontalAlignment значения Stretch, свойство Width элемента управления будет изменяться в соответствии с шириной контейнера, в котором он содержится. Элемент управления Border Элемент управления Border является очень простым и полезным элементом управления типа контейнера. Он может содержать только один дочерний элемент управления, а не несколько, как это могут делать более сложные элементы управления, о которых речь пойдет далее. Размер дочернего элемента изменяется так, чтобы полностью занять элемент управления Border. Это может показаться не особо полезным, но не стоит забывать о том, что можно использовать свойства Margin и Padding для размещения элемента управления Border в пределах его контейнера, а его содержимого — в пределах его собственных краев. Еще можно устанавливать значение для такого свойства элемента управления Border, как Background, и тем самым делать его видимым. Чуть позже этот элемент управления будет показан в действии. Элемент управления Canvas Элемент управления Canvas, как уже отмечалось ранее, предоставляет полную свободу в отношения расположения дочерних элементов. Еще одной его особенностью является то, что свойства HorizontalAligment и VerticalAlignment, используемые с дочерними элементами, не будут никак влиять на их размещение. Для размещения элементов в элементе управления Canvas можно, как и в предыдущих примерах, применять свойство Margin, но все-таки лучше вместо него использовать подключаемые свойства Canvas.Left, Canvas.Top, Canvas.Right и Canvas. Bottom, которые предоставляет сам класс Canvas: <Canvas ...> <Button Canvas.Top=0" Canvas.Left=0" ...>ButtonK/Button> </Canvas> В этом коде указано, что элемент управления Button должен размещаться так, чтобы его верхний край находился в 10 пикселях от верхнего края элемента управления Canvas, а его левый край — в 10 пикселях от левого края элемента управления Canvas. Обратите внимание на то, что свойства Тор и Left имеют более высокую степень приоритетности, чем свойства Bottom и Right. Например, в случае указания и Тор, и Bottom, свойство Bottom будет игнорироваться. На рис. 34.11 показан пример размещения двух элементов управления Rectangle в элементе управления Canvas внутри окон двух разных размеров. Все приводимые в этом разделе примеры компоновок можно найти в проекте LayoutExampl es внутри загружаемого кода для этой главы. Один элемент управления Rectangle размещается относительно левого верхнего угла, а второй — относительно правого нижнего. При изменении размера окна эти относительные позиции все равно сохраняются. Еще здесь можно увидеть ту важную роль, которую играет стопочный порядок при расположении элементов управления Rectangle. Тот элемент управления Rectangle, который размещается в правом нижнем углу, занимает более высокую позицию в стопочном порядке, поэтому при наложении обоих элементов управления друг на друга, видимым остается именно он.
1108 Часть V. Дополнительные технологии ■ Canvas v l-l^^frp Рис. 34.11. Пример компоновки элементов управления Rectangle в элементе управления Canvas Код, необходимый для реализации данного примера, выглядит следующим образом: <Canvas Background="AliceBlue"> <Rectangle Canvas.Left=0" Canvas.Top=0" Height=0" Width=00" Stroke=MBlack" Fill="Chocolate" /> <Rectangle Canvas.Right=0" Canvas.Bottom=0" Height=0" Width=00" Stroke="Black" Fill="Bisque" /> </Canvas> Элемент управления DockPanel Элемент управления DockPanel позволяет пристыковывать дочерние элементы управления к одному из своих краев. Такой вид компоновки уже должен быть знаком, даже если вы никогда специально не обращали на это внимания. Ведь именно таким образом, например, элемент управления Ribbon в Word остается всегда в верхней части окна Word и именно таким образом в VS и VCE размещаются различные окна. В VS и VCE можно (как специально, так и случайно) изменять место стыковки окон, перетаскивая из одного места в другое. У DockPanel имеется одно единственное подключаемое свойство DockPanel. Dock, которое дочерние элементы управления могут использовать для указания края для пристыковки. Для этого свойства можно устанавливать значение Left, Top, Right или Bottom. Стопочный порядок элементов управления в DockPanel играет чрезвычайно важную роль, поскольку с каждой пристыковкой к краю какого-то элемента управления пространство для пристыковки следующих дочерних элементов управления неизбежно уменьшается. Например, можно пристыковать к верхнему краю DockPanel одну панель инструментов, а к левому — вторую. Тогда первый элемент управления растянется вдоль всего верхнего края DockPanel, а второй — вдоль левого края, но начиная только от нижней части первой панели инструментов до нижней части DockPanel. Последний указанный дочерний элемент будет (обычно) занимать область, которая останется после размещения всех предыдущих дочерних элементов управления. (Этим поведением можно управлять, поэтому и было сделано такое уточнение.) При размещении элемента управления в DockPanel занимаемая им область может оказываться меньше той, что была для него зарезервирована в этом элементе управления. Например, в случае пристыковки элемента управления Button со значением 100 в свойстве Width, значением 50 в свойстве Height и значением Left в свойстве HorizontalAlingment, справа от него останется место, которое нельзя будет ис-
Глава 34. Windows Presentation Foundation 1109 пользовать для пристыковки никаких других дочерних элементов управления. Кроме того, при наличии у этого элемента управления свойства Margin со значением 20, в верхней части DockPanel под него будет зарезервировано всего 90 пикселей (в результате сложения высоты этого элемента управления с верхним и нижнем полем). Об этом поведении нужно обязательно помнить при применении элемента управления DockPanel для компоновки, иначе оно может привести к получению самых неожиданных результатов. На рис. 34.12 показан пример использования элемента управления DockPanel для компоновки. Рис. 34.12. Пример использования элемента управления DockPanel для компоновки Код, необходимый для реализации такой компоновки, выглядит следующим образом: <DockPanel Background="AliceBlue"> <Border DockPanel.Dock="Top" Padding=0" Margin=" Background="Aquamarine" Height=5"> <Label>l) DockPanel.Dock="Top"</Label> </Border> <Border DockPanel.Dock="Top" Padding=0" Margin=" Background="PaleVioletRed" Height=5" Width=00"> <Label>2) DockPanel.Dock="Top"</Label> </Border> <Border DockPanel.Dock="Left" Padding=0" Margin=" Background="Bisque" Width=00"> <Label>3) DockPanel.Dock="Left"</Label> </Border> <Border DockPanel.Dock="Bottom" Padding=0" Margin=" Background="Ivory" Width=00" HorizontalAlignment="Right"> <Label>4) DockPanel.Dock="Bottom"</Label> </Border> <Border Padding=0" Margin=" Background="BlueViolet"> <Label Foreground="White">5) Last control</Label> </Border> </DockPanel> В этом коде используется рассмотренный ранее элемент управления Border для четкого выделения областей стыкуемых элементов управления в примере компоновки, а также элементы управления Label для вывода простого информативного текста. Чтобы разобраться в этой компоновке, необходимо проанализировать ее сверху вниз, рассмотрев каждый элемент управления по очереди. ,
1110 Часть V. Дополнительные технологии 1. Первый элемент управления Border стыкуется к верхнему краю элемента управления DockPanel. Общая занимая им область в DockPanel составляет 55 верхних пикселей (Height + 2 х Margin). Обратите внимание на то, что свойство Padding на его расположение никак не влияет, поскольку он находится в пределах краев элемента управления Border, но зато оно влияет на расположение вложенного в него элемента управления Label. Элемент управления Border заполняет собой все доступное пространство вдоль того края, к которому пристыковывается, если не ограничивается свойствами Height или Width, из-за чего он и растягивается вдоль элемента управления DockPanel. 2. Второй элемент управления тоже пристыковывается к верхнему краю DockPanel и занимает еще 55 пикселей сверху области отображения. Этот элемент управления включает свойство Width, вынуждающее его занимать только часть ширины DockPanel. Размещается он по центру, поскольку для свойства HorizonalAlignement в DockPanel по умолчанию устанавливается значение Center. 3. Третий элемент управления Border пристыковывается к левому краю DockPanel и занимает 210 пикселей в левой части области отображения. 4. Четвертый элемент управления Border пристыковывается к нижнему краю DockPanel и занимает 30 пикселей плюс количество пикселей, которое в высоту занимает содержащийся в нем элемент управления Label (каким бы оно не было). Значение высоты, из-за того, что оно не было указано явно, вычисляется на основании значений свойств Margin и Padding и содержимого элемента управления Border. Еще этот элемент управления Border прикрепляется в правому нижнему углу DockPanel, поскольку в его свойстве HorizontalAlignment содержится значение Right. 5. Пятый и последний элемент управления Border занимает оставшееся пространство. Запустите этот пример и поэкспериментируйте с изменением размеров содержимого. Обратите внимание на то, что чем более высокое место элемент управления занимает в стопочном порядке, тем большая степень приоритетности предоставляется его пространству. При сужении окна пятый элемент управления Border может очень быстро оказаться скрытым за элементами управления с более высокой позицией в стопочном порядке. Во избежание подобных проблем, при использовании компоновки на базе элемента управления DockPanel всегда принимайте меры предосторожности, например, задавайте минимально допустимые размеры для окна. Элемент управления Grid Элементы управления Grid могут иметь много строк и столбцов, пригодных для размещения в них дочерних элементов управления. В этой главе они уже несколько раз использовались, но во всех случаях состояли только из одной строки и одного столбца. Для добавления большего количества строк и столбцов нужно использовать свойства RowDef initions и ColumnDef initions, которые представляют собой коллекции, соответственно, объектов RowDef inition и ColumnDef inition и задаются с помощью синтаксиса элементов свойств: <Grid> <Gr id. RowDef imtions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions>
Глава 34. Windows Presentation Foundation 1111 <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> </Grid> В этом коде определяется элемент управления Grid с тремя строками и двумя столбцами. Обратите внимание, что никакой дополнительной информации здесь не требуется; в случае использования такого кода, каждая строка и столбец будет автоматически изменяться в размере динамическим образом при изменении размера элемента управления Grid. В частности, размер каждой строки будет составлять треть от высоты элемента управления Grid, а каждого столбца— половину от его ширины. Устанавливая свойство Grid.ShowGridlines в true, можно делать так, чтобы между всеми ячейками в Grid отображались линии. С помощью свойств Width, Height, MinWidth, MaxWidth, MinHeight и MaxHeight можно управлять поведением, связанным с изменением размера. Например, установка значения для такого свойства столбца, как Width, гарантирует, что столбец будет всегда иметь в точности заданную ширину. Еще для этого свойства столбца можно устанавливать значение *, подразумевающее, что тот должен занимать по ширине все пространство, которое останется после вычисления ширины всех остальных столбцов. Фактически именно оно и является значением по умолчанию. При наличии нескольких столбцов со значением * в свойстве Width оставшееся пространство будет распределяться между ними поровну. Значение * может также использоваться и свойством Height для строк. Другим возможным значением для Height и Width является Auto, которое подразумевает установку размера строки или столбца в соответствии с его содержимым. Еще можно использовать элементы управления GridSplitter для предоставления пользователям возможности настраивать размеры строк и столбцов путем выполнения щелчка на их границах и их перетаскивания. Для дочерних элементов управления в Grid можно применять подключаемые свойства Grid.Column и Grid.Row для указания ячейки, в которой они должны содержаться. Оба эти свойства по умолчанию имеют значение 0, поэтому в случае их пропуска дочерний элемент будет размещаться в левой верхней ячейке. Еще для дочерних элементов управления можно использовать свойства Grid.ColumnSpan и Grid.RowSpan для их размещения сразу в целом ряде ячеек в таблице с заданием левой верхней ячейки в Grid.Column и Grid.Row. На рис. 34.13 показан пример отображения элемента управления Grid, содержащего множество овалов и элемент управления GridSplitter, в окнах двух разных размеров. Рис. 34.13. Пример отображения элемента управления Grid со множеством овалов и элементом управления GridSplitter внутри
1112 Часть V. Дополнительные технологии Код, необходимый для реализации этого примера, выглядит следующим образом: <Grid Background="AliceBlue"> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth=00M MaxWidth=00" /> <ColumnDefinition MaxWidth=00" /> <ColumnDefinition Width=0" /> <ColumnDefinition Width="*" /> </Gnd.ColumnDef initions> <Grid.RowDefmitions> <RowDefinition Height=0" /> <RowDefinition MinHeight=00" /> <RowDefinition/> </Grid.RowDefinitions> <Ellipse Grid.Row=" Grid.Column=" Fill="BlanchedAlmond" Stroke="Black" /> <Ellipse Grid.Row=" Grid.Column="l" Fill="BurlyWood" Stroke="Black" /> <Ellipse Grid.Row=M0" Grid.Column=" Fill=,,BlanchedAlmond,, Stroke="Black" /> <Ellipse Grid.Row=" Grid.Column=" Fill="BurlyWood" Stroke="Black" /> <Ellipse Grid.Row="l" Grid.Column=" Fill="BurlyWood" Stroke="Black" /> <Ellipse Grid.Row="l" Grid.Column="l" Fill="BlanchedAlmond" Stroke="Black" /> <Ellipse Grid.Row="l" Grid.Column=" Fill="BurlyWood" Stroke="Black" /> <Ellipse Grid.Row="l" Grid.Column=" Fill="BlanchedAlmond" Stroke="Black" /> <Ellipse Grid.Row=" Grid.Column=" Fill="BlanchedAlmond" Stroke="Black" /> <Ellipse Grid.Row=" Grid.Column="l" Fill="BurlyWood" Stroke="Black" /> <Ellipse Grid.Row=" Grid.Column=" Fill="BlanchedAlmond" Stroke="Black" /> <Ellipse Grid.Row=" Grid.Column=" Fill="BurlyWood" Stroke="Black" /> <Ellipse Grid.Row=" Grid.Column=" Grid.ColumnSpan=" Fill="Gold" Stroke="Black" Height=0" /> <GridSplitter Grid.RowSpan=" Width=0" BorderThickness="> <GridSplitter.BorderBrush> <SolidColorBrush Color="Black" /> </GridSplitter.BorderBrush> </GridSplitter> </Grid> В этом коде в определениях строк и столбцов используются различные комбинации свойств для достижения интересного эффекта при изменении размера отображения, поэтому действительно стоит протестировать его самостоятельно. Сначала рассмотрим строки. Верхняя строка имеет фиксированную высоту в 50 пикселей, вторая строка — минимальную высоту в 100 пикселей, а третья занимает все остающееся пространство. Это означает, что при высоте меньше 150 пикселей у Grid, третья строка будет не видна. При высоте от 150 до 250 пикселей у Grid изменяться будет размер только третьей строки, от 0 до 100. Объясняется это тем, что остающееся пространство вычисляется путем вычитания из общей высоты суммы высот всех строк, которые имеют фиксированные значения высоты. Это остающееся пространство выделяется между второй и третьей строкой, но из-за того, что у второй строки есть минимальная высота, составляющая 100 пикселей, она не будет изменяться по высоте до тех пор, пока общая высота Grid не достигнет 250 пикселей. И, наконец, при высоте более 250 пикселей у Grid остающееся пространство будет распределяться между и второй и третьей строкой, в результате чего их высота будет как равняться, так и превышать 100 пикселей. Теперь давайте рассмотрим столбцы. Только третий столбец имеет фиксированный размер, составляющий 50 пикселей. Первый и второй столбцы могут делить между собой максимум до 300 пикселей. Четвертый столбец, следовательно, будет единствен-
Глава 34. Windows Presentation Foundation 1113 ный увеличиваться в размере при достижении Grid общей ширины, превышающей 550 пикселей. Чтобы разобраться в этом самостоятельно, попробуйте догадаться, сколько пикселей доступно для столбцов и как они распределяются. Сначала 50 пикселей выделяется третьему столбцу, а 500 остается остальным столбцам. Максимальная ширина третьего столбца составляет 100 пикселей, оставляя на пространство между первым и четвертым столбцом 400 пикселей. Максимальная ширина первого столбца составляет 200 пикселей, так что даже в случае превышения этого значения, большего количества пространства он все равно занимать не будет. Вместо этого в размере будет увеличиваться четвертый столбец. Обратите внимание на два дополнительных момента в этом примере: во-первых, последний определенный эллипс захватывает третий и четвертый столбец, иллюстрируя тем самым действие свойства Grid.ColumnSpan, а, во-вторых, для обеспечения возможности изменять размер первого и второго столбца предоставляется элемент управления GridSplitter. При превышении элементом управления Grid 550 пикселей по общей ширине, однако, этот элемент управления GridSplitter не сможет позволять изменять размер этих столбцов, потому что ни первый, ни второй столбец не сможет увеличиваться в размерах. Элемент управления GridSplitter является очень полезным, но имеет довольно скучный внешний вид. Для извлечения из него максимальной пользы его действительно лучше подвергать стилизации или хотя бы делать невидимым, устанавливая его свойство Background в Transparent. При наличии нескольких элементов управления Grid в окне можно еще определять для строк или столбцов группы общего размера за счет использования в их определениях свойства ShareSizeGroup и установки для него в качестве значения предпочитаемого строкового идентификатора. Например, в случае изменения входящего в группу общего размера столбца в одном элементе управления Grid, размер столбца, входящего в эту же группу в другом элементе управления Grid, будет изменяться соответствующим ему образом. Включать и отключать эту функциональную возможность можно с помощью свойства Grid. IsSharedSizeScope. Элемент управления StackPanel Элемент управления StackPanel по сравнению со сложным Grid является относительно простым. Его можно считать своего рода усеченной версией элемента управления DockPanel, когда край, к которому пристыковываются дочерние элементы управления, становится для них фиксированным. Другое отличие между этими элементами управления состоит в том, что последний дочерний элемент в StackPanel не занимает оставшееся пространство. Однако по умолчанию все дочерние элементы растягиваются до краев элемента управления StackPanel. За направление, в котором располагаются элементы управления, отвечают три свойства: Orientation, HorizontalAlignment и VerticalAlignement. Для свойства Orientation может устанавливаться либо значение Horizontal, либо значение Vertical. А свойства HorizontalAlignment и VerticalAlignement позволяют указывать, как должны размещаться дочерние элементы: вдоль верхнего, нижнего, левого или правого края элемента управления StackPanel. Устанавливая для того или иного из них значения Center, можно даже размещать дочерние элементы по центру StackPanel. На рис. 34.14 показаны два элемента управления StackPanel, в каждом из которых содержится по три кнопки. Для их размещения используется элемент управления Grid с двумя строками и одним столбцом.
1114 Часть V. Дополнительные технологии Рис. 34.14. Пример использования элементов управления StackPanel Код, необходимый для реализации этих элементов управления StackPanel, выглядит следующим образом: <Grid Background="AliceBlue"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <StackPanel Grid.Row="> <Button>Buttonl</Button> <Button>Button2</Button> <Button>Button3</Button> </StackPanel> <StackPanel Grid.Row="l" Orientation="Horizontal"> <Button>Buttonl</Button> <Button>Button2</Button> <Button>Button3</Button> </StackPanel> </Grid> При использовании для компоновки элемента управления StackPanel зачастую требуется добавлять линейки прокрутки для обеспечения возможности просматривать все содержащиеся в нем элементы управления. Эта область является еще одной из тех, где WPF выполняет большую часть трудной работы вместо разработчика. В частности, для решения этой задачи WPF позволяет использовать элемент управления ScrollViewer путем размещения элемента управления StackPanel внутри него: <Grid Background="AliceBlue"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <ScrollViewer> <StackPanel Grid.Row="> <Button>ButtonK/Button> <Button>Button2</Button> <Button>Button3</Button> </StackPanel> </ScrollViewer> <StackPanel Grid.Row="l" Orientation=MHorizontal"> <Button>Buttonl</Button> <Button>Button2</Button> <Button>Button3</Button> </StackPanel> </Grid>
Глава 34. Windows Presentation Foundation 1115 Существуют и другие более сложные приемы, которые можно применять для обеспечения различных способов прокручивания или программной реализации прокручивания, но обычно вполне хватает и этого. Элемент управления WrapPanel Элемент управления WrapPanel, по сути, является расширенной версией элемента StackPanel, в которой "не умещающиеся" элементы управления переносятся на дополнительные строки (или в дополнительные столбцы). На рис. 34.15 показан элемент управления WrapPanel, содержащий множество фигур и отображающийся в окнах двух разных размеров. Рис. 34.15. Пример элемента управления WrapPanel Ниже приведена сокращенная версия кода, необходимого для реализации такого элемента управления WrapPanel: <WrapPanel Background^'AliceBlue"> <Rectangle Fill="#FF000000" Height=' RadiusX=0" RadiusY=0" /> <Rectangle Fill=,,#FFllllll" Height=' RadiusX=M10" RadiusY=0" /> '50" Width=0" Stroke="Black" 50" Width=0" Stroke="Black" <Rectangle Fill="#FF222222" Height=0" Width=0" Stroke=MBlack" RadiusX=0" RadiusY=0" /> <Rectangle Fill="#FFFFFFFF" Height=0" Width=0" Stroke="Black" RadiusX=0" RadiusY=0" /> </WrapPanel> Элементы управления WrapPanel представляют собой замечательный способ для создания динамических схем компоновки, позволяющих пользователям самим управлять тем, каким точно образом им просматривать содержимое. Стилизация элементов управления Одной из наилучших функциональных возможностей WPF является предоставление дизайнерам полного контроля над внешним видом и поведением пользовательских интерфейсов. Это подразумевает, главным образом, предоставление им возможности придавать элементам управления любой желаемый стиль в двух или даже трех измерениях. Пока что для элементов управления применялись только базовые стили, поставляемые с .NET 3.5, но на самом деле количество возможных вариантов просто бесконечно.
1116 Часть V. Дополнительные технологии В этом разделе речь пойдет о двух основных средствах. □ Стили. Наборы свойств, применяемые к элементу управления одним пакетом. □ Шаблоны. Элементы управления, применяемые для построения изображения элемента управления. Эти средства отчасти связаны между собой, потому что стили могут содержать шаблоны. Стили У элементов управления WPF есть свойство под названием Style (унаследованное от FrameworkElement), которое может устанавливаться в экземпляр класса Style. Класс Style является довольно сложным и способен предоставлять усовершенствованные средства стилизации, но в своей основе он, по сути, представляет собой просто набор объектов Setter. Каждый объект Setter отвечает за установку значения свойства в соответствии со значением своего свойства Property (содержащим имя устанавливаемого свойства) и значением своего свойства Value (содержащим значение, которое нужно установить для данного свойства). Можно либо полностью квалифицировать имя требуемого свойства в Property, т.е. указывать в нем и тип элемента управления, к которому оно относится (например, Button.Foreground), либо устанавливать значение для свойства TargetType объекта Style (например, Button), чтобы тот сам мог выводить имена свойств. Приведенный ниже код тогда показывает, как использовать объект Style для установки свойства Foreground элемента управления Button: <Button> Click me! <Button.Style> <Style TargetType="Button"> <Setter Property="Foreground"> <Setter.Value> <SolidColorBrush Color="Purple" /> </Setter.Value> </Setter> </Style> </Button.Style> </Button> Очевидно, что в данном случае было бы гораздо проще установить свойство Foreground кнопки обычным образом. Стили становятся гораздо более полезными тогда, когда превращаются в ресурсы, поскольку ресурсы могут применяться снова и снова. То, как именно стили могут превращаться в ресурсы, будет показано позже в этой главе. Шаблоны Элементы управления создаются с помощью шаблонов, которые можно настраивать. Любой шаблон состоит из иерархии элементов управления, применяемых для построения изображения данного элемента, в состав которой может входить также и элемент управления типа ContentPresenter, отвечающий за отображение содержимого элементов, которые, как, например, кнопки, таковым обладают. Шаблон элемента управления хранится в его свойстве Template, которое является экземпляром класса ControlTemplate. Класс ControlTemplate включает свойство TargetType, принимающее в качестве значения тип того элемента управления, для которого определяется шаблон, и может содержать только один элемент управления. Этим элементом управления может быть и контейнер вроде Grid, так что возможности это в принципе никак особо не ограничивает.
Глава 34. Windows Presentation Foundation 1117 Обычно шаблон устанавливается для класса применением стиля. Это просто подразумевает предоставление элементов управления, подлежащих использованию для свойства Template, следующим образом: <Button> Click me' <Button.Style> <Style TargetType="Button"> <Setter Property="Template"> <Setter.Value > <ControlTemplate TargetType="Button"> </ControlTemplate> </Setter.Value> </Setter> </Style> </Button.Style> </Button> Некоторым элементам управления может требоваться более одного шаблона. Например, для Checkbox необходим один шаблон (CheckBox. Template) для отображения ячейки для отметки и еще один (CheckBox.ContentTemplate) —для вывода рядом с этой ячейкой соответствующего текста. Шаблоны, в которых требуется использовать элементы управления ContentPresenter, могут включать их в том месте, где должно выводиться содержимое. Некоторые элементы управления, в частности, те, которые отвечают за отображения коллекций элементов, подразумевает применение альтернативных приемов, которые в этой главе не рассматриваются. Заменять шаблоны наиболее полезно в сочетании с применением ресурсов. Однако поскольку стилизация элементов управления является очень распространенным приемом, будет не лишним посмотреть, как именно она выполняется, в следующем практическом занятии. практическое занятие Применение стилей и шаблонов 1. Создайте новое WPF-приложение по имени Ch34Ex03 и сохраните его в каталоге С:\BegVCSharp\Chapter34. 2. Измените код в его файле Windowl .xaml следующим образом: <Window x:Class="Ch34Ex03.Windowl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Nasty Button" Height=50" Width=50"> <Grid Background="Black"> <Button Margin=0" Click="Button_Click"> Would anyone use a button like this? <Button.Style> <Style TargetType="Button"> <Setter Property="FontSize" Value=8" /> <Setter Property="FontFamily" Value="arial" /> <Setter Property="FontWeight" Value="bold" /> <Setter Property="Foreground" > <Setter.Value> <LinearGradientBrush StartPoint=.5,0" EndPoint=.5,l"> <LinearGradientBrush.GradientStops > <GradientStop Offset=.0" Color="Purple" /> <GradientStop Offset=.5" Color="Azure" />
1118 Часть V. Дополнительные технологии <GradientStop Offset="l.0" Color="PurpleM /> </LinearGradientBrush.GradientStops > </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width=0" /> <ColumnDefinition /> <ColumnDefinition Width=0" /> </Grid.ColumnDefinitions > <Grid.RowDefinitions> <RowDefinition MinHeight=0" /> </Grid.RowDefinitions> <Ellipse Grid.Column=" Height=0"> <Ellipse.Fill> <RadialGradientBrush> <RadialGradientBrush.GradientStops> <GradientStop Offset=.0" Color="Yellow" /> <GradientStop Offset=.0" Color="Red" /> </RadialGradientBrush.GradientStops> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> <Grid Grid.Column="l"> <Rectangle RadiusX=0" RadiusY=0M> <Rectangle.Fill> <RadialGradientBrush> <RadialGradientBrush.GradientStops> <GradientStop Offset=.0" Color="YellowM /> <GradientStop Offset=.0" Color="Red" /> </RadialGradientBrush.GradientStops > </RadialGradientBrush> </Rectangle.Fill> </Rectangle> <ContentPresenter Margin=0,0,20,0" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> <Ellipse Grid.Column=" Height=0"> <Ellipse.Fill> <RadialGradientBrush> <RadialGradientBrush.GradientStops> <GradientStop Offset=.0" Color="Yellow" /> <GradientStop Offset="l.0" Color="Red" /> </RadialGradientBrush.GradientStops> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </Button.Style> </Button> </Grid> </Window>
Глава 34. Windows Presentation Foundation 1119 3. Измените код в Windowl. xaml. cs, как показано ниже: public partial class Windowl : Window { private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Button clicked."); } } 4. Запустите приложение и щелкните один раз на кнопке. На рис. 34.16 показан результат, который должен получиться. Рис. 34.16. Приложение Ch34Ex03 в действии Описание полученных результатов Внешний вид кнопки в этом примере не особенно впечатляет. Однако если оставить в стороне эстетические детали, то данный пример действительно демонстрирует, как полностью изменять вид кнопки в WPF без приложения многочисленных усилий. Важно обратить внимание, что, несмотря на изменение шаблона кнопки, ее функциональные возможности остаются прежними. То есть на ней по-прежнему можно щелкать и обеспечивать реакцию на щелчок в обработчике событий. Нетрудно было заметить, что некоторые вещи, ассоциируемые с кнопками Windows, в используемом здесь шаблоне не реализованы. В частности, в нем не реализовано отображение визуальной подсказки ни при наведении курсора на кнопку, ни при выполнении на ней щелчка. Еще эта кнопка выглядит абсолютно одинаково, находясь и не находясь в фокусе. Для получения всех этих эффектов необходимо использовать триггеры, о которых речь пойдет в следующем разделе. Давайте несколько более детально остановимся на коде этого примера, уделив внимание стилям и шаблонам и тому, как именно здесь был создан шаблон. В начале примера идет обычный код для отображения элемента управления Button: <Button Margin=0" Click="Button_Click"> Would anyone use a button like this? В этом коде для кнопки предоставляются базовые свойства и содержимое. Далее для свойства Style в качестве значения устанавливается объект Style, в котором первым делом задаются три простых свойства, отвечающие за внешний вид шрифта в элементе управления Button:
1120 Часть V. Дополнительные технологии <Button.Style> <Style TargetType="Button"> <Setter Property="FontSize" Value=8" /> <Setter Property="FontFamily" Value="arial" /> <Setter Property="FontWeight" Value="bold" /> Затем устанавливается свойство Button.Foreground с применением синтаксиса элементов свойств, поскольку используется кисть: <Setter Property="Foreground"> <Setter.Value> <LinearGradientBrush StartPoint=.5,0" EndPoint=.5,l"> <LinearGradientBrush.GradientStops> <GradientStop Offset=.0M Color="Purple" /> <GradientStop Offset=.5" Color="Azure" /> <GradientStop Offset="l.0" Color="Purple" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Setter.Value> </Setter> В оставшейся части кода внутри объекта Style свойству Button.Template назначается объект ControlTemplate: <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> </ControlTemplate> </Setter.Value> </Setter> </Style> </Button.Style> </Button> Код шаблона, в общем, представляет собой элемент управления Grid с тремя ячейками в одной строке. В этих ячейках содержится элемент управления Ellipse, элемент управления Rectangle с элементом управления ContentPresenter для отображения содержимого шаблона, и еще один элемент управления Ellipse: <Grid> <Ellipse Grid.Column=" Height=0"> </Ellipse> <Grid Gnd.Column="l"> <Rectangle RadiusX=0" RadiusY=0"> </Rectangle > <ContentPresenter Margin=0,0,20,0" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> <Ellipse Grid.Column=" Height=0"> </Ellipse> </Grid> В этом коде нет особо сложных частей, поэтому можете самостоятельно проанализировать его более детально.
Глава 34. Windows Presentation Foundation 1121 Триггеры В первом примере, который приводился в этой главе, уже показывалось, как триггеры могут применяться для связывания событий с действиями. События в WPF могут включать в себя все что угодно, в том числе и щелчки на кнопках, и события, связанные с запуском приложения, и события, связанные с завершением работы приложения, а также многое другое. На самом деле в WPF существует несколько типов триггеров, причем все они наследуются от базового класса TriggerBase. Триггер, показанный в первом примере, представлял собой триггер типа EventTrigger. Класс Event Trigger содержит коллекцию действий, каждое из которых имеет вид объекта, унаследованного от базового класса TriggerAction. Эти действия выполняются при активизации триггера. В WPF не очень много классов унаследовано от TriggerAction, но зато разрешено определять свои собственные. Триггер типа EventTrigger можно использовать для воспроизведения анимационных эффектов с помощью действия BeginStoryboard, для манипулирования раскадровкой с помощью действия ControllableStoryboardAction и для добавления звуковых эффектов с помощью действия SoundPlayerAction. Поскольку триггер этого типа наиболее часто применяется в анимационных эффектах, более подробно о нем будет рассказываться в следующем разделе. У каждого элемента управления есть свойство Triggers, которое можно использовать для определения триггеров прямо на уровне данного элемента управления. Триггеры также можно определять и где-то выше в иерархии, например, как показывалось ранее, на уровне объекта Window. Для стилизации элементов управления чаще всего применяется триггер типа Trigger (хотя для сопровождения элементов управления анимационными эффектами все-таки чаще применяется триггер типа EventTrigger). Класс Trigger используется для установки свойств в ответ на изменения в других свойствах и особенно полезен в объектах Style. Конфигурируются объекты Trigger следующим образом. □ Для определения свойства, за которым должен следить объект Trigger, применяется свойство Trigger. Property. □ Для определения того, когда объект Trigger должен активизироваться, применяется свойство Trigger .Value. □ Для определения действий, которые должен выполнять объект Trigger, применяется свойство Trigger. Setter с коллекцией объектов Setters в качестве значения. Упомянутые здесь объекты Setter ничем не отличаются от тех, что описывались ранее в этой главе, в разделе "Стили". Например, приведенный ниже триггер будет анализировать значение свойства MyBooleanValue и, если оно равно true, устанавливать для свойства Opacity значение 0 .5: <Trigger Property="MyBooleanValue" Value="true"> <Setter Property="Opacity" Value=.5" /> </Trigger> Сам по себе этот код ни о чем особо не говорит, поскольку он не связан ни с одним элементом управления или стилем. Следующий код является гораздо более показательным, поскольку демонстрирует то, как объект Trigger может использоваться в объекте Style:
1122 Часть V. Дополнительные технологии <Style TargetType="Button"> <Style.Triggers> <Trigger Property="IsMouseOver" Value="true"> <Setter Property="Foreground" Value="Yellow" /> </Trigger> </Style.Triggers> </Style> Этот код в случае установки для свойства Button. IsMouseOver значения true будет приводить к изменению у элемента управления Button значения свойства Foreground на Yellow. Свойство IsMouseOver является одним из нескольких чрезвычайно полезных свойств, которые можно использовать в качестве быстрого способа для получения информации об элементах управления и их состоянии. Уже по названию понятно, что значение true для этого свойства устанавливается только тогда, когда на элемент управления наводится курсор мыши. Это позволяет кодировать передвижения мыши. К числу остальных подобных свойств относятся: свойство IsFocused, позволяющее определять то, находится элемент управления в фокусе или нет; свойство IsHitTestVisible, указывающее на то, возможно ли на элементе управления выполнить щелчок (т.е. не закрывают ли его другие элементы управления с более высокой позицией в стопочном порядке), и свойство Is Pressed, показывающее, нажата кнопка или нет. Последнее из этих свойств подходит только для кнопок, унаследованных от ButtonBase, в то время как остальные доступны для всех элементов управления. Помимо свойства Style.Triggers, многих эффектов еще также можно достигать и с помощью свойства ControlTemplate. Triggers. Оно позволяет создавать для элементов управления шаблоны, содержащие триггеры. Именно с его помощью стандартному элементу Button удается реагировать на передвижения мыши, щелчки и изменения фокуса в своем шаблоне. Именно его нужно изменять при желании реализовать упомянутые функциональные возможности самостоятельно. Анимация Анимационные эффекты создаются за счет использования раскадровки (storyboard). Несомненно, лучше всего определять их с помощью визуального средства вроде Expression Blend. Однако их также можно определять и непосредственным редактированием XAML-кода и привлечением файла отделенного кода (поскольку XAML является лишь способом для построения модели объектов WPF). Раскадровка задается с помощью объекта Storyboard, содержащего одну или более временных шкал. Временные шкалы (timeline) определяются с помощью либо ключевых кадров, либо одного из нескольких более простых объектов, инкапсулирующих целые анимационные эффекты. Сложные раскадровки могут даже содержать вложенные раскадровки. Как уже показывалось ранее в примере, объект Storyboard содержится в словаре ресурсов, поэтому на него нужно ссылаться с помощью свойства х:Кеу. Внутри временной шкалы можно анимировать свойства любого элемента в приложении, который относится к типу double, Point или Color. Эти типы охватывают большинство вещей, которые может потребоваться изменять, и обеспечивают вполне приличную степень гибкости. Есть, конечно, и некоторые вещи, которые нельзя делать, например, полностью заменять одну кисть другой, но всегда доступны другие способы получения почти любого эффекта с помощью этих трех типов. У каждого из трех упомянутых типов имеется по два ассоциируемых с ними элемента управления во временной шкале, которые можно использовать в качестве дочерних элементов Storyboard.
Глава 34. Windows Presentation Foundation 1123 Выглядят эти шесть элементов управления так: DoubleAnimation, DoubleAnimation UsingKeyFrames, PointAnimation, PointAnimationUsingKeyFrames, ColorAnimation и ColorAnimationUsingKeyFrames. Каждый из них можно ассоциировать с конкретным свойством конкретного элемента управления путем применения таких подключаемых свойств, как Storyboard.TargetName и Storyboard.TargetProperty. Например, при желании анимировать свойство Width элемента управления Rectangle со значением MyRectangle в свойстве Name, нужно было бы установить для этих свойств значения MyRectangle и Width соответственно, и затем использовать для его анимации либо класс DoubleAnimation, либо класс DoubleAnimationUsingKeyFramesWidth. Свойство Storyboard.TargetProperty способно интерпретировать довольно сложный синтаксис и благодаря этому устанавливать месторасположение любого свойства, которое нужно анимировать. В примере, который приводился в начале этой главе, для двух упомянутых подключаемых свойств использовались такие значения: Storyboard.TargetName="ellipse1" Storyboard.TargetProperty= " (UIElement.RenderTransform).(TransformGroup.Children)[0].(RotateTransform.Angle)" Элемент управления ellipsel относился к типу Ellipse, а свойство TargetProperty задавало угол, на который этот элемент управления должен был вращаться в ходе трансформации. Местонахождение значения этого угла устанавливалось с помощью свойства RenderTransform, принадлежащего экземпляру Ellipse и унаследованного от UIElement, а также первого дочернего элемента в объекте Trans formGroup, который являлся значением этого свойства. Этим первым дочерним элементом был объект RotateTransform, в свойстве Angle которого и содержалось искомое значение угла. Несмотря на то что такой синтаксис может занимать много строк, он очень прост в применении. Самым сложным является определение базового класса, от которого наследуется заданное свойство, но в этом может помочь окно Object Browser. Далее мы сначала рассмотрим простые временные шкалы анимационных эффектов без ключевых кадров, а затем те, в которых используются ключевые кадры. Временные шкалы без ключевых кадров Временные шкалы без ключевых кадров представлены классами DoubleAnimation, PointAnimation и ColorAnimation. У всех них есть свойства, имена которых выглядят идентично, а вот тип варьируется в соответствии с типом самой временной шкалы (обратите внимание на то, что все свойства длительности задаются в XAML-коде в следующем формате: [дни. ]часы:минуты:секунды). В табл. 34.2 приведено краткое описание доступных свойств. Таблица 34.2. Свойства DoubleAnimation, PointAnimation, and ColorAnimation Свойство Описание Name Позволяет указывать имя для временной шкалы, чтобы на нее можно было ссылаться в других местах BeginTime Позволяет указывать, сколько времени должно проходить после инициации раскадровки перед запуском временной шкалы Duration Позволяет указывать, сколько времени должна действовать временная шкала AutoReverse Позволяет указывать то, должна временная шкала возвращаться в исходное положение по завершении воспроизведения, и должны ли для ее свойств снова устанавливаться исходные значения. Принимает одно из булевских значений (true или false)
1124 Часть V. Дополнительные технологии Окончание табл. 34.2 Свойство Описание RepeatBehavior FillBehavior SpeedRatio From To By Позволяет указывать, сколько раз временная шкала должна повторять свои действия. Путем ввода целого числа со следующим за ним символом х (например, 5х) можно делать так, чтобы она повторяла свои действия соответствующее этому числу количество раз, а установкой в Forever — чтобы она повторяла их до тех пор, пока не будет временно или полностью приостановлено воспроизведение всей раскадровки Позволяет указывать, как временная шкала должна вести себя в случае завершения выполнения своих действий до остановки воспроизведения раскадровки. Путем установки этого свойства в Hold можно сделать так, чтобы у свойств временной шкалы оставались значения, которые у них были на момент завершения ею выполнения своих действий (именно это значение и используется по умолчанию), а установкой в stop — чтобы им возвращались их исходные значения Управляет скоростью воспроизведения анимационного эффекта относительно значений, указанных в других свойствах. По умолчанию имеет значение 1, но при желании это значение можно изменять в другом коде и тем самым увеличивать или замедлять скорость воспроизведения анимационных эффектов Позволяет указывать первоначальное значение, которое должно устанавливаться для свойства в начале воспроизведения анимационного эффекта. В случае не указания здесь никакого значения использоваться будет текущее значение свойства Позволяет указывать окончательное значение, которое должно устанавливаться для свойства в конце воспроизведения анимационного эффекта. В случае не указания здесь никакого значения использоваться будет текущее значение свойства Позволяет делать так, чтобы анимационный эффект воспроизводился от текущего значения свойства до значения, равного сумме текущего значения и значения, указанного в этом свойстве. Может использоваться как само по себе, так и в сочетании со свойством From Например, приведенная ниже временная шкала будет анимировать свойство Width элемента управления Rectangle со значением MyRectangle в свойстве Name между значениями 100 и 200 на протяжении 5 секунд: <Storyboard x:Key="RectangleExpanderM> <DoubleAnimation Storyboard.TargetName="MyRectangle" Storyboard.TargetProperty="Width" Duration=0:00:05" From=00" To=00" /> </Storyboard> Временные шкалы с ключевыми кадрами Временные шкалы с ключевыми кадрами (key frame) представлены классами DoubleAnimationUsingKeyFrames, PointAnimationUsingKeyFrames и ColorAnimation UsingKeyFrames. Эти классы имеют все те же свойства, что и классы из предыдущего раздела, кроме From, To и By. Вместо них предусмотрено свойство KeyFrames, которое представляет собой коллекцию объектов ключевых кадров. Эти временные шкалы могут содержать любое количество ключевых кадров, каждый из которых может заставлять анимируемое значение вести себя по-другому.
Глава 34. Windows Presentation Foundation 1125 Доступны три типа ключевых кадров для каждого типа временной шкалы. □ Дискретный (discrete). Дискретный ключевой кадр заставляет анимируемое значение перепрыгивать на указанное значение безо всяких переходов. □ Линейный (linear). Линейный ключевой кадр заставляет анимируемого значение достигать указанного значения с использованием линейного перехода. □ Сплайновый (spline). Сплайновый ключевой кадр заставляет анимируемое значение достигать указанного значения с использованием нелинейного перехода, задаваемого кубической функцией кривой Безье. Следовательно, всего существует девять типов объектов ключевого кадра: DiscreteDoubleKeyFrame LinearDoubleKeyFrame SplineDoubleKeyFrame DiscreteColorKeyFrame LinearColorKeyFrame SplineColorKeyFrame DiscretePointKeyFrame LinearPointKeyFrame SplinePointKeyFrame Классы ключевых кадров имеют те же три свойства, что и классы временных шкал, рассмотренные в предыдущем разделе, но у классов сплайновых ключевых кадров еще также есть одно дополнительное свойство (табл. 34.3). Таблица 34.3. Свойства классов ключевых кадров Свойство Описание Name Позволяет указывать имя для ключевого кадра, чтобы на него можно было ссылаться в других местах KeyTime Позволяет задавать месторасположение ключевого кадра в виде промежутка времени, который должен пройти после запуска воспроизведения временной шкалы value Позволяет указывать значение, которое должно быть достигнуто или установлено при достижении данного ключевого кадра KeySpline Позволяет задавать два набора из двух чисел в формате cplx, cply cp2x, cp2y для определения кубической функции Безье, которая должна использоваться для анимации свойства. (Только для сплайновых ключевых кадров.) Например, анимировать позицию элемента управления Ellipse в квадрате путем анимации его свойства Center, которое относится к типу Point, можно было бы следующим образом: Otoryboard x:Key="EllipseMover"> <PointAnimationUsingKeyFrames Storyboard.TargetName="MyE11ipse" Storyboard.TargetProperty="Center" RepeatBehavior="Forever> <LinearPomtKeyFrame KeyTime=0:00:00" Value=0,50" /> <LinearPointKeyFrame KeyTime=0:00:01" Value=00,50" /> <LinearPointKeyFrame KeyTime=0:00:02" Value=00,100" /> <LinearPointKeyFrame KeyTime=0:00:03" Value=0,100" /> <LinearPointKeyFrame KeyTime=0:00:04" Value=0,50" /> </PointAnimationUsingKeyFrames> </Storyboard> Значения точек указываются в XAML-коде в формате х, у. Статические и динамические ресурсы Еще одним замечательным средством WPF является возможность определять ресурсы вроде стилей и шаблонов элементов управления, и затем использовать их повторно в других частях приложения. Их можно применять и во множестве других приложений, при условии определения в правильном месте.
1126 Часть V. Дополнительные технологии Определяются ресурсы как элементы в объекте ResourceDictionary. Уже по самому названию этого объекта понятно, что он представляет собой коллекцию объектов с ключами. Именно поэтому в приведенных до сих пор примерах кода при определении ресурсов использовались атрибуты х:Кеу для указания ассоциируемого с ресурсом ключа. Получать доступ к объектам ResourceDictionary можно в разных местах. Они могут размещаться как в локальном элементе управления, окне или приложении, так и во внешней сборке. Ссылаться на ресурсы можно двумя способами: статически и динамически. Обратите внимание, что сам ресурс от этого другим не становится, т.е. он не определяется как статический или динамический. Разница заключается лишь в том, как он используется. Статические ресурсы Статические ресурсы применяются тогда, когда точно известно, каким должен быть ресурс во время проектирования, и что по истечении жизненного цикла приложения изменять ссылку на него точно не потребуется. Например, в случае определения стиля кнопки с намерением сделать так, чтобы именно он использовался для кнопок в приложении, необходимость изменять его во время выполнения приложения вряд ли будет возникать. В ситуациях, подобных этой, лучше ссылаться на ресурс статическим образом. Кроме того, при использовании статического ресурса его тип вычисляется во время компиляции, так что производительность получается очень высокой. Для ссылки на статический ресурс применяется следующий синтаксис расширения разметки: {СтатическийРесурс имяРесурса} Например, при наличии стиля, определенного для элементов управления Button, со значением MyStyle в атрибуте х:Кеу, сослаться на него в элементе управления можно следующим образом: <Button Style="{StaticResource MyStyle}" ...>...</Button> Динамические ресурсы Свойство, определенное с одним динамическим ресурсом, во время выполнения может изменяться и использовать другой динамический ресурс. Подобное может оказаться полезным в целом ряде ситуаций. Например, иногда бывает нужно обеспечивать пользователей контролем над общей темой приложения, в случае чего требуется, чтобы ресурсы выделялись динамически. Кроме того, иногда предугадать, какой ключ будет необходим для получения доступа к ресурсу во время выполнения, не возможно, как, например, в случае динамического подключения к сборке ресурсов, Следовательно, получается, что динамические ресурсы обеспечивают большую гибкость, чем статические. Однако у них есть один важный недостаток. Использование динамических ресурсов приводит к получению чуть большего количества накладных расходов, из-за чего их следует применять как можно реже, чтобы производительность приложений была оптимальной Синтаксис, требуемый для ссылки на ресурс динамически, очень похож на тот, что необходим для ссылки на ресурс статическим образом: {ДинамическийРе сур с имяРе сур с а} Например, при наличии стиля, определенного для элементов управления Button, со значением MyDynamicStyle, в атрибуте х:Кеу, сослаться на него в элементе управления можно так: <Button Style="{DynamicResource MyDynamicStyle}" ...>...</Button>
Глава 34. Windows Presentation Foundation 1127 Ссылка на ресурсы типа Style Ранее уже было показано, каким образом ссылаться на ресурс типа Style в элементе управления Button, как статически, так и динамически. Этот ресурс Style может находиться, например, в свойстве Resources локального элемента управления Windows, как показано ниже: <Window . . .> <Window.Resources> <Style x:Key="MyStyle" TargetType="Button"> </Style> </Window.Resources> </Window> Каждый элемент управления Button, для которого должен применяться этот ресурс, тогда должен ссылаться на него в своем свойстве Style (статически или динамически). В качестве альтернативного варианта можно сделать ресурс Style сразу глобальным для всех элементов управления данного типа, т.е. так, чтобы объект Style применялся к каждому элементу управления в приложении, который относится к указанному типу. Для этого достаточно просто опустить атрибут х: Key: <Window ...> <Wmdow. Resources> <Style TargetType="Button"> </Style> </Window.Resources> </Window> Такой подход является замечательным способом для снабжения приложений различными темами. Он позволяет определять набор глобальных стилей для разных типов элементов управления и тем самым делать так, чтобы они применялись повсюду. В последних нескольких разделах было рассмотрено много базового материала, поэтому теперь пришла пора испробовать полученные знания на практике. В следующем практическом занятии демонстрируется применение триггеров и анимационных эффектов, а также определение стиля как глобального и пригодного для многократного использования ресурса путем изменения элемента управления Button, который рассматривался в предыдущем практическом занятии. :т ." Практическое занятие Триггеры, аНИМЭЦИОННЫе Эффекты И ресурсы 1. Создайте новое WPF-приложение по имени Сп34Ех04 и сохраните его в каталоге С:\BegVCSharp\Chapter34. 2. Скопируйте код из файла Windowl.xaml в Сп34ЕхОЗ в файл Windowl.xaml в Сп34Ех04, но измените ссылку на пространство имен в элементе Window следующим образом: <Window x:Class="Ch34Ex04.Windowl" 3. Скопируйте обработчик событий Button_Click () из Windowl. xaml. cs в Ch34Ex03 в файл Windowl .xaml .cs в Ch34Ex04.
1128 Часть V. Дополнительные технологии 4. Добавьте дочерний элемент <Window.Resources> в элемент <Window> и переместите определение <Style> из элемента <Button. Style> в элемент <Window. ResourcesX Удалите пустой элемент <Button.Style>. Ниже показан результат, который должен получиться (в сокращенной форме): <Window x:Class="Ch34Ex04.Windowl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Nasty Button" Height=50" Width=50"> <Window.Resources> <Style TargetType="Button"> </Style> </Window.Resources> <Grid Background="Black"> <Button Margin=0" Click="Button_Click"> Would anyone use a button like this? </Button> </Grid> </Window> 5. Запустите приложение и удостоверьтесь в том, что результат выглядит точно так же, как в предыдущем примере. 6. Добавьте атрибуты Name в основной элемент Grid внутри шаблона и элемент Rectangle, в котором содержится элемент ContentPresenter, как показано ниже: <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid Name="LayoutGrid"> <Grid.ColumnDefinitions> <Grid Grid.Column="l"> <Rectangle RadiusX=0" RadiusY=0" Name="BackgroundRectangle"> <Rectangle.Fill> </Rectangle.Fill> </Rectangle> </Grid> </Grid> </ControlTemplate> </Setter.Value> </Setter> 7. Добавьте в элемент <ControlTemplate>, прямо перед дескриптором </ControlTemplate>, следующий код: </Gnd> <ControlTemplate.Resources> <Storyboard x:Key="PulseButton"> <ColorAnimationUsingKeyFrames BeginTime=0:00:00" RepeatBehavior="Forever" Storyboard.TargetName="BackgroundRectangle" Storyboard.TargetProperty= "(Shape.Fill).(RadialGradientBrush.GradientStops)[1].(GradientStop.Color)">
Глава 34. Windows Presentation Foundation 1129 <LinearColorKeyFrame Value="Red" KeyTime=0:00:00" /> <LinearColorKeyFrame Value="Orange" KeyTime=0:00:01" /> <LinearColorKeyFrame Value="Red" KeyTime=0:00 : 02" /> </ColorAnimationUsingKeyFrames> </Storyboard> </ControlTemplate.Resources> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="LayoutGrid" Property="BitmapEffect"> <Setter.Value> <OuterGlowBitmapEffect GlowColor="Red" GlowSize=0" /> </Setter.Value> </Setter> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName='LayoutGrid" Property="BitmapEffect"> <Setter.Value> <OuterGlowBitmapEffect GlowColor="Yellow" GlowSize=0" /> </Setter.Value> </Setter> </Trigger> <EventTrigger RoutedEvent="UIElement,MouseEnter"> <BeginStoryboard Storyboard="{StaticResource PulseButton}" x:Name="PulseButton_BeginStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="UIElement.MouseLeave"> <StopStoryboard BeginstoryboardName="PulseButton_BeginStoryboard" /> </EventTrigger> </ControlTemplate.Triggers> </ControlTemplate> 8. Запустите приложение и наведите курсор мыши на кнопку, как показано на рис. 34.17. Кнопка начнет пульсировать и светиться. [ f NMtyftffiOA |г-,|ДЧИИП Рис. 34.17. Внешний вид кнопки при наведении на нее курсора мыши 9. Щелкните на кнопке, как показано на рис. 34.18. Стиль подсветки изменится. Рис. 34.18. Внешний вид кнопки после щелчка на ней
1130 Часть V. Дополнительные технологии Описание полученных результатов В этом примере мы сделали две вещи. Во-первых, мы определили глобальный ресурс, который должен использоваться для форматирования всех кнопок в приложении (в нем присутствует только одна кнопка, но это не важно). Во-вторых, мы добавили в стиль, созданный в предыдущем практическом занятии, некоторые функциональные возможности, чем сделали его практически заслуживающим уважения. В частности, мы заставили его светиться и пульсировать в ответ на наведение курсора мыши и выполнение щелчка. Для превращения стиля в глобальный ресурс мы просто переместили элемент <Style> в раздел ресурсов элемента управления Window. Мы могли бы добавить атрибут х: Key, но благодаря тому, что мы этого не сделали, не потребовалось устанавливать свойство Style элемента управления Button на странице; стиль сразу же стал глобальным. После превращения стиля в ресурс мы продолжили вносить в него изменения. Сначала мы добавили для двух из элементов управления в нем атрибуты Name. Это потребовалось для того, чтобы можно было ссылаться на них в другом коде, а именно—в коде анимационных эффектов и триггеров для шаблона элемента управления, являющегося частью стиля. Затем мы добавили анимационный эффект в виде локального ресурса для шаблона элемента управления, указанного в стиле. Объект Storyboard мы определили с использованием для х:Кеу значения PulseButton: <ControlTemplate.Resources> <Storyboard x:Key="PulseButton"> Внутри объекта Storyboard мы добавили элемент ColorAnimationUsingKeyFrames для анимации цвета, используемого в шаблоне элемента управления. Подлежащим анимации свойством стал красный цвет, используемый в качестве внешнего цвета в радиальной заливке внутри элемента управления ВасkgroundRectangle. Для определения местонахождения этого свойства из элемента управления потребовался довольно сложный синтаксис в свойстве Storyboard.TargetProperty: <ColorAnimationUsingKeyFrames BeginTime=0:00:00" RepeatBehavior="Forever" Storyboard.TargetName="BackgroundRectangle" Storyboard.TargetProperty= "(Shape.Fill).(RadialGradientBrush.GradientStops)[1].(GradientStop.Color)"> В состав временной шкалы данного анимационного эффекта вошли три ключевых кадра для переключения цвета с красного (Red) на оранжевый (Orange), а затем обратно, через две секунды с момента запуска: <LinearColorKeyFrame Value="Red" KeyTime=0:00:00" /> <LinearColorKeyFrame Value="Orange" KeyTime=0:00:01" /> <LinearColorKeyFrame Value="Red" KeyTime=0:00:02" /> </ColorAnimationUsingKeyFrames> </Storyboard> </ControlTemplate.Resources> Добавление анимационного эффекта в качестве ресурса не приводит к его выполнению. Поэтому далее, чтобы обеспечить такое поведение, мы добавили два триггера EventTrigger: <EventTrigger RoutedEvent="UIElement.MouseEnter"> <BeginStoryboard Storyboard="{StaticResource PulseButton}" x:Name="PulseButton_BeginStoryboard" /> </EventTrigger>
Глава 34. Windows Presentation Foundation 1131 <EventTrigger RoutedEvent="UIElement.MouseLeave"> <StopStoryboard BeginstoryboardName="PulseButton_BeginStoryboard" /> </EventTrigger> В этом коде для управления выполнением анимационного эффекта используются события MouseEnter и MouseLeave, унаследованные от базового класса UIElement элемента управления Button. Событие MouseEnter заставляет анимационный эффект запускаться через элемент BeginStoryboard, а событие MouseLeave — останавливаться через элемент StopStoryboard. Важно обратить внимание на то, что местонахождение ресурса раскадровки устанавливается с помощью ссылки на статический ресурс. Здесь в этом есть смысл, потому что объект Storyboard определен локально в элементе управления и намерений изменять его во время выполнения нет. Еще мы определили два других триггера для обеспечения подсвечивания кнопки при наведении на нее курсора и выполнении на ней щелчка за счет использования растрового эффекта OuterGlowBitmapEf f ect. Чтобы достичь этого, мы воспользовались такими уже описанными ранее в главе свойствами, как IsMouseOver и IsPressed: <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="LayoutGrid" Property="BitmapEffect"> <Setter.Value> «DuterGlowBitmapEffect GlowColor="Red" GlowSize=0" /> </Setter.Value> </Setter> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName="LayoutGrid" Property="BitmapEffect"> <Setter.Value> <OuterGlowBitmapEffect GlowColor="Yellow" GlowSize=0" /> </Setter.Value> </Setter> </Trigger> Этот код указывает, что при наведении курсора мыши кнопка должна подсвечиваться красным цветом с небольшой областью свечения, а при выполнении на ней щелчка — желтым цветом с чуть большей областью свечения. Программирование с использованием WPF Теперь, когда были рассмотрены все основные приемы программирования WPF, можно переходить к созданию собственных приложений. К сожалению, здесь не хватит места, чтобы описать другие замечательные функциональные возможности WPF, вроде способов привязки данных и форматирования изображения списков. Однако останавливаться сейчас было бы неправильно. Потому прежде чем завершить эту главу, мы предлагаем рассмотреть еще два вопроса, которые выбраны не из-за их сложности, а из-за того, что они охватывают задачи, которые вам наверняка придется наиболее часто решать в своих WPF-приложениях. □ Создание и использование собственных элементов управления. □ Реализация в элементах управления свойств зависимостей. В самом конце мы еще рассмотрим последний пример, иллюстрирующий больше приемов, чем было описано в этой главе, и даже немного деталей, касающихся выполнения привязки данных в WPF.
1132 Часть V. Дополнительные технологии Пользовательские элементы управления в WPF В WPF предлагается ряд элементов управления, которые являются полезными во многих ситуациях. Однако, как и во всех .NET-каркасах разработки, в WPF разрешено и расширять эти элементы управления, а точнее — создавать собственные элементы управления за счет наследования своих классов от тех, что входят в состав иерархии стандартных классов WPF. Одним из наиболее полезных классов, от которого можно наследовать свои классы, является UserControl. Этот класс обладает всей базовой функциональностью, которая, скорее всего, понадобится элементу управления WPF, и позволяет создаваемому элементу управления практически незаметно вливаться в существующий набор элементов WPF. Все, что можно делать со стандартными элементами управления WPF, например, создавать анимационные эффекты, задавать стили, определять шаблоны и т.д., можно также делать и с пользовательскими элементами управления. Добавлять пользовательские элементы в проект можно, выбирая в меню Project (Проект) пункт Add User Control (Добавить пользовательский элемент управления). Это приводит к отображению на экране в качестве отправной точки пустого элемента управления Canvas. Определяются пользовательские элементы с помощью высокоуровневого элемента UserControl в XAML, унаследованного от System.Windows. Controls .UserControl класса в файле отделенного кода. После добавления пользовательского элемента в проект можно приступать к добавлению элементов управления для его компоновки и отделенного кода для его настройки. Сделав это, можно начинать использовать этот элемент в различных частях приложения и даже в других приложениях. При создании пользовательских элементов управления обязательно необходимо знать о том, как реализуются свойства зависимостей. Ранее в этой главе уже говорилось о том, что свойства зависимостей являются важным средством программирования в WPF. При создании своих собственных элементов управления не использовать предоставляемые этими свойствами функциональные возможности будет просто неразумно. Реализация свойств зависимостей Свойства зависимостей можно добавлять в любой класс, который наследуется от System.Windows.DependencyObject. Этот класс присутствует в иерархии наследования многих классов WPF, в том числе и классов всех стандартных элементов управления, а также класса UserControl. Для внедрения свойства зависимости в класс в его определение добавляется общедоступный статический член типа System. Windows . DependencyProperty. Имя для этого члена можно выбирать любое, но лучше всего пользоваться принятым форматом <ИмяСвойства>Свойство: public static DependencyProperty MyStringProperty; Может показаться странным, что данное свойство определяется как статическое, ведь в конечном итоге получается свойство, которое может уникальным образом определяться для каждого экземпляра класса. Каркас свойств WPF сам следит за этими вещами, поэтому пока об этом беспокоиться не стоит. Далее добавленный член должен конфигурироваться путем применения статического метода DependencyProperty. Register (): public static DependencyProperty MyStringProperty = DependencyProperty.Register(...);
Глава 34. Windows Presentation Foundation 1133 Этот метод принимает от трех до пяти параметров, которые перечислены в табл. 34.4 (причем первые три из них являются обязательными). Таблица 34.4. Параметры, которые принимает метод DependencyProperty. Register () Параметр Описание string name Имя свойства Type propertyType Тип свойства Type ownerType Тип класса, содержащего данное свойство PropertyMetadata typeMetadata Дополнительные параметры для свойства, а именно: значение, которое должно использоваться для свойства по умолчанию, и методы обратного вызова, которые должны применяться для уведомления об изменении свойства и приведения его значения ValidateValueCallback Методы обратного вызова, которые должны использо- validateValueCallback ваться для верификации значений свойства Существуют и другие методы, которые можно использовать для регистрации свойств зависимостей, например. Register Attached (), который можно применять для реализации подключаемого свойства. В настоящей главе эти методы рассматриваться не будут, но изучить их самостоятельно совершенно не помешает. Например, свойство зависимости MyStringProperty можно было бы зарегистрировать с помощью этого метода и трех параметров следующим образом: public class MyClass : DependencyObject { public static DependencyProperty MyStringProperty = DependencyProperty.Register( "MyString", typeof(string), typeof(MyClass) ...) ; } Еще может добавляться свойство .NET для обеспечения возможности получения доступа к свойствам зависимостей напрямую (хотя, как будет показано позже, обязательным это не является). Из-за того, что свойства зависимостей определяются как статические, однако, использовать тот же синтаксис, что и для обычных свойств, нельзя. Для получения доступа к значению свойства зависимости необходимо применять методы, унаследованные от DependencyObject: public class MyClass : DependencyObject { public static DependencyProperty MyStringProperty = DependencyProperty.Register ( "MyString", typeof(string), typeof(MyClass) . . .) ; public string MyString { get { return (string)GetValue(MyStringProperty); } set { SetValue(MyStringProperty, value); } } }
1134 Часть V. Дополнительйые технологии Здесь методы Get Value () и SetValue (), соответственно, получают и устанавливают значение свойства зависимости MyStringProperty для текущего экземпляра. Эти два метода являются общедоступными, так что клиентский код может использовать их напрямую для манипулирования значениями свойств зависимостей. Именно поэтому добавление соответствующего .NET-свойства для обеспечения возможности получения прямого доступа к свойству зависимости не является обязательным. Если требуется указать для свойства метаданные, тогда нужно использовать объект, унаследованный от PropertyMetadata, например, FrameworkPropertyMetadata, и передавать его в виде четвертого параметра методу Register(). У конструктора FrameworkPropertyMetadata имеется 11 перегрузок и каждая из них принимает один или более из параметров, перечисленных в табл. 34.5. Таблица 34.5. Параметры, которые принимает конструктор FrameworkPropertyMetadata Тип параметра Описание object defaultValue FrameworkPropertyMetadataOptions flags PropertyChangedCallback propertyChangedCallback CoerceValueCallback coerceValueCallback bool isAnimationProhibited UpdateSourceTrigger defaultUpdateSourceTrigger Значение, которое должно использоваться для свойства по умолчанию Комбинация флагов (из перечисления Framework PropertyMetadataOptions), которые можно использовать для указания дополнительных метаданных для свойства. Например, флаг Af f ectsArrange служит для объявления о том, что внесение изменений в свойство может отражаться на компоновке элемента управления. Это будет вынуждать отвечающий за компоновку механизм окна заново просчитывать компоновку элемента управления в случае изменения свойства. Полный перечень доступных для этого параметра опций можно найти в документации MSDN Метод обратного вызова, который должен использоваться при изменении значения свойства Метод обратного вызова, который должен использоваться при приведении значения свойства Указывает, может ли данное свойство изменяться в результате применения анимационного эффекта Когда значения свойств привязываются к данным, это свойство определяет то, когда должно происходить обновление источника данных, в соответствии со значениями из перечисления UpdateSourceTrigger. По умолчанию для него устанавливается значение PropertyChanged, указывающее, что обновление источника привязки должно происходить сразу же при изменении свойства. Такое поведение подходит не для всех случаев; например, свойство TextBox.Text требует, чтобы для этого свойства использовалось значение Lost Focus. Оно в его случае гарантирует, что обновление источника привязки не будет происходить преждевременно. Еще для этого свойства можно использовать значение Epiicit, которое указывает, что обновление источника привязки должно осуществляться только по требованию (т.е. при вызове метода updateSource () класса, унаследованного от DependancyObj ect)
Глава 34. Windows Presentation Foundation 1135 В качестве простого примера FrameworkPropertyMetadata можно применить просто для указания значения, которое должно использоваться для свойства по умолчанию: public class MyClass : DependencyObject { public static DependencyProperty MyStringProperty = DependencyProperty. Register( "MyString", typeof(string), typeof(MyClass), new FrameworkPropertyMetadata ("Default value") // значение по умолчанию ...) ; } Ранее уже было рассказано о трех методах обратного вызова, которые можно применять для уведомления об изменении значения свойства, для приведения значения свойства и для верификации значения свойства. Эти методы, подобно самому свойству зависимости, должны обязательно реализоваться в виде общедоступных, статических методов. У каждого из них имеется конкретный возвращаемый тип и список параметров, которые нужно использовать. В следующем практическом занятии демонстрируется пример создания и применения пользовательского элемента управления с двумя свойствами зависимостей. В нем можно будет увидеть, как конкретно реализуются методы обратного вызова для этих свойств в коде пользовательского элемента управления. 1. Создайте новое WPF-приложение по имени Ch34Ex05 и сохраните его в каталоге С:\BegVCSharp\Chapter34. 2. Добавьте в это приложение новый пользовательский элемент управления Card и измените код в представляющем его файле Card.xaml следующим образом: <UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Ch34Ex05.Card" Height=50" Width=00" x:Name="UserControl" FontSize=6" FontWeight="Bold"> <UserControl.Resources> <DataTemplate x:Key="SuitTemplate"> <TextBlock Text="{Binding}" /> </DataTemplate> </UserControl.Resources> <Grid> <Rectangle Stroke="{x:Null}" RadiusX=2.5" RadiusY=2.5"> <Rectangle.Fill> <LinearGradientBrush EndPoint=.47,-0.167" StartPoint=.86,0.92"> <GradientStop Color="#FFDlC78F" Offset=" /> <GradientStop Color="#FFFFFFFF" 0ffset="l" /> </LinearGradientBrush> </Rectangle.Fill> <Rectangle.BitmapEffect> <DropShadowBitmapEffect /> </Rectangle.BitmapEffect> </Rectangle>
1136 Часть V. Дополнительные технологии <Label x:Name="SuitLabel" Content="{Binding Path=Suit, ElementName=UserControl/ Mode=Default}" ContentTemplate="{DynamicResource SuitTemplate}" HorizontalAlignment=uCenter" VerticalAlignment="Center" Margin="8,51,8,60" /> <Label x:Name="RankLabel" Content="{Binding Path=Rank, ElementName=UserControl, Mode=Default}" ContentTemplate="{DynamicResource SuitTemplate}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="8,8,0,0" /> <Label x:Name="RankLabelInverted" Content="{Binding Path=Rank, ElementName=UserControl, Mode=Default}" ContentTemplate="{DynamicResource SuitTemplate}" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin=,0,8,8" RenderTransformOrigin=.5,0.5"> <Label.RenderTransform> <RotateTransform Angle=80" /> </Label.RenderTransform> </Label> <Path Fill="#FFFFFFFF" Stretch="Fill" Stroke="{x:Null}" Margin=,0,35.218,-0.077" Data="Fl MHO.5,51 L123 .16457, 51 СПб.5986, 76.731148 115.63518,132.69684 121.63533,149.34013 133.45299, 182.12018 152.15821,195.69803 161.79765,200.07669 LllO.5,200 C103.59644, 200 98,194.40356 98,187.5 L98,63.5 C98,56.596439 103.59644,51 110.5,51 z"> <Path.OpacityMask> <LinearGradientBrush EndPoint=.957,1.127" StartPoint=,-0.06"> <GradientStop Color="#FF000000" Offset=" /> <GradientStop Color="#00FFFFFF" Offset="l" /> </LinearGradientBrush> </Path.OpacityMask> </Path> </Grid> </UserControl> 3. Измените код в Card.xaml. cs, как показано ниже: public partial class Card : UserControl { public static string[] Suits = { "Club", "Diamond", "Heart", "Spade" }; public static DependencyProperty SuitProperty = DependencyProperty.Register( "Suit", typeof(string), typeof(Card), new PropertyMetadata ("Club", new PropertyChangedCallback(OnSuitChanged)), new ValidateValueCallback(ValidateSuit) ); public static DependencyProperty RankProperty = DependencyProperty.Register( "Rank", typeof(int) , typeof(Card) , new PropertyMetadataA) , new ValidateValueCallback(ValidateRank) ); public Card() { InitializeComponent(); }
Глава 34. Windows Presentation Foundation 1137 public string Suit get { return (string)GetValue(SuitProperty); } set { SetValue(SuitProperty, value); } public int Rank get { return (int)GetValue(RankProperty); } set { SetValue(RankProperty, value); } public static bool ValidateSuit(object suitValue) string suitValueString = (string)suitValue; if (suitValueString != "Club" && suitValueString != "Diamond" && suitValueString != "Heart" && suitValueString != "Spade") { return false; } return true; } public static bool ValidateRank(object rankValue) { int rankValuelnt = (int)rankValue; if (rankValuelnt < 1 I I rankValuelnt > 12) { return false; } return true; } private void SetTextColor () { if (Suit == "Club" | | Suit == "Spade") I RankLabel.Foreground = new SolidColorBrush(Color.FromRgb@, 0, 0) ) ; SuitLabel.Foreground = new SolidColorBrush(Color.FromRgb@, 0, 0) ) ; RankLabelInverted.Foreground = new SolidColorBrush(Color.FromRgb@, 0, 0) ) ; } else { RankLabel.Foreground = new SolidColorBrush(Color.FromRgbB55, 0, 0) ) ; SuitLabel.Foreground = new SolidColorBrush(Color.FromRgbB55, 0, 0) ) ; RankLabellnverted.Foreground = new SolidColorBrush(Color.FromRgbB55, 0, 0)); } } public static void OnSuitChanged(DependencyObject source, DependencyPropertyChangedEventArgs args) { ((Card)source) .SetTextColor (); } } 4. Измените код в Windowl .xaml следующим образом: <Window x:Class="Ch34Ex05.Windowl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Card Dealer" Height=00" Width="800V
1138 Часть V. Дополнительные технологии <Grid Name="contentGrid" MouseLeftButtonDown="Grid_MouseLeftButtonDown" MouseLeftButtonUp="Grid_MouseLeftButtonUp" MouseMove="Grid_MouseMove"> <Grid.Background> <LinearGradientBrush EndPoint=.364,0.128" StartPoint=.598,1.042"> <GradientStop Color="#FF0D4FlA" Offset=" /> <GradientStop Color="#FF448251" Offset="l" /> </LinearGradientBrush> </Grid.Background> </Grid> </Window> 5. Измените код в Window 1. xaml. cs, как показано ниже: public partial class Windowl : Window { private Card currentCard; private Point offset; private Random random = new Random(); public Windowl () { InitializeComponent(); } private void Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.Source is Card) { currentCard = (Card)e.Source; offset = Mouse.GetPosition(currentCard); contentGrid.Children.Remove(currentCard); } else { currentCard = new Card { Suit = Card.Suits[random.Next@, 4)], Rank = random.Next A, 13) }; currentCard.HorizontalAlignment = HorizontalAlignment.Left; currentCard.VerticalAlignment = VerticalAlignment.Top; offset = new Point E0, 75); } contentGrid.Children.Add(currentCard); PositionCardO ; } private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { currentCard = null; } private void Grid_MouseMove(object sender, MouseEventArgs e) { if (currentCard != null) { PositionCardO ; } }
Глава 34. Windows Presentation Foundation 1139 private void PositionCard() { Point mousePos = Mouse.GetPosition(this); currentCard.Margin = new Thickness(mousePos.X - offset.X, mousePos.Y - offset.Y, 0, 0) ; } } 6. Запустите приложение. Для добавления случайных карт щелкайте на поверхности окна, а для перемещения карт щелкайте на них и перетаскивайте. При выполнении щелчка на существующей карте она должна начинать отображаться поверх всех остальных, как показано на рис. 34.19. Рис. 34.19. Приложение Ch34Ex05 в действии Описание полученных результатов В этом примере был создан пользовательский элемент управления с двумя свойствами зависимостей, а также добавлен клиентский код для тестирования этого элемента. Он позволил охватить много базовых приемов, но начинать рассмотрение кода все равно надо с элемента управления Card. Элемент управления Card состоит в основном из кода, который должен выглядеть уже знакомо, благодаря коду, который приводился ранее в этой главе. В коде компоновки не используется ничего нового, хотя нельзя не заметить, что результат выглядит привлекательнее той неуклюжей кнопки, которая присутствовала в двух предыдущих примерах. Одной совершенно новой деталью при этом является то, что в данном элементе управления применяется немного кода для привязки данных. Привязка данных подразумевает связывание свойства элемента управления с источником данных и потому предусматривает массу различных приемов. WPF позволяет легко привязывать свойства к всевозможным источникам данных, вроде баз данных, коду XML и (как в этом примере) значений свойств зависимостей.
1140 Часть V. Дополнительные технологии В частности, код в Card предоставляет клиентскому коду доступ к двум таким свойствам зависимостей, как Suit и Rank, и привязывает эти свойства к визуальным элементам в компоновке элемента управления. В результате этого при установке Suit в Club по центру карты отображается слово Club, а в двух из ее углов— соответствующее значение Rank. Реализация свойств Suit и Rank рассматривается позже. Пока достаточно знать только то, что эти свойства принимают значения, соответственно, типа string и типа int. Можно было использовать для них, например, и перечисление, но тогда потребовалось бы добавлять немного больше нового кода, поэтому чтобы не усложнять пример, для них были выбраны эти базовые значения. Для привязки значения к свойству применяется синтаксис привязки, представляющий собой расширение разметки. Этот синтаксис подразумевает указание значения свойства в формате {Binding . . . }. Для настройки привязки предусмотрено несколько различных способов. В данном примере привязка для элемента управления SuitLabel конфигурируется следующим образом: <Label x:Name="SuitLabel" Content="{Binding Path=Suit, ElementName=UserControl, Mode=Default}" ContentTemplate="{DynamicResource SuitTemplate}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,51,8,60" /> Здесь для настройки привязки задаются три свойства: Path (представляющее имя свойства), ElementName (указывающее на элемент, к которому относится данное свойство) и Mode (указывающее, в каком режиме должна выполняться привязка). Свойства Path и ElementName довольно просты, а на свойство Mode можно пока не обращать внимания. Главное заключается в том, что такая спецификация приводит к привязке свойства Label. Content к свойству Card. Suit. При привязке значений свойств нужно еще обязательно указывать, как должно визуализироваться привязанное содержимое, применяя шаблон данных. В данном примере используется шаблон данных по имени SuitTemplate, причем как динамический ресурс (хотя в данном случае его привязка к статическому ресурсу тоже бы прекрасно подошла). Его определение содержится в разделе ресурсов элемента управления и выглядит следующим образом: <UserControl.Resources> <DataTemplate x:Key="SuitTemplate"> <TextBlock Text="{Binding}" /> </DataTemplate> </UserControl.Resources> Строковое значение свойства Suit используется в качестве значения для свойства Text элемента управления TextBlock. Для двух надписей свойства Rank применяется это же самое определение DataTemplate, причем то, что в Rank содержится значение типа int, роли не играет, поскольку при привязке к свойству TextBlock.Text оно все равно преобразуется в значение string. О привязке данных и шаблонах данных можно было бы рассказать еще очень многое, но в этой книге просто не хватит места, чтобы описать все детали. В заключительной части главы будут перечислены источники, в которых можно найти дополнительную информацию. Напоследок хотелось бы обратить внимание на то, что средство Expression Blend позволяет очень эффективно привязывать данные и при этом особо не беспокоиться о синтаксисе XAML. Чтобы такая привязка данных работала, нам пришлось определить два свойства зависимостей с помощью приемов, описанных в предыдущем разделе. Их определения содержатся в файле отделенного кода пользовательского элемента управления и вы-
Глава 34. Windows Presentation Foundation 1141 глядят следующим образом (у обоих из них имеются простые оболочки свойств .NET, показывать которые здесь из-за их простоты не имеет никакого смысла): public static DependencyProperty SuitProperty = DependencyProperty.Register( "Suit", typeof(string), typeof(Card), new PropertyMetadata("Club", new PropertyChangedCallback(OnSuitChanged)), new ValidateValueCallback(ValidateSuit) ); public static DependencyProperty RankProperty = DependencyProperty.Register( "Rank", typeof(int), typeof(Card), new PropertyMetadataA) , new ValidateValueCallback(ValidateRank) ); Оба свойства зависимостей предусматривают использование метода обратного вызова для верификации значений, а свойство Suit также использует метод обратного вызова при изменении своего значения. Эти предназначенные для верификации методы обратного вызова имеют возвращаемый тип bool и один параметр типа object, представляющий значение, которое клиентский код пытается установить для свойства. Если с этим значением все в порядке, тогда методы должны возвращать true, a если нет, тогда— false. В данном примере, значение свойства Suit ограничивается одной из четырех строк: public static bool ValidateSuit(object suitValue) { string suitValueString = (string)suitValue; if (suitValueString != "Club" && suitValueString !» "Diamond" && suitValueString != "Heart" && suitValueString !» "Spade") { return false; } return true; } Показанный код довольно груб и, конечно, перечисление подошло бы здесь гораздо больше, однако оно не применялось по указанным ранее причинам. Свойство Rank тоже ограничивается значением в диапазоне от 1 (туз) до 12 (король): public static bool ValidateRank(object rankValue) { int rankValuelnt = (mt) rankValue; if (rankValuelnt < 1 I I rankValuelnt > 12) { return false; } return true; } При изменении значения Suit вызывается метод обратного вызова OnSuitChanged (). Этот метод отвечает за установку цвета текста в красный (для червей и бубен) или в черный (для пик и треф). Делает он это за счет вызова вспомогательного метода в источнике его вызова. Вызывать вспомогательный метод ему необходимо из-за того, что он реализован как статический метод, а ему в качестве параметра передается эк-
1142 Часть V. Дополнительные технологии земпляр пользовательского элемента управления, который сгенерировал событие, чтобы он мог с ним взаимодействовать. Называется этот вспомогательный метод SetTextColor(). public static void OnSuitChanged(DependencyObject source, DependencyPropertyChangedEventArgs args) { ((Card)source).SetTextColor(); } Метод SetTextColor () является приватным, но, как очевидно, он все равно доступен из метода OnSuitChanged (), поскольку они оба являются членами одного и того же класса, несмотря на то, что при этом представляют собой, соответственно, метод экземпляра и статический метод. SetTextColorO просто устанавливает свойство Foreground различных надписей элемента управления в сплошную цветную кисть, в которой используется либо черный, либо красный цвет, в зависимости от значения Suit: private void SetTextColorO { if (Suit == "Club" | | Suit == "Spade") { RankLabel.Foreground = new SolidColorBrush(Color.FromRgb@, 0, 0)); SuitLabel.Foreground = new SolidColorBrush(Color.FromRgb@, 0, 0)); RankLabellnverted.Foreground = new SolidColorBrush(Color.FromRgb@, 0, 0)); } else { RankLabel.Foreground = new SolidColorBrush(Color.FromRgbB55, 0, 0) ) ; SuitLabel.Foreground = new SolidColorBrush(Color.FromRgbB55, 0, 0)); RankLabellnverted.Foreground = new SolidColorBrush(Color.FromRgbB55, 0, 0)); } } Вот и все, на что требуется обратить внимание в коде элемента управления Card. Клиентский код, содержащийся в Window! . xaml и Windowl. xaml. cs, выглядит довольно просто. В нем применяются кое-какие базовые приемы стилизации для обеспечения фона с градиентной зеленой заливкой, а также ряд операций обработки событий (в которых участвуют как просто маршрутизируемые, так и подключаемые маршрутизируемые события) для обеспечения возможности взаимодействия с пользователем. Есть еще несколько трюков, вроде применения полей для расположения карт и сдвига для обеспечения возможности перетаскивания существующих карт из точки, в которой выполняется щелчок, но разобраться в том, как они работают, вполне можно самостоятельно, внимательно изучив код. Резюме В этой главе было рассказано обо всем, что необходимо знать для того, чтобы начать программировать с помощью WPF. Здесь также, хотя и кратко, были рассмотрены некоторые более сложные приемы, что позволит хотя бы немного заглянуть в мир возможностей, которые открывают более развитые методики программирования в WPF. Данная тема является слишком обширной для того, чтобы ее можно было осветить в одной главе, и потому те, кого она заинтересовала, наверняка захотят заглянуть в дополнительные посвященные ей источники. Рекомендуем обратиться к полноценному источнику сведений о WPF и применении XAML в Web-среде — книге WPF: Windows Presentation Foundation в .NET 3.0 для профессионалов (ИД "Вильяме", 2007 г.).
Глава 34. Windows Presentation Foundation 1143 Конечно, кто-то может предпочесть просто поэкспериментировать с доступными средствами, например, с Expression Blend, и изучить доступные возможности. Документация MSDN тоже поможет, но из-за того, что WPF все еще является слишком новой технологией, в ней встречаются заметные пробелы, для восполнения которых может потребоваться заглянуть в другие ресурсы. Также существует несколько замечательных Web-сайтов, на которых тоже можно попробовать поискать дополнительную информацию, среди них сайт сообщества любителей WPF, расположенный по адресу http: //wpf. netf хЗ . com/, и блог Скотта Гатри (Scott Guthrie), доступный по адресу http: //weblogs .asp.net/scottgu. В этой главе рассмотрены следующие вопросы. □ Что собой представляет технология WPF и какое влияние она может потенциально оказать на разработку настольных и Web-приложений. □ Каким образом структура WPF позволяет дизайнерам и разработчикам трудиться над проектами вместе путем применения как Expression Blend, так и VS или VCE. □ Что собой представляет язык XAML и как выглядит его базовый синтаксис, и какие термины в нем используются. □ Как используется объект Application. □ Каким образом в WPF работают элементы управления, и в том числе, что собой представляют зависимые и подключаемые свойства, а также маршрутизируемые и подключаемые события. □ Каким образом в WPF работает система компоновки и как для размещения элементов управления можно использовать различные компоновочные контейнеры. □ Как применять стили и шаблоны для настройки внешнего вида и поведения элементов управления. □ Как с помощью триггеров и анимационных эффектов можно улучшать впечатление пользователя от работы с приложением. □ Как определять ресурсы во внутренних и внешних словарях ресурсов и получать к ним доступ статическим и динамическим образом. □ Как создавать пользовательские элементы управления со свойствами зависимостей. В следующей главе речь пойдет об еще одной новой технологии в .NET 3.0 и 3.5 — Windows Communication Foundation (WCF). Упражнения 1. Для настольных и браузерных WPF-приложений можно использовать абсолютно одинаковый XAML-код. Верно это или нет? 2. Какой прием вы бы использовали для обеспечения дочерних элементов управления возможностью устанавливать отдельные значения для свойства, определенного в родительском элементе? Как бы выглядел требуемый для этого XAML- синтаксис? Напишите XAML-код, иллюстрирующий установку двумя дочерними элементами управления Branch (Ветка) различных значений для свойства Leaf Count (Количество листьев), определенного в родительском элементе Tree (Дерево).
1144 Часть V. Дополнительные технологии 3. Какое из приведенных ниже утверждений о свойствах зависимостей является верным? • Свойства зависимостей должны обязательно быть доступны через соответствующее свойство .NET. • Свойства зависимостей определяются в виде общедоступных, статических членов. • В каждом определении класса допускается иметь только одно свойство зависимости. • Свойства зависимостей нужно именовать с использованием синтаксиса [ИмяСвойства]Свойство. • Проверять правильность значений, присваиваемых свойствам зависимостей, можно с помощью метода обратного вызова. 4. Какой компоновочный элемент управления вы бы использовали для отображения элементов управления в виде одной строки или столбца? 5. Туннельные события в WPF именуются особым образом, чтобы их можно было распознавать. Как именно выглядит необходимый синтаксис? 6. Свойства каких типов могут снабжаться анимационными эффектами? 7. В каких случаях лучше использовать ссылку на динамический, а не на статический ресурс?
35 Windows Communication Foundation В главе 21 рассматривались Web-службы и то, как их можно использовать для обеспечения простой связи между приложениями. В ней было показано, как для обмена данным с Web-службой применять HTTP-запросы GET и POST и как использовать протокол SOAP. По прошествии нескольких лет после того, как Web-службы были впервые предоставлены .NET-разработчикам, стало очевидно, что хотя они и являются замечательными, существует возможность их расширения. Для решения этой проблемы Microsoft выпустила дополнительный пакет под названием WSE (Web Service Enhancements— расширения для Web-служб). Он позволил разработчикам Web-служб добавлять средства защиты для сообщений, механизмы маршрутизации и другие различные политики для улучшения Web-служб. Однако возможность для еще большего усовершенствования все равно оставалась. Другая .NET-технология, а именно— технология удаленного взаимодействия (Remoting), позволяет создавать экземпляры объектов в одном процессе и использовать их в другом процессе. Она позволяет делать это, даже если объект находится на компьютере, отличном от того, на котором он используется. Эта технология, однако, хотя и является замечательным усовершенствованием по сравнению с предыдущими технологиями вроде DCOM, все равно имеет свои проблемы. В частности, она является ограниченной и далеко не самой простой для изучения начинающими программистами. Технология Windows Communication Foundation (WCF), по сути, заменяет собой и технологию Web-служб, и технологию удаленного взаимодействия. Она объединяет в себе понятия наподобие служб и не зависящего от платформы обмена SOAP-сообще- ниями из технологии Web-служб, и понятия вроде серверных приложений и усовершенствованных возможностей привязки из технологии удаленного взаимодействия. В результате получается технология, которая представляет собой более совершенный набор, включающий и возможности технологии Web-служб, и возможности технологии удаленного взаимодействия, но только при этом является гораздо более мощной,
1146 Часть V. Дополнительные технологии чем Web-службы, и гораздо более простой в плане использования, чем удаленное взаимодействие. С применением WCF можно создавать приложения самой разной степени сложности, начиная от простых приложений и заканчивая приложениями с ориентированной на службы архитектурой (Service-Oriented Acrchitecture — SOA). Такая архитектура подразумевает децентрализацию обработки и использование распределенной обработки за счет подключения к службам и данным по мере возникновения в них необходимости через локальную сеть или Internet. В настоящей главе речь пойдет о принципах WCF и том, как можно создавать и использовать службы WCF из кода приложений. В частности, здесь будут освещены следующие вопросы. □ Что собой представляет WCF. □ Концепции WCF. □ Программирование с использованием WCF Создавать WCF-службы в VCE нельзя, их можно создавать только в полной версии VS. В версии Visual Web Developer 2008 Express Edition, правда, еще можно создавать службы WCF с сервером IIS в роли хоста, но в этой главе для охвата всех возможных вариантов будет применяться полная версия VS. Что собой представляет WCF WCF представляет собой технологию, которая позволяет создавать службы с возможностью получения к ним доступа как из других приложений, так и из процессов, компьютеров и даже сетей. С помощью таких служб можно и организовывать общий доступ к каким-то функциональным возможностям для нескольких приложений, и предоставлять источники данных, и абстрагировать сложные процессы. Как и в случае Web-служб, функциональные возможности, которые предлагают WCF-службы, инкапсулируются в отдельные предоставляемые службой методы. Каждый такой метод— или, согласно терминологии WCF, каждая такая операция— обладает конечной точкой (endpoint), с которой нужно взаимодействовать для того, чтобы использовать данный метод. В этом моменте службы WCF и Web-службы отличаются. В случае Web-служб взаимодействовать с конечной точкой можно только по протоколу SOAP поверх HTTP. В случае WCF-служб на выбор доступно несколько протоколов. Допускается даже иметь конечные точки, способные работать посредством более одного протокола, в зависимости от сети, через которую осуществляется подключение к службе, и специфических требований. В WCF у конечной точки может быть несколько привязок (bindings), в каждой из которых указываются применяемые средства коммуникации. В привязках может также указываться и дополнительная информация, вроде требований, соблюдение которых является обязательным для взаимодействия с данной конечной точкой. Привязка может требовать, например, аутентификации имени пользователя и пароля или маркера (token) учетной записи пользователя Windows. При подключении к конечной точке протокол, на который ссылается привязка, влияет на используемый адрес, как будет показано чуть позже. После подключения с конечной точкой можно взаимодействовать через SOAP-co- общения. Внешний вид этих сообщений зависит от того, какая операция используется, и какие структуры данных требуются для отправки и получения сообщений в и из этой операции. В WCF для указания всего это применяются контракты (contracts), обнаруживать которые можно посредством обмена метаданными со службой. Они напоминают WSDL-документы, которые применяются в Web-службах для описания их
Глава 35. Windows Communication Foundation 1147 функциональных возможностей. На самом деле информацию о WCF-службах тоже можно получать в формате WSDL, хотя для описания их функциональности доступны и другие способы. После определения службы и конечной точки, которую требуется использовать, а также привязки и контрактов, которых нужно придерживаться, с WCF-службой можно взаимодействовать так же легко, как и с объектом, который был определен локальным образом. Процессы взаимодействия с WCF-службами могут иметь вид как простых, односторонних транзакций или сообщений с запросами и ответами, так и полнодуплексных сеансов связи, инициируемых на одном из концов канала связи. Еще также можно применять и технологии оптимизации связанных с передачей сообщений показателей полезной нагрузки, вроде механизма МТОМ (Message Transmission Optimization Mechanism— механизм оптимизации процессов передачи сообщений), для упаковки данных, если таковая требуется. Сама служба WCF может выполняться на компьютере-хосте (т.е. на компьютере, где она размещается) в одном из ряда различных процессов. В отличие от Web-служб, которые всегда выполняются в процессе IIS, для WCF-служб можно выбирать для хостинга такой процесс, который больше всего подходит в данной ситуации. Для хостинга WCF-служб можно применять IIS, но еще также можно использовать и службы Windows или исполняемые файлы. В случае применения TCP для взаимодействия с WCF-службой по локальной сети нет никакой необходимости даже устанавливать IIS на том компьютере, который отвечает за хостинг данной службы. Дизайн каркаса WCF позволяет настраивать практически все детали, о которых было рассказано в этом разделе. Данная тема, однако, является сложной, и потому в настоящей главе будут демонстрироваться способы применения только тех механизмов, которые предлагаются в .NET 3.5 по умолчанию. Теперь, когда мы рассказали об основных составляющих WCF-служб, пришла пора рассмотреть эти понятия более подробно, что и будет сделано в следующих разделах. Концепции WCF В этом разделе рассматриваются следующие понятия WCF. □ Коммуникационные протоколы WCF. □ Адреса, конечные точки и привязки. □ Контракты. □ Шаблоны передачи сообщений. □ Механизмы поведения. □ Хостинг. Коммуникационные протоколы Как уже упоминалось ранее, взаимодействие с WCF-службами может осуществляться посредством нескольких различных транспортных протоколов. В частности, в .NET 3.5 Framework для этого предусмотрены четыре следующих протокола. □ HTTP. Позволяет взаимодействовать с WCF-службами из любого места, включая Internet. Его можно использовать для создания WCF-служб, предназначенных для работы в Web. □ TCP. Позволяет взаимодействовать с WCF-службами как по локальной сети, так и по Internet при условии соответствующей настройки брандмауэра. Более эффективен, чем протокол HTTP, и имеет больше,возможностей, но его может быть труднее конфигурировать.
1148 Часть V. Дополнительные технологии □ Именованный канал (named pipe). Позволяет взаимодействовать с WCF-служ- бами на том же компьютере, на котором находится вызывающий код, но только в отдельном процессе. □ MSMQ. Представляет собой технологию организации очередей, которая позволяет маршрутизировать отправляемые приложением сообщения для достижения ими пункта назначения через очередь. MSMQ является надежной технологией передачи сообщений, которая гарантирует попадание сообщения в ту очередь, в которую оно было отправлено. Еще MSMQ является по своей природе асинхронной технологией, а это значит, что помещаемое в очередь сообщение будет обрабатываться только после завершения обработки всех идущих перед ним сообщений в этой очереди и появления доступа к службе обработки. Эти протоколы часто позволяют устанавливать безопасные соединения. Например, протокол HTTP позволяет применять для установки безопасного SSL-соединения по Internet протокол HTTPS. Что касается протокола TCP, то он предлагает обширные возможности для обеспечения безопасности в локальной сети за счет применения системы безопасности Windows. На рис. 35.1 показано, как эти транспортные протоколы могут подключать приложение к WCF-службам в разных месторасположениях. В настоящей главе описываются все эти протоколы, кроме MSMQ, который является более сложной темой и требует более глубокого рассмотрения. Локальная сеть Internet Рис. 35.1. Подключение приложения к WCF-службам Для установки соединения с WCF-службой, прежде всего, необходимо знать, где она находится. На практике это означает, что необходимо знать адрес ее конечной точки. Адреса, конечные точки и привязки Формат адреса, который нужно использовать для подключения к службе, зависит от лежащего в основе протокола. Формат адресов служб, в основе которых лежат те три протокола, которые описываются в настоящей главе, выглядит следующим образом.
Глава 35. Windows Communication Foundation 1149 □ HTTP. Адреса для подключений по протоколу HTTP представляют собой URL- адреса и имеют вид http: / /<серверу : <порт>/<служба>. Для установки SSL- соединений еще также может использоваться и такой формат: https: //<сервер>: <порт>/<служба> Если за хостинг службы отвечает IIS, на месте <служба> указывается файл с расширением .svc (. svc-файлы являются аналогом . asmx-файлов, которые применяются в Web-службах). Адреса IIS, скорее всего, будут включать больше подкаталогов, чем в данном примере, т.е. наверняка будут иметь больше отделенных символом / разделов перед именем файла . svc. □ TCP. Адреса для подключений по протоколу TCP имеют вид: net.tcp://<серверу:<порт>/<служба> □ Именованный канал. Адреса для подключений по именованному каналу выглядят похожим образом, но не предусматривают указания номера порта, т.е. имеют такой вид: net .pipe: //<серверу: <портУ/<службаУ. Адрес службы является базовым адресом, который можно использовать для создания адресов для конечных точек, представляющих операции. Например, адрес операции может выглядеть как net .pipe: //<серверу: <портУ/<службаУ/<операция 1у. Для примера предположим, что требуется создать WCF-службу с одной операцией, имеющей привязки для всех трех перечисленных здесь протоколов. Базовые адреса этой службы тогда могут выглядеть следующим образом: http://www.mydomain.com/services/amazingservices/mygreatservice.svc net.tcp://myhugeserver:8080/mygreatservice net.pipe://localhost/mygreatservice Адреса для ее операции тогда, соответственно, могут выглядеть так: http://www.mydomain.com/services/amazingservices/mygreatservice.svc/greatop net.tcp://myhugeserver:8080/mygreatservice/greatop net.pipe://localhost/mygreatservice/greatop В привязках, как уже упоминалось ранее, может задаваться не только транспортный протокол, который должна использовать операция. В них еще также могут указываться и требования по безопасности, которые должны соблюдаться при взаимодействии по этому транспортному протоколу, транзакционные возможности конечной точки, кодировка сообщений и многое другое. Из-за того, что привязки обладают такой замечательной степенью гибкости, в .NET Framework предлагается несколько предопределенных привязок, которые можно использовать как непосредственно в том виде, в каком они есть, так и в качестве отправных точек и затем настраивать до получения требуемого поведения. У этих предопределенных привязок имеются определенные возможности, которых нужно обязательно придерживаться. Каждая из них представлена соответствующим классом в пространстве имен System. ServiceModel. Все они перечислены в табл. 35.1. Многие из перечисленных в этой таблице классов привязок имеют схожие свойства, которые можно использовать для их дополнительной настройки. Например, у них есть свойства, которые можно применять для настройки значений тайм-аута. Более подробно об этом будет рассказываться чуть позже в этой главе при рассмотрении кода.
1150 Часть V. Дополнительные технологии Таблица 35.1. Предопределенные привязки в .NET Framework Привязка Описание BasicHttpBinding WSHttpBinding WSDualHttpBinding WSFederationHttpBinding NetTcpBinding NetNamedPipeBinding NetPeerTcpBinding NetMsmqBinding И MsmqlntegrationBinding Простейшая HTTP-привязка. Используется Web-службами по умолчанию. Имеет ограниченные возможности в плане обеспечения безопасности и не предусматривает никакой поддержки для транзакций Более усовершенствованная версия HTTP-привязки, способная применять все дополнительные функциональные возможности, которые были представлены в WSE Расширяет возможности WSHttpBinding, включая в их число возможности дуплексной связи. При дуплексной связи сервер помимо обычного обмена сообщениями может инициировать и другие сеансы связи с клиентом Расширяет возможности WSHttpBinding, включая в их число возможности федераций. Федерации позволяют третьей стороне реализовать технологию единого входа (Single Sign-On) и другие специфические меры безопасности. Данная тема является сложной и в настоящей главе не рассматривается Применяется для сеансов связи по протоколу TCP и позволяет настраивать безопасность, транзакции и так далее Применяется для сеансов связи по именованным каналам и позволяет настраивать безопасность, транзакции и т.д. Позволяет транслировать сеансы связи нескольким клиентам и представляет собой еще один сложный класс, который в настоящей главе не рассматривается Эти привязки применяются с системой MSMQ, которая тоже в настоящей главе не рассматривается Контракты Контракты позволяют определять то, как именно могут использоваться WCF-служ- бы. Они бывают нескольких видов. □ Контракт службы. Позволяет добавлять общую информацию о службе и предоставляемых ею операциях. Например, это могут быть сведения об используемом службой пространстве имен. Службы обладают уникальными пространствами имен, которые применяются при определении шаблона для SOAP-сообщений во избежание возможных конфликтов с другими службами. □ Контракт операции. Позволяет задавать, как должна использоваться операция. Это подразумевает указание параметров и возвращаемого значения для лежащего в основе операции метода, а также любой необходимой дополнительной информации, вроде того, должен ли этот метод возвращать сообщение с ответом. □ Контракт сообщений. Позволяет указывать, каким образом должна форматироваться информация внутри SOAP-сообщений, например, должны ли данные включаться в заголовок или в тело SOAP-сообщений. Подобное может быть полезно при создании WCF-службы, пригодной для интеграции с унаследованными системами.
Глава 35. Windows Communication Foundation 1151 □ Контракт неполадок. Позволяет указывать ошибки, которые может возвращать операция. При использовании .NET-клиентов такие ошибки могут приводить к генерации исключений, которые можно перехватывать и обрабатывать обычным образом. □ Контракт данных. В случае использования сложных типов, вроде определяемых пользователем структур или объектов, в качестве параметров или возвращаемых типов для операций, для них нужно обязательно определять контракты данных. Контракты данных позволяют определять типы на основе данных, которые они должны предоставлять через свои свойства. В классы служб контракты обычно добавляются путем применения соответствующих атрибутов, а каких именно — будет показываться позже в этой главе. Шаблоны передачи сообщений В предыдущем разделе было сказано о том, что в контракте операции можно указывать, должна ли данная операция возвращать какое-то значение, а в разделе, посвященном привязкам— о том, что с помощью привязки WSDualHttpBinding можно обеспечивать дуплексную связь. И то и другое является разновидностью шаблонов передачи сообщений, каковые бывают трех видов. □ Шаблон передачи сообщений запроса и ответа. Представляет собой "обычный" шаблон передачи сообщений, в котором каждое сообщение, отправляемое службе, приводит к отправке ответного сообщения обратно клиенту. Это вовсе необязательно означает, что клиент должен дожидаться ответного сообщения, поскольку операции могут, как обычно, вызываться асинхронно. □ Односторонний, или симплексный, шаблон передачи сообщений. В таком шаблоне сообщения отправляются от клиента соответствующей операции WCF, но обратно ответ не посылается. Подобное может быть удобно в ситуации, когда никакого ответа не требуется. Например, в случае создания операции WCF, приводящей к перезагрузке сервера-хоста WCF, ожидание ответа вряд ли будет желаемым или необходимым. □ Двусторонний, или дуплексный, шаблон передачи сообщений. Является более совершенным шаблоном, в котором клиент, по сути, выступает в роли сервера, а также клиента, и сервер — в роли клиента, а также сервера. После настройки он позволяет клиенту и серверу отправлять друг другу сообщения, которые могут быть, а могут и не быть ответными. Это аналогично созданию объекта и подписке на предоставляемые этим объектом события. То, как эти шаблоны передачи сообщений используются на практике, будет показано позже в этой главе. Механизмы поведения Механизмы поведения (behaviors) представляют собой способ применения дополнительной конфигурации, которая не должна предоставляться службами и операциями непосредственно клиенту. За счет снабжения механизмом поведения службы можно управлять тем, как должен создаваться экземпляр данной службы, как она должна использоваться в отвечающем за ее хостинг процессе, как должна участвовать в транзакциях, каким образом в ней должны обрабатываться проблемы, связанные с многопоточностью и т.д. За счет снабжения механизмами поведения операций можно управлять тем, должен ли во время их выполнения использоваться режим заимствования прав (impersonation), какое влияние они должны оказывать на транзакции и т.п.
1152 Часть V. Дополнительные технологии Поскольку целью этой главы является предоставление лишь базовых сведений о WCF-службах, здесь позже будут показаны лишь самые основные функциональные возможности механизмов поведения. Хостинг Во вводной части настоящей главы уже говорилось о том, что роль хоста Web- служб может выполнять несколько различных процессов. Все эти возможные кандидаты перечислены ниже. □ Web-сервер. WCF-службы, роль хоста для которых выполняет IIS, больше всего похожи на Web-службы. Однако в WCF-службах можно использовать такие дополнительные функциональные возможности и средства безопасности, которые реализовать в Web-службах очень трудно. Кроме того, их можно интегрировать со средствами IIS, например, с системой безопасности IIS. □ Исполняемая программа. В роли хоста для WCF-службы можно использовать приложения любого из тех типов, которые можно создавать в .NET, т.е. и консольные приложения, и приложения Windows Forms, и приложения WPF. □ Служба Windows. В роли хоста для WCF-службы можно также использовать и службы Windows, а вместе с ними и предоставляемые ими полезные функциональные возможности, вроде возможности автоматического запуска и возможности восстановления после сбоев. □ Служба активации Windows (Windows Activation Service — WAS). Эта служба предназначена специально для хостинга WCF-служб и, по сути, представляет собой простую версию IIS, которой можно пользоваться в тех случаях, когда сервер IIS не доступен. Два варианта в этом списке — IIS и WAS — предоставляют для WCF-служб полезные возможности, вроде активации, повторного использования циклов и организации пулов объектов. В случае применения любого из этих двух вариантов WCF-служба считается службой с собственным хостом или хостингом (self-hosted). Это вовсе необязательно плохо, поскольку дополнительные возможности, предлагаемые средами-хостами, могут и не требоваться. Однако в случае служб с собственными хостами в действительности необходимо писать больше кода. Программирование с использованием WCF Теперь, когда мы рассмотрели все базовые понятия, пришла пора заняться кодом. В настоящем разделе мы первым делом создадим простую WCF-службу с Web-сервером в качестве хоста, а также клиентское консольное приложение для ее тестирования. После изучения структуры созданного кода мы поговорим о базовой структуре WCF- служб и клиентских приложений, а затем более детально остановимся на рассмотрении следующих ключевых вопросов. □ Определение контрактов WCF-служб. □ WCF-службы с собственными хостами.
Глава 35. Windows Communication Foundation 1153 Практическое зан< штие Создание простой WCF-службы и клиентского приложения для нее 1. Создайте новый проект типа приложения службы WCF (WCF Service Application) по имени Ch35Ex01 и сохраните его в каталоге С: \BegVCSharp\Chapter35. 2. Добавьте в решение консольное приложение с именем Ch35Ex01Client. 3. В меню Build (Сборка) выберите пункт Build Solution (Собрать решение). 4. Щелкните правой кнопкой мыши на проекте Ch35Ex01 в окне Solution Explorer и выберите в контекстном меню, которое появится после этого, пункт Add Service Reference (Добавить ссылку на службу). 5. В диалоговом окне Add Service Reference (Добавление ссылки на службу), которое отобразится на экране далее, щелкните на кнопке Discover (Обнаружить). 6. После запуска Web-сервера разработки и загрузки информации о службе WCF, разверните ссылку, чтобы просмотреть детали, как показано на рис. 35.2 (номер порта, конечно, может выглядеть и по-другому). Add Service Reference То see a list of available services on a specific server, enter a service URL and dick Go. To browse for available services, dick Discover. Address: http:/flocalhost51173rtervicel.$vc Services: Operations: 3 0#^ ServiceLsvc £5 ]] Service! VGetData «GetDatallsingDataContract 1 servtce(s) found at address http://localhost:51173/Servicel.svc. ServiceReferencel Рис. 35.2. Просмотр деталей ссылки 7. Щелкните на кнопке ОК, чтобы добавить ссылку на службу. 8. Измените код в файле Program, cs приложения Ch35Ex01Client следующим образом: using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ch35Ex01Client.ServiceReferencel; namespace Ch35Ex01Client { class Program
1154 Часть V. Дополнительные технологии static void Main(string [ ] args) { string numericlnput = null; int intParam; do { Console.WriteLine( "Enter an integer and press enter to call the WCF service.") ; // Введите целое число и нажмите Enter, чтобы вызвать службу WCF numericlnput = Console. ReadLine () ; } while (!int.TryParse(numericlnput, out intParam)) ; ServicelClient client = new ServicelClient () ; Console.WriteLine(client.GetData(intParam)); Console. WriteLine ("Press an key to exit.") ; // Для выхода нажмите любую клавишу Console.ReadKey(); } } 9. Щелкните правой кнопкой мыши на решении в окне Solution Explorer и выберите в контекстном меню пункт Set Startup Projects (Установка стартовых проектов). 10. Сделайте оба проекта стартовыми, как показано на рис. 35.3, и щелкните на кнопке ОК. Solution "CM5EX0V Property Pagts Common Properties Startup Project Project Dependencies Debug Source Files Configuration Properties Рис. 35.3. Превращение проектов в стартовые 11. Щелкните правой кнопкой мыши на файле Service 1. svc в Ch35Ex01 и выберите в контекстном меню пункт Set a Startup Page (Сделать стартовой страницей). 12. Запустите приложение. Если появится соответствующее приглашение, щелкните на кнопке ОК, чтобы включить отладку в Web. conf ig. Далее введите в окне консольного приложения какое-то число и нажмите клавишу <Enter>. На рис. 35.4 показан результат, который должен получиться.
Глава 35. Windows Communication Foundation 1155 Рис. 35.4. Приложение Ch35Ex01 Client в действии 13. Просмотрите информацию на Web-странице, которая появится в окне браузера, как показано на рис. 35.5. Л Servicel S#mcf Window» Internet ЕжрЮгег e Mtp://kxa»io*t:Sll/J/S«»kel»*< sV ; Щ Service 1 Service Servicel Service •|*t j X WlveSeorrh a * в • <» - ••••.• о.*** You have created a service. To test this service, you will need to create a client and use it to call the service. You can do this using the I svcubl.exe tool from the command line with the following syntax: svcutll.exe http:// localhostiSU'Ta/Servirel . svc?»sdl This will generate a configuration hie and a code hie that contains the dient class. Add the two hies to your client application and use the generated client dass to call the Service. For example: class Test: ( atstlc void HainO ( ServicelClient client - ue* SetvluelCllent(); // Use the 'client' variable to call operations on the service. // always close the client, client.Closet)I % Local Intranet | Protected Mode: On *IH1* Puc. 35.5. Просмотр информации в окне браузера 14. Щелкните на ссылке в верхней части этой представляющей службу Web-страницы, чтобы отобразить содержимое файла WSDL. Беспокоиться не следует: знать, что обозначает все содержимое в файле WSDL, вовсе не обязательно. Описание полученных результатов В этом примере мы создали простую WCF-службу с Web-сервером в качестве хоста и клиентское консольное приложение для нее. Для создания проекта WCF-службы применялся стандартный шаблон WS, благодаря чему не потребовалось добавлять никакого кода. Вместо этого мы воспользовались одной из определенных в этом стандартном шаблоне операций, а именно — GetData (). Для целей данного примера сама операция не важна; вместо нее больше интересует структура кода и внутренние детали, которые позволяют ему работать. Сначала давайте взглянем на проект сервера — Ch35Ex01. Ниже описан его состав. □ Файл Servicel. svc, в котором определяются связанные с хостингом детали службы. □ Определение класса CompositeType, в котором определяется используемый службой контракт данных.
1156 Часть V. Дополнительные технологии □ Определение интерфейса IServicel, в котором определяется контракт службы и два контракта операций для службы. □ Определение класса Servicel, в котором реализуется интерфейс IServicel и определяется функциональность службы. □ Конфигурационный раздел <system. serviceModel> (в файле Web.config), в котором осуществляется конфигурирование службы. В файле Servicel. svc содержится следующая строка кода (чтобы увидеть ее, щелкните правой кнопкой мыши на этом файле в окне Solution Explorer и выберите в контекстном меню пункт View Markup (Просмотреть разметку)): <%@ ServiceHost Language="C#" Debug="true" Service="Ch35Ex01.Servicel" CodeBehind="Servicel.svc.cs" %> Она представляет собой инструкцию ServiceHost, которая используется для указания Web-серверу (в данном случае Web-серверу разработки, хотя это также распространяется и на сервер IIS), хостинг какой службы должен обеспечиваться по данному адресу. В атрибуте Service объявляется класс, в котором содержится определение службы, а в атрибуте CodeBehind— файл кода, в котором хранится определение самого этого класса. Наличие такой инструкции является необходимым для получения тех предоставляемых посредством хостинга возможностей Web-сервера, о которых рассказывалось в предыдущем разделе. Очевидно, что для WCF-служб, у которых в роли хоста выступает не Web-сервер, файл Servicel. svc не требуется. О том, как WCF-службы можно обеспечивать собственным хостом, будет рассказываться позже в этой главе. Далее определяется контракт данных CompositeType в файле IServicel. cs. В коде видно, что контракт данных представляет собой просто определение класса, в котором определение самого класса сопровождается атрибутом DataContract, а определения его членов — атрибутом DataMember: [DataContract] public class CompositeType { bool boolValue = true; string stringValue = "Hello "; [DataMember] public bool BoolValue { get { return boolValue; } set { boolValue = value; } } [DataMember] public string StringValue { get { return stringValue; } set { stringValue = value; } } } Этот контракт данных предоставляется клиентскому приложению через метаданные (если вы просмотрели содержимое файла WSDL, как указывалось в примере, то наверняка заметили это). Это позволяет клиентским приложениям определять тип, допускающий сериализацию в такую форму, из которой служба сможет преобразовать его обратно посредством десериализации в объект CompositeType. Клиенту не нужно знать, как в действительности выглядит определение данного типа; на самом деле в
Глава 35. Windows Communication Foundation 1157 используемом клиентом классе может даже присутствовать совершенно другая реализация. Такой простой способ определения контрактов данных является чрезвычайно мощным и позволяет осуществлять обмен сложными структурами данных между WCF- службой и ее клиентами. Еще в файле IServicel. cs содержится контракт службы, представляющий собой определение интерфейса с атрибутом ServiceContract. Этот интерфейс, опять-таки, полностью описывается в метаданных службы и может воссоздаваться в клиентских приложениях. Члены этого интерфейса образуют предоставляемые службой операции, и каждый из них используется для создания соответствующего контракта операции путем применения атрибута OperationContract. В коде данного примера присутствуют две операции, в одной из которых используется рассматривавшийся выше контракт данных: [ServiceContract] public interface IServicel { [OperationContract] string GetData(int value); [OperationContract] CompositeType GetDataUsingDataContract(CompositeType composite); } Все четыре атрибута для определения контрактов, которые встречались до сих пор, могут конфигурироваться далее с помощью других соответствующих атрибутов; как именно — будет показываться в следующем разделе. Код, реализующий службу, выглядит во многом так же, как и определение любого другого класса: public class Servicel : IServicel { public string GetData(int value) { return string.Format("You entered: {0}", value); } public CompositeType GetDataUsingDataContract(CompositeType composite) { if (composite.BoolValue) { composite.StringValue += "Suffix"; } return composite; } } Обратите внимание на то, что определение данного класса не нужно ни наследовать от какого-то особого типа, ни снабжать никакими особыми атрибутами. Все, что в нем необходимо сделать— это реализовать интерфейс, определяющий контракт службы. На самом деле в этот класс и его членов можно добавить атрибуты для спецификации их поведения, но обязательным это не является. Отделение контракта службы (интерфейса) от реализации службы (класса) дает замечательный результат. Благодаря этому, клиенту не требуется ничего знать о классе, который вполне мог бы предусматривать и гораздо больше функциональных возможностей, а не только реализацию службы. Он мог бы даже реализовать и более одного контракта службы. Наконец-то мы дошли и до конфигурации в файле Web. conf ig. Конфигурирование WCF-служб в конфигурационных файлах представляет собой возможность, которая была позаимствована из .NET-технологии удаленного взаимодействия и подходит как
1158 Часть V. Дополнительные технологии для всех типов WCF-служб (т.е. и служб с отдельным хостом, и служб с собственным хостом), так и (как будет показано чуть позже) для их клиентов. Предусмотренный словарный запас настолько велик, что позволяет применять практически любую вообразимую конфигурацию к службе и даже расширять этот синтаксис. Конфигурационный код WCF содержится в конфигурационном разделе <system. serviceModel> файлов Web. conf ig или арр. conf ig. В данном примере он содержится в файле Web. conf ig и состоит из двух подразделов. □ <services>. Здесь определяются входящие в состав проекта службы. Каждая служба определяется в отдельном дочернем разделе. □ <behaviors>. Здесь определяются механизмы поведения, которые должны использоваться для элементов в разделе <services>. Те механизмы поведения, которые определяются в этом разделе в отдельных дочерних разделах <behavior>, могут использоваться повторно для множества других элементов. В данном примере в состав проекта входит лишь одна служба. В конфигурационном коде ей присваивается имя и назначается именованный механизм поведения, который определяется в разделе <behaviors>: <configuration> <system.serviceModel> <services> <service name="Ch35Ex01.Service1" behaviorConfiguration="Ch35Ex01.ServicelBehavior"> Элемент <service> содержит два дочерних элемента <endpoint>, в каждом из которых (как нетрудно догадаться) определяется конечная точка для службы. На самом деле эти конечные точки являются базовыми конечными точками службы. Конечные точки для операций выводятся из них. Адреса конечных точек определяются в атрибутах address. В случае WCF-служб, у которых роль хоста выполняет Web-сервер, эти адреса соотносятся с их . svc-фай- лами. Привязки конечных точек определяются в атрибутах binding, а контракты служб для привязок задаются с помощью имен интерфейсов. Первая конечная точка является главной для службы, поэтому в ней в качестве контракта службы указывается интерфейс IServicel. Еще в ней указывается адрес по умолчанию и привязка типа WSHttpBinding: <endpoint address="" binding="wsHttpBinding" contract="Ch35Ex01.IServicel"> <identity> <dns value="localhost" /> </identity> </endpoint> Внутри элементов <endpoint> могут присутствовать различные дополнительные элементы, вроде показанного здесь элемента <identity>, в котором задается адрес базового сервера для конечной точки. Поскольку таковым в данном случае является сервер разработки, в этом элементе используется значение local host. Наша демонстрационная служба имеет также и вторую конечную точку, которая находится по адресу тех, что в полной версии звучит как metadata exchange (обмен метаданными). Она используется для предоставления клиентам возможности получать описания WCF-служб. В отличие от Web-служб, WCF-службы не предоставляют клиентам свои описания по умолчанию. Путем добавления конечной точки, использующей контракт IMetadataExchange, описания служб можно делать доступными. Что касается привязок, то для конечных точек, отвечающих за предоставления описаний служб,
Глава 35. Windows Communication Foundation 1159 можно применять либо привязку mexHttpBinding, либо привязку mexHttpsBinding, либо привязку mexNamedPipeBinding, либо же mexTcpBinding, в зависимости от используемого протокола: <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> </service> </services> В данном примере WSDL-описание службы является доступным. Получается оно присоединением к адресу службы окончания ?wsdl и на самом деле предоставляется механизмом поведения, а не через осуществляющую обмен метаданными конечную точку. Определение этого механизм поведения, который применяется ко всей службе целиком, выглядит следующим образом: <behaviors> <serviceBehaviors> <behavior name="Ch35Ex01.ServicelBehavior"> <serviceMetadata httpGetEnabled="true" /> Этот же механизм поведения еще и включает детали возникновения исключений в любые сообщения о неполадках, которые передаются клиенту, что обычно допускается только на этапе разработки: <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration > На этом определение сервера завершено. В клиентском приложении мы добавили ссылку на данную службу с помощью инструмента Add Service Reference (Добавление ссылки на службу), который использует предоставляемые службой метаданные (т.е. WSDL-файл службы) для создания прокси- классов. Такой подход не является единственным способом для получения доступа к WCF-службам, но зато он один из наиболее простых. Существует еще один распространенный подход, который заключается в определении контрактов для WCF-служ- бы в отдельной сборке с последующим добавлением ссылки на нее и в проект хоста, и в проект клиента. Такой подход позволяет клиенту тогда генерировать прокси не через предоставляемые метаданные, а непосредственно используя сами контракты. При желании код, сгенерированный инструментом Add Service Reference, можно просмотреть (открыв всех имеющиеся в проекте файлы вместе, включая скрытые), хотя пока, пожалуй, лучше этого не делать, потому что осталось еще много непонятного кода. Главное здесь обратить внимание на то, что этот инструмент создает все классы, которые требуются для получения доступа к службе. К их числу относится прокси- класс для службы, содержащий методы для всех предоставляемых данной службой операций (ServiceClient), и клиентский класс, сгенерированный из контракта данных (CompositeType). Еще этот инструмент добавляет в проект конфигурационный файл app.config. В нем определяются две вещи: □ детали привязки для конечной точки службы; □ адрес и контракт для этой конечной точки.
1160 Часть V. Дополнительные технологии Детали привязки берутся из описания службы, и при этом в конфигурационный файл клиентского приложения из него копируется каждая конфигурируемая опция: <configuration> <system.serviceModel> <bindings> <wsHttpBinding> <bmding name="WSHttpBinding_IServicel" closeTimeout=0:01:00" openTimeout=M00:01:00" receiveTimeout=0:10:00" sendTimeout=0:01:00" bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize=24288" maxReceivedMessageSize=5536" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false"> <readerQuotas maxDepth=2" maxStringContentLength="8192" maxArrayLength=6384" maxBytesPerRead= 096" maxNameTableCharCount=6384" /> <reliableSession ordered="true" inactivityTimeout=0:10:00" enabled="false" /> <security mode="Message"> <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" /> <message сlientCredentialType="Windows" negotiateServiceCredential="true" algorithmSuite="Default" establishSecurityContext="true" /> </security> </binding> </wsHttpBinding> </bmdings> Эта привязка используется в конфигурации конечной точки вместе с базовым адресом службы (каковым в случае служб с Web-сервером в качестве хоста является адрес .svc-файла) и клиентской версией контракта IServicel: <client> <endpoint address="http://localhost:51173/Servicel.svc" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IServicel" contract="ServiceReferencel.IServicel" name="WSHttpBinding_IServicel"> <identity> <dns value="localhost" /> </identity> </endpoint> </client> </system.serviceModel> </configuration> Инструмент Add Service Reference поработал здесь весьма основательно. На самом деле в большинстве из этих деталей нет никакой необходимости, потому что мы использовали стандартную привязку WSHttpBinding. А это значит, что можно было бы заменить содержимое этого конфигурационного файла следующим кодом: <configuration> <system.serviceModel> <client> <endpoint address="http: //localhost: 51173/Servicel. svc" binding="wsHttpBinding" contract="ServiceReferencel.IServicel" name="WSHttpBinding_IServicel"> <identity> <dns value="localhost" /> </identity> </endpoint> </client> </system.serviceModel> </configuration>
Глава 35. Windows Communication Foundation 1161 Здесь атрибут bindingConf iguration в элементе <endpoint> полностью отсутствует, а это значит, что клиент будет использовать стандартную конфигурацию привязки. Для изучения WCF-служб, однако, такая тщательность инструмента Add Service Reference является довольно-таки полезной. Она позволяет увидеть все параметры, которые входят в состав стандартной привязки WSHtttpBinding. О конфигурировании WCF-служб очень уж подробно в этой главе рассказываться не будет, но зато уже не трудно заметить, что некоторые из параметров, вроде настроек тайм-аута, являются довольно легкими для понимания благодаря своим явным именам. В данном примере было рассмотрено много всего, и поэтому не помешать кратко резюмировать полученные знания прежде, чем двигаться дальше. □ Что касается определения Web-служб: • службы определяются с помощью представляющего контракт службы интерфейса, в состав которого входят представляющие контракты операций члены; • реализуются службы в классе, который реализует представляющий контракт службы интерфейс; • контракты данных представляют собой просто определения типов, в которых используются атрибуты контрактов данных; □ Что касается конфигурирования WCF-служб: • для конфигурирования WCF-служб можно использовать конфигурационные файлы (Web. conf ig или арр. conf ig); □ Что касается хостинга WCF-служб на Web-сервере: • при хостинге на Web-сервере в роли базовых адресов служб используются адреса их . svc-файлов; □ Что касается конфигурирования клиентов WCF-служб: • для конфигурирования клиентов WCF-служб тоже можно использовать конфигурационные файлы (Web. conf ig или арр. conf ig). В следующем разделе мы более подробно поговорим о контрактах. Определение контрактов для служб WCF Пример в предыдущем разделе продемонстрировал, как инфраструктура WCF упрощает определение контрактов для WCF-служб с помощью комбинации классов, интерфейсов и атрибутов. В настоящем разделе мы рассмотрим эту методику более подробно. Контракты данных Контракты данных для службы определяются за счет применения к определению класса атрибута DataContractAttribute. Этот атрибут находится в пространстве имен System. Runtime . Serialization. Конфигурировать его можно с помощью свойств, перечисленных в табл. 35.2. Оба эти свойства полезны в случае необходимости обеспечения функциональной совместимости с существующими форматами SOAP-сообщений (как и именованными похожим образом свойствами для других контрактов), но во всех остальных случаях они вряд ли могут потребоваться. У каждого члена класса, который является частью контракта данных, должен обязательно быть атрибут DataMemberAttribute, который тоже находится в пространстве имен System.Runtime.Serialization. Свойства, доступные для конфигурирования этого атрибута, перечислены в табл. 35.3.
1162 Часть V. Дополнительные технологии Таблица 35.2. Свойства атрибута DataContractAttribute Свойство Предназначение Name Namespace Присваивает контракту данных имя, отличное от того, что используется для определения класса. Именно это имя будет применяться в SOAP-сообщениях и клиентских объектах данных, создаваемых на основе метаданных службы Задает пространство имен, которое должно использоваться вместе с данным контрактом в SOAP-сообщениях Таблица 35.3. Свойства атрибута DataMemberAttribute Свойство Предназначение Name Задает имя члена данных при сериализации (по умолчанию используется имя члена) isRequired Указывает, должен ли данный член присутствовать в SOAP-сообщении Order Принимает значение int, задающее порядок сериализации или десериали- зации члена, что может быть необходимо в случае, когда присутствие одного члена является обязательным для понимания другого члена. Члены с более низкими значениями Order будут обрабатываться первыми EmitDefauitvalue Установка для этого свойства значения false позволяет предотвращать включение членов в SOAP-сообщения в случае, если их значение совпадает со значением по умолчанию Контракты служб Контракты служб определяются путем применения к определению интерфейса атрибута System. ServiceModel. ServiceContractAttribute. Настраивать такие контракты можно с помощью свойств, перечисленных в табл. 35.4. Таблица 35.4. Свойства атрибута ServiceContractAttribute Свойство Предназначение Name Namespace ConfigurationName HasProtectionLevel ProtectionLevel SessionMode CallbackContract Задает имя контракта службы в соответствии с тем, как указано в элементе <portType> bWSDL Задает пространство имен для контракта службы в соответствии с тем, как указано в элементе <portType> в WSDL Имя контракта службы в том виде, в котором оно используется в конфигурационном файле Определяет то, имеются ли у используемых службой сообщений явно определенные уровни защиты. Уровни защиты позволяют подписывать (или подписывать и шифровать) сообщения Используемый уровень защиты сообщений Определяет то, включен ли для сообщений режим сеансов. Применение такого режима позволяет гарантировать корреляцию сообщений, отправляемых в разные конечные точки службы, т.е. гарантировать использование для них одинакового экземпляра службы и, следовательно, наличия у них общего состояния, и т.д. При двустороннем обмене сообщениями клиент предоставляет контракт, так же как и служба. Объясняется это тем, что клиент, как уже рассказывалось ранее, в двусторонних коммуникациях выступает еще и в роли сервера. Данное свойство позволяет указывать то, какой контракт использует клиент.
Глава 35. Windows Communication Foundation 1163 Контракты операций Контракты операций определяются в виде членов-операций внутри интерфейсов, определяющих контракты по службам, путем применения атрибута System. ServiceModel .OperationContractAttribute. Свойства, доступные для настройки этого атрибута, перечислены в табл. 35.5. Таблица 35.5. Свойства атрибута OperationContractAttribute Свойство Предназначение Name IsOneWay AsyncPattern HasProtectionLevel ProtectionLevel Islnitiating IsTerminating Action ReplyAction Задает имя операции службы. По умолчанию используется имя члена Указывает на то, возвращает ли данная операция какой-то ответ. В случае установки для этого свойства значения true клиенты не будут ожидать завершения операции, прежде чем продолжить работу Установка для этого свойства значения true позволяет реализовать операцию в виде двух методов — Вед1п<имя_метода> и Епс1<имя_метода> — и затем использовать их для вызова этой операции асинхронным образом См. предыдущий раздел См. предыдущий раздел В случае применения режима сеансов это свойство определяет то, может ли вызов данной операции приводить к запуску нового сеанса В случае применения режима сеансов это свойство определяет то, может ли вызов данной операции приводить к завершению текущего сеанса В случае применения режима адресации (представляющего собой усовершенствованную возможность служб WCF) с операцией ассоциируется определенное действие, имя которого можно задавать с помощью данного свойства Похоже на предыдущее свойство, но только позволяет указывать имя для ответного действия операции Контракты сообщений В приведенном ранее примере контракты сообщений не использовались. При желании применять их нужно определить класс, представляющий сообщение, и указать для него атрибут MessageContractAttribute, а для его членов— атрибуты Message BodyMember At tribute, MessageHeaderAttribute или MessageHeaderArrayAttribute. Все эти атрибуты находятся в пространстве имен System. ServiceModel. Желание определять такие контракты может возникать только при острой необходимости в наличии очень высокого уровня контроля над SOAP-сообщениями, которые используются WCF-службами, поэтому более детально они здесь рассматриваться не будут. Контракты неполадок При наличии определенного типа исключения, например, специального исключения, которое требуется сделать доступным для клиентских приложений, можно применять к операции, которая может генерировать такое исключение, атрибут System. ServiceModel. FaultContractAttribute. Такой подход не часто используется при обычной работе с WCF.
1164 Часть V. Дополнительные технологии Практическое занятие Контракты WCF 1. Создайте новый проект типа приложения службы WCF (WCF Service Application) по имени Ch35Ex02 и сохраните его в каталоге С: \BegVCSharp\Chapter35. 2. Добавьте в решение проект типа библиотеки классов по имени Ch35Ex01Contracts и удалите из него файл Classl. cs. 3. Добавьте в проект Ch35Ex01Contracts ссылки на сборки System.Runtime. Serialization.dll и System.ServiceModel.dll. 4. Далее добавьте в этот проект класс с именем Person и измените код в представляющем этот класс файле Person. cs: using System; using System.Collections.Generic- using System.Linq; using System.Text; using System. Runtime. Serialization ; namespace Ch35Ex02Contracts { [DataContract] public class Person { [DataMember] public string Name { get; set; } [DataMember] public int Mark { get; set; } } } 5. Теперь добавьте в проект класс IAwardService и измените код в представляющем его файле IAwardService.cs следующим образом: using System.Collections .Generic- using System.Linq; using System.Text; using System. ServiceModel; namespace Ch35Ex02Contracts { [ServiceContract(SessionMode=SessionMode.Required)] public interface IAwardService { [OperationContract(IsOneWay=true,Islnitiating=true)] void SetPassMark(int passMark) ; [OperationContract] Person[] GetAwardedPeople(Person[] peopleToTest); } } 6. Что касается проекта Ch35Ex01, то сначала добавьте в него ссылку на проект Ch35Ex01Contracts. 7. Затем удалите из проекта Ch35Ex01 файлы IServicel. cs и Servicel. svc. 8. Теперь добавьте в него новую службу WCF с именем AwardService.
Глава 35. Windows Communication Foundation 1165 9. Далее удалите из него файл IAwardService. cs. 10. Измените код в файле AwardService. svc. cs следующим образом: using System; using System.Collections .Generic- using System.Linq; . using System.Runtime.Serialization; using System.ServiceModel; using System.Text; using Ch35Ex02Contracts ; namespace Ch35Ex02 { public class AwardService : IAwardService { private int passMark; public void SetPassMark(int passMark) { this. passMark = passMark; } public Person[] GetAwardedPeople(Person[] peopleToTest) { List<Person> result = new List<Person>() ; foreach (Person person in peopleToTest) { if (person.Mark > passMark) { result.Add(person); } } return result.ToArrayO ; } } } 11. Измените отвечающий за конфигурацию этой службы раздел в Web.config следующим образом: <system.serviceModel> <services> <service name="Ch35Ex02.AwardService"> <endpoint address="" binding="wsHttpBinding" contract="Ch35Ex02Contracts.IAwardService" /> </service> </services> </system.serviceModel> 12. Сделайте AwardService.svc стартовой страницей проекта Ch35Ex02. 13. Запустите проект Ch35Ex02 в режиме отладки и зафиксируйте где-то используемый в браузере URL-адрес (вместе с номером порта, который вскоре потребуется). 14. Остановите процесс отладки и добавьте в решение новый проект типа консольного приложения по имени Ch35Ex02Client. 15. Добавьте в этот проект Ch35Ex02Client ссылку на сборку System. ServiceModel .dll и проект Ch35Ex02Contracts. 16. Измените код в файле Program.cs в этом проекте следующим образом:
1166 Часть V. Дополнительные технологии using System; using System. Col lections. Generic- using System.Linq; using System.Text; using System.ServiceModel; using Ch35Ex02Contracts; namespace Ch35E02Client { class Program { static void Main(string[] args) { Person [] people = new Person [] { new Person { Mark = 46, Name="Jim" }, new Person { Mark = 73, Name="Mike" }, new Person { Mark = 92, Name="Stefan" }, new Person { Mark = 84, Name="George" }, new Person { Mark = 24, Name= "Arthur" }, new Person { Mark = 58, Name="Nigel" } }; Console.WriteLine("People:"); OutputPeople(people); IAwardService client = ChannelFactory<IAwardService>. CreateChannel ( new WSHttpBinding () , new EndpointAddress("http://localhost:51425/AwardService.svc")) ; client.SetPassMarkG0); Person[] awardedPeople = client.GetAwardedPeople(people); Console.WriteLine(); Console .WriteLine ("Awarded people: ") ; OutputPeople(awardedPeople); Console.ReadKey(); } static void OutputPeople (Person [] people) { foreach (Person person in people) { Console.WriteLine("{0}, mark: {1}", person.Name, person.Mark); } } } } 17. Запустите приложение. На рис. 35.6 показан результат, который должен получиться. Рис. 35.6. Приложение Ch35Ex02Client в действии
Глава 35. Windows Communication Foundation 1167 Описание полученных результатов В этом примере мы создали ряд контрактов в проекте библиотеки классов и использовали эту библиотеку классов и в WCF-службе и в клиенте. За хостинг службы, как и в предыдущем примере, отвечает Web-сервер. Конфигурация службы была сокращения до минимума. Главное отличие в этом примере состоит в том, что клиенту не требовались никакие метаданные, поскольку у него был доступ к сборке контрактов. Вместо того чтобы генерировать прокси-класс из метаданных, клиент получал ссылку на интерфейс контракта службы посредством альтернативного метода. Еще одним моментом, на который нужно обратить внимание в этом примере, является использование сеанса для поддержания состояния в службе. Контракт данных, использованный в этом примере, предназначался для простого класса по имени Person, имеющего свойство типа string с именем Name и свойство типа int с именем Mark. Атрибуты DataContractAttribute и DataMemberAttribute применяются без всякой настройки, да и в повторной итерации кода для этого контракта здесь нет никакой необходимости. Контракт службы был определен путем применения к интерфейсу IAwardService атрибута ServiceContractAttribute. Для свойства SessionMode этого атрибута было установлено значение SessionMode.Required, поскольку данной службе требуется состояние: [ServiceContract(SessionMode=SessionMode.Required)] public interface IAwardService { Контракт первой операции — SetPassMark () — является именно тем, в котором устанавливается состояние и потому для свойства Islnitiating его атрибута Operation ContractAttribute устанавливается значение true. Эта операция ничего не возвращает и потому определяется как односторонняя операция путем установки IsOneWay в true: [OperationContract(IsOneWay=true, Islnitiating=true)] void SetPassMark(int passMark); Контракт другой операции — GetAwardedPeople () — не требует никакой настройки и использует определенный ранее контракт данных: [OperationContract] Person [] GetAwardedPeople (Person [] peopleToTest); } Напоминаем, что оба типа — Person и IAwardService — доступы и для службы, и для клиента. Служба реализует контракт IAwardService в типе по имени AwardService, который никакого примечательного кода не содержит. Единственное отличие между этим классом и классом службы, который демонстрировался ранее, состоит в том, что он обладает состоянием. Подобное является допустимым, поскольку для корреляции сообщений от клиента определяется сеанс. Клиент выглядит интереснее в первую очередь из-за следующей строки кода: IAwardService client = ChannelFactory<IAwardService>.CreateChannel( new WSHttpBindingO , new EndpointAddress("http://localhost:51425/AwardService.svc")); Получается, что у клиентского приложения нет ни файла арр. con fig для настройки коммуникаций со службой, ни прокси-класса, определяемого на основе метаданных для взаимодействия со службой. Вместо этого прокси-класс создается методом
1168 Часть V. Дополнительные технологии ChannelFactory<T> .CreateChannel (). Этот метод создает прокси-класс, реализующий клиент IAwardService, хотя "за кулисами" этот генерируемый класс взаимодействует со службой точно так же, как и тот, что демонстрировался ранее и генерировался на основе метаданных. В случае создания прокси-класса подобным способом канал связи будет по умолчанию инициировать тайм-аут через минуту, что чревато возникновением ошибок связи. Конечно, существуют способы для поддержания соединения в активном состоянии, но их рассмотрение выходит за рамки контекста настоящей книги. Такое создание прокси-классов является чрезвычайно полезным приемом, которым можно пользоваться для быстрой генерации клиентского приложения в процессе работы. Службы WCF с собственным хостом Пока что в этой главе демонстрировались только WCF-службы, за хостинг которых отвечал Web-сервер. Такой подход удобен для обмена данными через Internet, но для обмена данными по локальной сети наиболее эффективным решением он не является. Во-первых, он требует наличия Web-сервера на том компьютере, который должен выступать в роли хоста службы. Во-вторых, архитектура создаваемых приложений может делать применение независимой WCF-службы нежелательным. По этим причинам в некоторых случаях может быть выгоднее использовать WCF- службу с собственным хостом. WCF-службой с собственным хостом называется такая служба, которая существует в создаваемом самим разработчиком процессе, а не в процессе специально предназначенного для обеспечения хостинга приложения вроде Web-сервера. Это означает, что для хостинга службы можно использовать, например, консольное приложение или приложение Windows. Для оснащения WCF-службы собственным хостом применяется класс System. ServiceModel. ServiceHost. Экземпляр этого класса создается с помощью либо типа службы, которую требуется снабдить хостом, либо экземпляра класса службы. Конфигурировать хост службы можно либо с помощью свойств и методов, либо (что разумнее) с применением конфигурационного файла. На самом деле процессы хостов вроде Web-серверов тоже используют для обеспечения своего хостинга экземпляр класса ServiceHost. Отличие состоит в том, что при снабжении службы собственным хостом взаимодействие с этим классом осуществляется напрямую. Однако синтаксис конфигурации, помещаемой в раздел <system. serviceModel> файла арр. conf ig для приложения-хоста, выглядит абсолютно так же, как и в тех конфигурационных разделах, которые уже демонстрировались ранее в настоящей главе. Предоставлять доступ к службе с собственным хостом можно через любой протокол, хотя обычно в приложениях такого типа все-таки применяется привязка к протоколу TCP или именованному каналу. Службы с доступом через протокол HTTP чаще всего функционируют внутри процессов Web-серверов, поскольку это обеспечивает их дополнительными функциональными возможностями, которые предлагают Web- серверы, вроде безопасности и т.п. При желании снабдить хостом службу по имени MyService, например, можно было бы использовать для создания экземпляра класса ServiceHost такой код: ServiceHost host = new ServiceHost(typeof(MyService)); А при желании снабдить хостом экземпляр MyService по имени myServiceObject, для создания экземпляра ServiceHost можно было бы применить следующий код:
Глава 35. Windows Communication Foundation 1169 MyService myServiceObject = new MyServiceO; ServiceHost host = new ServiceHost(myServiceObject); Обратите внимание, что последний прием работает только при условии настройки службы таким образом, чтобы вызовы всегда направлялись одному и тому же экземпляру объекта, что требует применения к классу службы атрибута ServiceBehaviorAttribute и установки свойства InstanceContextMode этого атрибута вInstanceContextMode.Single. После создания экземпляра ServiceHost можно переходить к настройке службы, ее конечных точек и привязки посредством свойств. Вместо свойств эту настройку можно выполнять и посредством файла . conf ig, в случае чего экземпляр ServiceHost будет конфигурироваться автоматически. Для запуска хоста службы после настройки экземпляра ServiceHost применяется метод ServiceHost. Open (), а для останова — соответственно, метод ServiceHost. Close (). При первом запуске хоста привязанной к TCP службы может поступить предупреждающее сообщение от службы брандмауэра Windows, если таковая включена, поскольку эта служба по умолчанию блокирует TCP-порт. В таком случае потребуется открыть TCP-порт, чтобы служба начала ожидать подключения к данному порту. В следующем практическом занятии демонстрируется применение приемов снабжения WCF-службы собственным хостом для предоставления через нее доступа к некоторым функциональным возможностям WPF-приложения. Практическое занятие Службы WCF С СОбСТВвННЫМ ХОСТОМ 1. Создайте новое приложение по имени Ch35Ex03 и сохраните его в каталоге С:\BegVCSharp\Chapter35. 2. Добавьте в проект новую службу WCF AppControlService с помощью мастера добавления нового элемента (Add New Item Wizard). 3. Измените код в файле Windowl. xaml следующим образом: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Ch35Ex03.Windowl" Title="Solar Evolution" Height=50" Width=30" Loaded="Window_Loaded" Closing="Window_Closing"> <Grid Height=00" Width=00" HorizontalAlignment="Center" VerticalAlignment="Center"> <Rectangle Fill="Black" RadiusX=0" RadiusY=0" StrokeThickness=0"> <Rectangle.Stroke> <LinearGradientBrush EndPoint=.358,0.02" StartPoint=.642,0.98"> <GradientStop Color="#FF121A5D" Offset=" /> <GradientStop Color="#FFBlB9FF" 0ffset="l" /> </LinearGradientBrush> </Rectangle.Stroke> </Rectangle> <Ellipse Name="AnimatableEllipse" Stroke="{x:Null}" Height=" Width=" HorizontalAlignment="Center" VerticalAlignment="Center"> <Ellipse.Fill> <RadialGradientBrush> <GradientStop Color="#FFFFFFFF" Offset=" /> <GradientStop Color="#FFFFFFFF" 0ffset="l" /> </RadialGradientBrush> </Ellipse.Fill>
1170 Часть V. Дополнительные технологии <Ellipse.BitmapEffect> <OuterGlowBitmapEffect GlowColor="#FFFFFFFF" GlowSize=6" /> </Ellipse.BitmapEffect> </Ellipse> </Grid> </Window> 4. Измените код в файле Windowl. xaml. cs, как показано ниже: using System.Windows.Shapes; using System. ServiceModel ; using System.Windows.Media.Animation ; namespace Ch35Ex03 { /// <summary> /// Логика взаимодействия для Windowl.xaml /// </summary> public partial class Windowl : Window { private AppContrо 1 Service service; private ServiceHost host; public Windowl() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { service = new AppControlService(this) ; host = new ServiceHost (service) ; host.Open(); } private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { host.Close(); } internal void SetRadius(double radius, string foreTo, TimeSpan duration) { if (radius > 200) { radius = 200; } Color foreToColor = Colors.Red; try { foreToColor = (Color)ColorConverter.ConvertFromString(foreTo) ; ) catch { // Игнорировать неудачные попытки преобразования цвета. ) Duration animationLength = new Duration (duration) ; DoubleAnimation radiusAnimation = new DoubleAnimation ( radius * 2, animationLength); ColorAnimation colorAnimation = new ColorAnimation ( foreToColor, animationLength); AnimatableEllipse. BeginAnimation (Ellipse. HeightProperty, radiusAnimation);
Глава 35. Windows Communication Foundation 1171 AnimatableEllipse. BeginAnimation (Ellipse. WidthProperty, radiusAnimation) ; ((RadialGradientBrush) AnimatableEllipse. Fill) . Gradients tops [ 1 ] .BeginAnimation(GradientStop.ColorProperty, color Animation); } } } 5. Измените код в IAppControlService.es следующим образом: [ServiceContract] public interface IAppControlService { [OperationContract] void SetRadius(int radius, string foreTo, int seconds); } 6. Измените код в AppControlService. cs следующим образом: [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)] public class AppControlService : IAppControlService { private Window 1 hostApp; public AppControlService (Window 1 hostApp) { this. hostApp = hostApp; } public void SetRadius (int radius, string foreTo, int seconds) { hostApp.SetRadius(radius, foreTo, new TimeSpan@, 0, seconds)); } } 7. Измените код в арр. conf ig следующим образом: <configuration> <system.serviceModel> <services> <service name="Ch35Ex03. AppControlService"> <endpoint address="net. tcp: //localhost: 8081/AppControlService" binding="netTcpBinding" contract="Ch35Ex03.IAppControlService" /> </service> </services> </system.serviceModel> </configuration> 8. Добавьте в проект новое консольное приложение по имени Ch35Ex03Client. 9. Сконфигурируйте решение как имеющее несколько стартовых проектов, причем так, чтобы оба созданных ранее проекта запускались одновременно. 10. Добавьте в проект Ch35Ex03Client ссылку на System.ServiceModel.dll и Сп35ЕхОЗ. 11. Измените код в Program, cs следующим образом: using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ch35Ex03; using System. ServiceMOdel;
1172 Часть V. Дополнительные технологии namespace Ch35Ex03Client { class Program { static void Main(string [ ] args) { Console.WriteLine("Press enter to begin.11) ; // Нажмите Enter, чтобы начать работу Console.ReadLine(); Console.WriteLine("Opening channel..."); // Открытие канала IAppControlService client = ChannelFactory<IAppControlService>.CreateChannel( new NetTcpBinding () , new EndpointAddress ("net. tcp: //localhost: 8081/AppControlService")) ; Console.WriteLine("Creating sun.. .") ; // Создание солнца client.SetRadiusA00, "yellow", 3) ; Console.WriteLine("Press enter to continue."); // Нажмите Enter, чтобы продолжить Console.ReadLine(); Console.WriteLine("Growing sun to red giant..."); // Увеличение солнца до красного гиганта client.SetRadiusB00, "Red", 5) ; Console.WriteLine("Press enter to continue."); // Нажмите Enter, чтобы продолжить Console.ReadLine(); Console.WriteLine("Collapsing sun to neutron star..."); // Коллапс солнца в нейтронную звезду client.SetRadiusE0, "AliceBlue", 2) ; Console.WriteLine("Finished. Press enter to exit."); // Процесс завершен. Нажмите Enter, чтобы выйти Console.ReadLine(); } } } 12. Запустите решение. Если появится соответствующее приглашение, разблокируйте TCP-порт брандмауэра Windows, чтобы служба WCF могла начать ожидание подключений. 13. Когда появится окно Solar Evolution (Эволюция солнца) и окно консольного приложения, нажмите в окне консольного приложения клавишу <Enter>. На рис. 35.7 показан результат, который должен получиться. 14. При желании продолжить цикл эволюции солнца снова нажмите клавишу <Enter>. Описание полученных результатов В данном примере мы добавили в WPF-приложение WCF-службу и использовали ее для управления анимационными эффектами элемента управления Ellipse, после чего создали простое клиентское приложение для ее тестирования. Тем, кто пока не слишком знаком с WPF, не стоит особо волноваться об XAML-коде в этом примере, поскольку здесь больше интересуют детали кода WCF.
Глава 35, Windows Communication Foundation 1173 Ш fite:///C:/BeflVCSharp/Ch*pttr35/Ch35Ex03/... ^± Рис. 35.7. Консольное приложение и окно Solar Evolution Итак, WCF-служба AppControlService предоставляет единственную операцию — SetRadius () , которую клиенты могут вызывать для управления анимационными эффектами. Она взаимодействует с имеющим идентичное имя методом, который определен в классе Windows 1 WPF-приложения. Чтобы это было возможно, у службы должна обязательно быть ссылка на это приложение, а это требует обеспечения хоста для экземпляра объекта этой службы. Как уже рассказывалось ранее, у службы должен быть атрибут поведения: [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)] public class AppControlService : IAppControlService { } В Window 1. xaml. cs создается экземпляр службы в обработчике событий Windows_ Loaded (), который и запускает хост путем создания для службы объекта ServiceHost и вызова его метода Open (): public partial class Windowl : Window { private AppControlService service; private ServiceHost host; private void Window_Loaded(object sender, RoutedEventArgs e) { service = new AppControlService(this); host = new ServiceHost(service); host.Open(); } Работа этого хоста завершается в обработчике событий WindowClosing () после закрытия приложения. Конфигурационный файл выглядит настолько просто, насколько это возможно. В нем для WCF-службы определяется единственная конечная точка, прослушивающая порт 8081 по адресу net. tcp и использующая стандартную привязку NetTcpBinding:
1174 Часть V. Дополнительные технологии <service name="Ch35Ex03.AppControlService"> <endpoint address="net.tcp://localhost:8081/AppControlService" binding="netTcpBinding" contract="Ch35Ex03.IAppControlService" /> </service> Этот код соответствует коду в клиентском приложении, как было и в предыдущем примере (хотя здесь используется ТСР-привязка): IAppControlService client = ChannelFactory<IAppControlService>.CreateChannel( new NetTcpBinding(), new EndpointAddress("net.tcp://localhost:8081/AppControlService")); После создания клиентского прокси-класса клиент может вызывать метод SetRadius () с параметрами радиуса, цвета и длительности анимационных эффектов; все они будут пересылаться через службу WPF-приложению. Простой код в WPF-приложении далее будет определять и использовать их для изменения размера и цвета эллипса. При желании можно было бы сделать так, чтобы этот код работал и по локальной сети, указав не адрес localhost, а имя какого-то компьютера и разрешения передачи трафика в этой сети через заданный порт. В качестве альтернативного варианта можно было бы еще больше разделить клиентское приложение и приложение-хост и подключаться через Internet. В любом случае WCF-службы представляют собой замечательные средства связи, настройка которых действительно не требует приложения особых усилий. Резюме В настоящей главе были рассмотрены основные приемы по применению WCF- служб для обеспечения связи между приложения, процессами и компьютерами. Сначала здесь было рассказано о том, что собой вообще представляют WCF-службы и чем они отличается от Web-служб и механизмов удаленного взаимодействия, а также об основных концепциях, с которыми нужно быть знакомым для того, чтобы ими пользоваться. Далее было показано, как программировать WCF-службы, как их использовать в клиентских приложениях и как их обеспечивать различными хостами. В этой главе рассмотрены следующие вопросы. □ Что собой представляют WCF-службы. □ Как WCF-службы конфигурировать с помощью адресов, конечных точек, привязок, контрактов и механизмов поведения. □ Какие варианты существуют для хостинга WCF-служб. □ Какие классы, атрибуты, интерфейсы и конфигурационные параметры требуются для создания WCF-служб. □ Как создавать прокси-классы для клиентских приложений через метаданные WCF. □ Как определять и использовать контракты данных, служб и операций. □ Как генерировать клиентские прокси-классы из WCF-контрактов. □ Как обеспечивать WCF-службы собственным хостом. Этот материал является лишь базовым минимумом, который необходимо знать для того, чтобы использовать WCF-службы в своих приложениях. Он далеко не охватывает весь круг доступных на самом деле возможностей, особенно в том, что касается конфигурации WCF-служб в файле . con fig и механизмов поведения. Каркас WCF позволяет обеспечивать интеграцию с более совершенными инфраструктурами безопасности и настраивать связь практически любым вообразимым образом.
Глава 35. Windows Communication Foundation 1175 Упражнения 1. Какие из перечисленных ниже приложений могут играть роль хоста для служб WCF? • Web-приложения • Приложения Windows Forms • Службы Windows • Приложения СОМ+ • Консольные приложения 2. Контракт какого типа вы бы реализовали при желании обмениваться с WCF- службой параметрами MyClass? Какие атрибуты потребовались бы для этого? 3. В случае применения для хостинга WCF-службы Web-приложения как будет выглядеть расширение базовой конечной точки этой службы? 4. В случае WCF-служб с собственным хостом конфигурировать службу нужно путем установки свойств и вызова методов класса ServiceHost. Верно это или нет? 5. Напишите код для контракта службы IMusicPlayer и определите в нем операции Play (), Stop () и GetTracklnf ormation (). Где необходимо, используйте односторонние методы. Какие другие контракты можно определить для работы такой службы?
36 Windows Workflow Foundation Приветствуем вас в последней главе книги и надеемся, что нам удалось припасти самое интересное напоследок! Многие приложения нуждаются в наличии какой-нибудь возможности для их конфигурирования или настройки, и традиционно таковая обеспечивалась путем добавления в приложения какого-то хитрого приема, с помощью которого сторонние разработчики могли бы вставлять в них дополнительный код. Наиболее распространенный подход предусматривал определение в .NET интерфейса и последующую проверку того, что каждый подключаемый модуль поддерживает его. Недостаток такого подхода состоял в том, что все требовалось делать с помощью кода, что для конечных пользователей без опыта программирования было устрашающим, а то и вовсе невозможным. Другим способом для расширения приложений было и остается применение какого- то языка сценариев, вроде VBA (Visual Basic for Applications), который входит в состав Microsoft Word, Excel и многих других приложений. Проблема такого подхода состоит в стоящем на пути для начала его применения барьере, а именно — затратах, связанных с приобретением лицензии на использование VBA, к тому же интеграция этого продукта в собственные приложения тоже никак не является простым процессом. В .NET 3.0 и выше доступен другой (и, по нашему мнению, гораздо более удобный) способ для предоставления пользователям возможности выполнять настройку приложения, и заключается он в применении такого продукта, как Windows Workflow Foundation (WF). К рабочему потоку (workflow) можно относиться и просто как к функции на языке С#, которая состоит из ряда операторов (называемых действиями (activities)), а также необязательных параметров и возвращаемых значений. На этом, однако, все его сходство с функцией заканчивается, потому что рабочий поток определяется не с помощью кода, а с помощью ряда графических строительных блоков.
Глава 36. Windows Workflow Foundation 1177 Действия являются теми строительными блоками, из которых конструируется рабочий поток: в .NET поставляется 30 встроенных действий, часть из которых рассматривается в этой главе. Каждое действие, по сути, представляет собой всего лишь .NET-класс, который наследуется от определенного базового класса, что позволяет разработчику создавать свои собственные действия для выполнения специальных операций по обработке. Процесс создания рабочего потока напоминает процесс конструирования формы или страницы ASP.NET: разработчик может перетаскивать действия из панели Toolbox (Панель инструментов) на поверхность конструктора и объединять их в завершенный рабочий поток. При запуске потока в действие вступает механизм исполняющей среды (называемый исполняющей средой рабочего потока — Workf lowRuntime), который и выполняет все входящие в состав данного рабочего потока действия. Этот механизм знает, как планировать рабочие потоки, и применяется для обеспечения взаимодействия между выполняющимися экземплярами рабочих потоков. Рабочий поток можно сравнить с шаблоном Word: если рабочий поток— это шаблон, тогда выполняющийся экземпляр данного рабочего потока — это созданный из этого шаблона документ. В частности, в этой главе будут рассматриваться следующие темы. □ Как создаются рабочие потоки. □ Что собой представляет действие и как пользоваться несколькими встроенными действиями. □ Как обрабатывать неисправности в рабочем потоке. □ Как пользоваться службой обеспечения постоянства рабочих потоков. □ Как передавать параметры рабочему потоку. практическое занятие пример "Hello World" (в стиле рабочего потока) Ни одно описание нового принципа программирования не бывает полным без примера программы "Hello World", поэтому чтобы не нарушать традицию, ниже предлагается реализовать этот традиционный пример с использованием Windows Workflow Foundation. 1. В Visual Studio 2008 доступно несколько новых типов проектов, поэтому чтобы создать новый проект типа рабочего потока, выберите в древовидном представлении Project Types (Типы проектов) элемент Workflow (Рабочий поток), а затем в представлении Templates (Шаблоны) — шаблон Sequential Workflow Console Application (Консольного приложение последовательного рабочего потока), как показано на рис. 36.1. Обратите внимание на раскрывающийся список в правом верхнем углу показанного на рис. 36.1 окна: он является новым средством в Visual Studio 2008 и позволяет выбирать в качестве целевой сразу несколько версий .NET Framework из одной и той же копии Visual Studio. Проект, созданный на этом первом шаге, приведет к созданию целого ряда файлов, которые будут рассматриваться далее в этом разделе. Самым интересным является сам рабочий поток, который показан на рис. 36.2. Как видно на этом рисунке, рабочий поток такого типа похож на блок-схему, поскольку тоже имеет начальную и конечную фигуру с областью посередине, куда можно перетаскивать действия.
1178 Часть V. Дополнительные технологии | л Visual С* Windows Web 0ff.ee Smart Device Database Test WCf jafl Database Project* Other Languages Distributed System* Other Project Type» L....TestPic««?s. Vnutl Studio installed templates .^i Empty Workflow Project ^Sequential Workflow Console AppUcet.. ^ Sequent lei Workflow Library Je1 State Mach.ne Workflow Console Appl 2? State MKhm Workflow Library 9 Workflow Activity librtry My TempUtes J] Search Online TempUtes... A project for creating ■ sequent*! workflow console ee«*c«ben. (J«T Ffemtwerk 33) D:\LKe«\Mo«ganvDocun»er*s\HwTie\o>gmning C*\Workflow\Code -» [^Jeajpjjj, j ■ С icate directory for solutoon - Рис. 36. 1. Создание нового проекта типа Sequential Workflow Console Application 2. Продолжая создавать пример "Hello World", добавьте действие, каковым в данном случае должно быть Code (Код), доступное в окне Toolbox. На рис. 36.3 показано, как будет выглядеть рабочий поток после добавления этого действия. Последовательный рабочий поток ф Область, в которую можно перетаскивать действия Последовательный рабочий поток О £J codeActlvityl —^ /ityl Рис. 36.2. Последовательный рабочий поток Рис. 36.3. Рабочий поток после добавления действия Code Обратите внимание на восклицательный знак в правом верхнем углу блока, представляющего действие: он указывает на то, что для этого свойства еще не было установлено обязательное свойство, каковым в данном случае является метод, который должен вызываться при выполнении данного действия. 3. Выполнение двойного щелчка на действии Code позволит сгенерировать необходимый метод автоматически, как показано в следующем фрагменте кода: public sealed partial class Workflowl: SequentialWorkflowActivity { public WorkflowlO { InitializeComponent();
Глава 36. Windows Workflow Foundation 1179 private void codeActivityl_ExecuteCode(object sender, EventArgs e) Здесь видно, что автоматически был создан метод codeActivityl_ExecuteCode. Еще обратите внимание на то, что рабочий поток унаследован от класса SequentialWorkf lowActivity, а это значит, что он сам тоже является, по сути, всего лишь еще одной разновидностью действия. 4. Теперь добавьте в этот код вызов метода Console.WriteLine для вывода традиционного приветствия "Hello World": private void codeActivityl_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Hello World"); } Скомпилируйте и запустите приложение, чтобы увидеть, как оно выведет текст на консоль. Описание полученных результатов Основная программа, созданная после того, как вы выбрали проект Sequential Workflow Console Application, представляет собой экземпляр объекта Workf lowRuntime. Этот объект координирует выполнение всех рабочих потоков и может применяться для запуска их экземпляров. В данном примере он создает экземпляр класса Workf 1 owl и выполняет его. Сначала исполняющая среда выполняет первое действие в рабочем потоке (каковым в данном случае является сам рабочий поток). Далее она выполняет каждое действие по очереди, в результате чего вызывается действие CodeActivity, которое, в свою очередь, выполняет метод, куда был добавлен вызов метода Console .WriteLine. По завершении выполнения рабочего потока консольное приложение закрывается. Весь код этого основного приложения показан ниже: static void Main(string[] args) { using(WorkflowRuntime workflowRuntime = new WorkflowRuntime()) { AutoResetEvent waitHandle = new AutoResetEvent(false); workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) {waitHandle.Set();}; workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e) { Console.WriteLine(e.Exception.Message); waitHandle.Set() ; }; Workflowlnstance instance = workflowRuntime.CreateWorkflow (typeof(_l_HelloWorld.Workflowl)); instance.Start() ; waitHandle.WaitOne(); }
1180 Часть V. Дополнительные технологии Здесь сначала внутри блока using создается объект Workf lowRuntime и объект AutoResetEvent, который будет использоваться для подачи сигнала о создании и о завершении выполнения рабочего потока. Для обеспечения возможности ожидания таких сигналов присоединяются обработчики для, соответственно, событий WorkflowCompleted и WorkflowTerminated. Далее создается и запускается экземпляр Workf lowlnstance — делается это за счет согласования экземпляра с расписанием исполняющей среды и последующего его запуска вызовом метода Start (). Напоследок консольное приложение ожидает поступления сигнала о событии, каковым может быть либо успешное завершение выполнения рабочего потока, либо его неожиданное прерывание (т.е. генерация исключения во время выполнения какого-то из действий в рамках этого рабочего потока). Класс Workf lowRuntime еще будет рассматриваться чуть позже в этой главе. Действия Строительными блоками любого рабочего потока являются действия (да и сам рабочий поток тоже на самом деле является таковым). Каждое действие (activity) представляет собой .NET-класс, который обычно наследуется от класса System. Workf low. ComponentModel .Activity. Несколько других классов могут улучшать поведение действия, например, изменять его внешний вид на этапе проектирования или обеспечивать его верификацию на предмет определения всех обязательных свойств. Всего в составе .NET 3.5 поставляется 30 действий; в настоящем разделе будут рассмотрены некоторые из них и показано, как их использовать в приложении. Действие CodeActivity уже встречалось в настоящей главе, в частности, оно использовалось в предыдущем разделе для отображения кое-какого вывода в окне консоли. В большинстве случаев, однако, лучше создавать специальные действия, а не применять CodeActivity, поскольку их можно использовать повторно и в других рабочих потоках. Что касается действия CodeActivity, то его повторно использовать можно только при копировании файла отделенного кода. Действия в WF бывают двух видов: простые и составные. Простые действия вроде CodeActivity наследуются, как уже упоминалось, от базового класса Activity, и отображаются в визуальном конструкторе рабочего потока в виде одиночного элемента. Составные действия, в отличие от них, служат, по сути, контейнерами для других действий. Сначала в этом разделе мы рассмотрим некоторые простые действия, затем — составные, а потом покажем, как создавать свои собственные. Действие DelayActivity Действие DelayActivity может использоваться в рабочем потоке для перевода его в режим ожидания на определенный период времени. Подобное может быть удобно, например, в случае применения рабочих потоков для напоминания об обновлении страховых полисов. При получении нового полиса может запускаться рабочий поток напоминания с 11-месячной задержкой в качестве первого действия. По истечения этого периода задержки может выполняться подготовка документов на возобновление действия полиса и их автоматическая рассылка держателям страховым полисов. На рис. 36.4 показаны свойства, доступные для DelayActivity. Если требуемый период задержки известен (например, два часа), тогда можно изменять свойство TimeoutDuration и указывать время в формате {дни} ЧЧ:ММ:СС, причем часть {дни} здесь является необязательной.
Глава 36. Windows Workflow Foundation 1181 deUyActtvityl System Wor«cflo*.Activrties.D « I S«quent,al Workflow l ^ J Щ dcUyAc tivity 1 Description Enabled True InrtialueTimeoutDurttion i TimeoutDuration I 00:00.00 I I a | Рис. 36.4. Свойства Delay Activity При выполнении действие DelayActivity, по сути, приостанавливает рабочий поток на указанный период времени и затем, по истечении этого периода задержки, возобновляет его работу со следующего действия. Длительностью периода задержки можно управлять с помощью свойства Initialize TimeoutDuration, которое привязывается к обработчику событий в файле отделенного кода. Добавляется обработчик событий двойным щелчком на DelayActivity и выглядит так, как показано ниже: private void delayActivityl_InitializeTimeoutDuration (object sender, EventArgs e) { } Здесь первым аргументом является DelayActivity, а это означает, что можно добавлять код, подобный показанному ниже, для инициализации свойства TimeoutDuration любым желаемым значением. В данном примере оно устанавливается равным 10 секундам: private void delayActivityl_InitializeTiroeoutDuration (object sender, EventArgs e) { DelayActivity delay = sender as DelayActivity; if (null != delay) delay.TimeoutDuration = new TimeSpan@, 0, 0, 10); } Обратите внимание, что аргумент sender нужно обязательно приводить к типу экземпляра DelayActivity. Когда рабочий поток находится в состоянии задержки, он фактически ничего не делает: механизм таймера внутри исполняющей среды рабочего потока знает, как обращаться с задержками, и умеет "пробуждать" спящий поток по истечении его периода задержки. Более подробно об этом будет рассказываться позже в этой главе, в разделе "Исполняющая среда рабочего потока". Действие SuspendActivity При выполнении рабочего потока может быть необходимо приостанавливать его выполнение: например, может быть нужно, чтобы в случае недоступности службы базы данных в текущий момент выполнение рабочего потока приостанавливалось до тех пор, пока эта служба не станет доступной снова. Действие приостановки (SuspendActivity), как видно на рис. 36.5, имеет свойство Error, для которого в качестве значения может устанавливаться строка с описанием причины приостановки рабочего потока. '«
1182 Часть V. Дополнительные технологии Sequential Workflow ■ -i—=i || suspendActivityl _____ I в Рис. 36.5. СвойстваSuspendActivity В случае приостановки рабочего потока в Workf lowRuntime генерируется событие Workf lowSuspended, и на этом этапе как раз и можно использовать сообщение об ошибке для принятия решения о том, какое действие должно быть выполнено дальше. Приостановленный рабочий поток не может возобновлять свою работу самостоятельно — поэтому требуется получать экземпляр рабочего потока и вызывать для него метод Resume (). С учетом вероятности выполнения в одно и то же время многих экземпляров одного и того же рабочего потока, возникает вопрос о том, а как из них всех можно вычленять именно тот, который был приостановлен? При запуске любого рабочего потока ему присваивается уникальный идентификатор Guid. Для этого идентификатора Guid при запуске экземпляра рабочего потока можно устанавливать свое значение, как показано в приведенном ниже коде; в случае отсутствия такого кода, однако, исполняющая среда (Workf lowRuntime) будет создавать уникальный идентификатор самостоятельно. Guid thelnstanceld = Guid.NewGuidO; Dictionary<string, object> parms = new Dictionary<string,object> (); Workflowlnstance instance = workflowRuntime.CreateWorkflow (typeof(_3_Suspend.Workflowl), parms, thelnstanceld); instance.Start(); Здесь функция Guid.NewGuidO создает для рабочего потока новый уникальный идентификатор, который передается в третьем параметре вызову CreateWorkf low. Параметр dictionaryo передает в экземпляр рабочего потока данные, о чем более подробно будет рассказываться позже в этой главе. Наличие такого уникального идентификатора позволяет далее запрашивать у Workf lowRuntime конкретный экземпляр Workf lowlnstance на основании этого идентификатора: Workflowlnstance wf = workflowRuntime.GetWorkflow(thelnstanceld); wf .Resume () ; Метод GetWorkf low () принимает параметр Guid и пытается отыскать экземпляр с таким идентификатором. В случае передачи ему недействительного значения Guid генерируется исключение System. InvalidOperationException. Действие TerminateActivity работает похожим образом, но только не приостанавливает, а немедленно прекращает выполнение экземпляра рабочего потока и генерирует в исполняющей среде событие Workf lowTerminated. После завершения выполнения рабочего потока подобным образом его возобновление уже не возможно. «nperaMctMtyl System.Workflow.Compot * Description Enabled True Error a I Mi Bee h
Глава 36. Windows Workflow Foundation 1183 Действие WhileActivity Действие WhileActivity функционирует точно так же, как и традиционный оператор while в языке С#, а именно— позволяет выполнять несколько действий в цикле. Его главное отличие заключается в том, что оно представляет собой составное действие, т.е. такое, которое может содержать другие действия. На рис. 36.6 показано, как действие WhileActivity и его свойства выглядят после перетаскивания в окно визуального конструктора. Sequential Workflow й I 0 wMeActinty 1 О*— Drop an Activity Htr* whfeActivTtyl System.Wortflow.Act.vit.es.V < Condition Description Enabled True Puc. 36.6. Свойства WhileActivity Свойство Condition является обязательным и задает критерии, на основании которых должно приниматься решение о том, следует ли еще раз входить в цикл. Оно может определяться как в коде, так и декларативно, и здесь описаны оба способа. Определять условие в коде можно путем разворачивания раскрывающегося списка рядом со свойством Condition и выбора в нем варианта Code Condition (Кодовое условие), а затем ввода имени того метода, который должен использоваться для вычисления этого условия во время выполнения. Это приводит к генерации лежащего в основе кода метода, как показано ниже: private void NotFinished(object sender, ConditionalEventArgs e) { e.Result = _howManyTimes++ < 5; Этому методу передается параметр ConditionalEventArgs, который используется для возврата WhileActivity значения true или false. Возврат для свойства Result объекта ConditionalEventArgs значения true приводит к продолжению прогона цикла, а значения false — к выходу из него. В предыдущем коде предполагается наличие целочисленной переменной _howManyTimes, которая используется для отсчиты- вания пяти итераций. Определять условие декларативным образом можно путем выбора из раскрывающегося списка рядом со свойством Condition элемента Declarative Rule Condition (Условие с декларативными правилами) и выполнения щелчка на кнопке с изображением троеточия (...) рядом со свойством Condition Name. Это приводит к открытию диалогового окна Select Condition (Выбор условия), которое показано на рис. 36.7 и позволяет определять условие на базе свойств текущего рабочего потока.
1184 Часть V. Дополнительные технологии Select Condition Select • rule condition to be assigned to the activity's condition. You can also add. edit, delete or rename existing conditions. ЪМиь. •name... л Qelete i Puc. 36.7. Диалоговое окно Select Condition Создание нового условия осуществляется щелчком на кнопке New (Создать) для открытия диалогового окна Rule Condition Editor (Редактор условия с декларативными правилами) и ввода желаемого условия, как показано на рис. 36.8. Rule Condition Editor W Edit constraints to create a rule condition. —^ Example thrs.Propl = = "Yes" || thrs.Prop2 = = 2 ^his.LoopCount < 5 Puc. 36.8. Диалоговое окно Rule Condition Editor Это диалоговое окно позволяет вводить кодовые выражения, которые должны вычисляться во время выполнения. В данном примере в качестве такого выражения определяется свойство по имени LoopCount для выполнения проверки на предмет того, является ли количество прогонов цикла меньше 5. Если вы скомпилируете этот код, то обнаружите, что выход из цикла не будет происходить никогда, а все потому, что в нем пока нет никакого действия, которое бы приводило к обновлению значения свойства LoopCounter. Это означает, что в рабочий поток необходимо добавить действие CodeActivity, способное обновлять это значение при каждом прохождении цикла. Код рабочего потока тогда должен выглядеть так, как показано ниже:
Глава 36. Windows Workflow Foundation 1185 public sealed partial class Workflowl: SequentialWorkflowActivity { public WorkflowlO { InitializeComponent (); } public int LoopCount { get; set; } private void incCounter_ExecuteCode(object sender, EventArgs e) { LoopCount++; } } Все декларативные правила сохраняются вместе с рабочим потоком в файле с расширением .rules. Увидеть этот файл можно, развернув рабочий поток в окне Solution Explorer, как показано на рис. 36.9. ^ J Л ^Э Solution 05 '.VMeDedarati-.e A project! 3 05 WhieDecWative Ш ^М Properties I ^ References cjJ Program.cs - 4Й Workflowl.es ^ Workflowl.designer.es Рис. 36.9. Файл . rules в окне Solution Explorer Этот файл содержит все касающиеся конкретного потока правила в формате XML. Эти правила вычисляются во время выполнения (т.е. в формат .NET-кода они не преобразуются), так что лучше использовать их как можно реже, потому что работать так же быстро, как кодовые условия, они не будут. Однако стоит отметить, что конечным пользователям, конечно же, легче определять правила декларативно, чем писать и компилировать код. Действие WhileActivity принимает только одно единственное действие в качестве непосредственного потомка, т.е., например, включать в него напрямую одновременно и действие С ode Activity, и действие DelayActivity нельзя. На первый взгляд это может показаться очень ограничивающим фактором для использования While, но перетаскивание действия Sequence Activity позволяет добавлять в него и другие действия. Действие SequenceActivity Действие SequenceActivity представляет собой еще одно составное действие, которое позволяет добавлять внутри него любое количество дочерних действий. По самому его названию понятно, что оно последовательно выполняет эти дочерние действия. В примере с WhileActivity оно применялось для размещения все действий, которые должны были выполняться внутри тела цикла. Как и у других составных действий, у SequenceActivity также имеется два других представления. Доступ к ним можно Рис. 36.10. получать щелчком на доступном рядом с любым составным дей- Раскрывающийся ствием раскрывающемся списке. На рис. 36.10 показано, где он список действия находится в случае действия SequenceActivity. t SequenceActivity —\ Э sequenceActivityl S3
1186 Часть V. Дополнительные технологии В раскрывающемся списке действия SequenceActivity доступно три варианта: View Sequence (Вид последовательности), View Cancel Handler (Вид обработчика операций отмены) и View Fault Handlers (Вид обработчиков неисправностей). На рис. 36.11 показаны представления обработчика операций отмены и неисправности. SequenceActivityl в cancellationHandlerActivity Сюда можно w перетаскивать действия 1 SequenceActivity2 faultHandlersActivityl ер Сюда можно перетаскивать действия типа FaultHandlerActivity 1 Сюда можно перетаскивать действия типа FaultHandlerActivity 1 О Рис. 36.11. Обработчики операций отмены и неисправности При написании .NET-кода его обычно заключают внутрь блоков try /catch. В блоке try, как правило, выполняется такой код, который может выдавать исключение, а в блоке catch (каковых может быть и несколько) определяется то, что должно происходить в случае выдачи исключения. Следствием этого в WF является применение в определенном составном действии обработчиков неисправностей. Блок try в таком экземпляре состоит из того действия или действий, которые находятся внутри основного тела составного действия — в случае SequentialActivity таковыми являются те действия, которые находятся внутри представления по умолчанию, а в случае WhileActivity — те, которые находятся внутри блока while. Обработка неисправностей sequenceActivity2 0 faultHandlersActivityl ©IS® I© argumentException ♦ 1 - handlelnvalidArg j i Puc. 36.12. Пример добавления обработчиков исключений System. ArgumentException и Systern.Exception Определять блок catch (или набор блоков catch) можно путем отображения представления обработчиков неисправностей и последующего перетаскивания любого количества экземпляров FaultHandler Activity в область с пометкой Drop FaultHandlerActivity Here (Сюда можно перетаскивать действия типа Fault HandlerActivity), которая показывалась на рис. 36.11. В примере на рис. 36.12 обработчики определяются для двух исключений, которые могут выдаваться в SequenceActivity, а именно— для исключения System.ArgumentException и для исключения System.Exception. При перетаскивании действия FaultHandlerActivity на поверхность конструктора необходимо обязательно задавать для него свойство Fault Type — оно позволяет указывать тип перехватываемого исключения. После этого внутри тела действия уже можно определять то действие или действия, которые должны выполняться в случае генерации исключения такого типа.
Глава 36. Windows Workflow Foundation 1187 В сущности все это приводит к созданию блока try/catch, как показано в следующем фрагменте кода: try // Выполнение действий внутри тела SequentialActivity catch ( System.ArgumentException argex ) // Действия, подлежащие выполнению в случае // генерации исключения ArgumentException catch ( System.Exception ex ) // Действия, подлежащие выполнению в случае генерации события Exception В примере 06 CancellationAndFaults, который доступен в загружаемом коде для этой книги, рабочий поток определен так, как показано на рис. 36.13. В теле действия tryAndCatch (представляющего собой экземпляр SequentialActivity) содержится действие типа CodeActivity, отображающее в окне консоли сообщение о том, что сейчас будет сгенерировано исключение. После него используется действие ThrowActivity для генерации исключения System.ArgumentException. Последнее действие — after Throw — не выполняется, поскольку исключение к этому моменту уже создано. Sequential Workflow Sequential Workflow I a tryAndCatch a i 1 ■ 0 i -a tryAndCatch 51 faultHandlenActivityl ®(Ш 1° faultHancHerActrvityl 1 i l2 Рис. 36.13. Пример 06 CancellationAndFaults Исключение ArgumentException перехватывается внутри обработчика неисправностей, после чего снова выполняется действие типа CodeActivity, которое отображает в окне консоли сообщение, но на этот раз о том, что исключение было перехвачено. Наличие такого кода позволяет рабочему потоку продолжать успешно выполняться до самого конца SequenceActivity, потому что любое генерируемое исключение перехватывается и обрабатывается. Обработчики неисправностей могут определяться в любом составном действии, а это означает, что обработчик неисправностей может определяться и в самом рабочем потоке для перехвата любых исключений, которые поступают из действий, выполняющихся внутри него. В случае отсутствия обработчика на таком высоком уровне, исключения будут выходить за пределы рабочего потока и перехватываться'экземпляром Workf lowRuntime,
1188 Часть V. Дополнительные технологии что будет приводить к завершению рабочего потока и генерации события Workflow Terminated. Код, добавляемый в консольное приложение для этого события по умолчанию, предусматривает лишь вывод в окно консоли сообщения о возникновении исключения, как показано ниже: workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e) { Console.WriteLine(e.Exception.Message); waitHandle.SetO ; }; Здесь предполагается, что в параметре Workf lowTerminatedEventArgs содержится исключение, которое было выдано, а также экземпляр Workf lowlnstance, который привел к его выдаче. Предоставление кода очистки В предыдущем разделе было показано, как исключения могут генерироваться и обрабатываться в составных событиях, но существует еще один круг вопросов, требующих рассмотрения, и касается он обеспечения в рабочем потоке поведения, подобного логике отмены. В рассматриваемом здесь примере рабочего потока для этого применяется действие ParallelActivity, которое позволяет выполнять действия в параллельной манере, как показано на рис. 36.14. 1 ' в parallelActw 1 sequenceActivityl ■ 1 Drop Activities Here 4 ityl 1 sequenceActivity2 31 1 Drop Activities Here 4 1 Puc. 36.14. Действие ParallelActivity Это действие состоит из, минимум, двух ветвей SequentialActivity, которых можно добавлять и больше с помощью его контекстного меню. Внутри этих ветвей может присутствовать множество действий, но что же будет происходить в случае выдачи исключения во второй ветви и необходимости выполнения отмены ряда действий, которые уже успели выполниться в первой ветви? Это и есть то самое место, где может пригодиться обработчик операций отмены. В случае генерации исключения в одной ветви ParallelActivity все остальные ветви уведомляются об этом факте выполнением в них операций отмены. Если обработчик операций отмены был определен, тогда именно он и вызывается. Он представляет собой еще один набор действий, который, по сути, планирует их выполнение. Внутри него можно предоставлять любую необходимую логику, например, в случае отправки электронного сообщения в левой ветви ParallelActivity, в этом обработчике можно обеспечить отправку другого сообщения, указывающего получателю не обращать внимания на то электронное сообщение, которое было послано ранее. Что касается действия ParallelActivity, то стоит обратить внимание, что его название является не совсем корректным. Если вы подумали, что это действие выполняет свои дочерние действия параллельным образом и, возможно, использует потоки
Глава 36. Windows Workflow Foundation 1189 (thread) для планирования выполнения действий в каждой ветви, то это не так. На самом деле Parallel Activity использует только один поток и, в сущности, выполняет сначала первое действие в первой ветви, затем второе действие во второй ветви и т.д. Очень важно разобраться в этом — дизайнеры WF выбрали этот подход для упрощения программирования рабочих потоков таким образом, чтобы конечным пользователям не требовалось ничего знать ни о потоковой обработке, ни о блокировках. При желании, чтобы части рабочего потока все-таки действительно выполнялись параллельно, можно использовать действие InvokeWorkf lowActivity для создания нового рабочего потока (или ряда рабочих потоков). Исполняющая среда тогда будет планировать выполнение нескольких рабочих потоков в разных потоках (возможно, даже с использованием разных процессоров в случае компьютера с несколькими процессорами). Специальные действия Пока что в этой главе речь шла только о встроенных действиях. В настоящем разделе будет показано, как создавать свои собственные действия на примере создания действия, предназначенного для оказания помощи в отладке рабочих процессов путем отображения соответствующего вывода в окне консоли. В предыдущих примерах для отображения сообщений в окне консоли применялось действие CodeActivity, но вместо него было бы гораздо лучше создать действие со строковым свойством, определяющим выводимое сообщение — тогда не нужно было бы создавать никакого отделенного кода для отображения сообщений, поскольку это действие могло бы справляться со всем самостоятельно. Все действия, в конечном счете, наследуются от класса System. Workflow. ComponentModel .Activity, и именно он будет использоваться в качестве базового класса в следующем практическом занятии. ^шяшяштшшшшштшшшшяшшшштшшшштшввшшшшшвттштшшяшттвшшшшшшштнтшшшшвт Практическое занятие СоЗДЭНИе Специального ДвЙСТВИЯ 1. Создайте новый проект типа консольного приложения для последовательного рабочего потока (Sequential Workflow Console) и добавьте в него новый класс по имени DebugActivity. Этот класс должен наследоваться от базового класса Activity, поэтому введите следующий код и затем добавьте конструкцию using для System.Workflow.ComponentModel: public class DebugActivity : Activity { } 2. Определите в этом классе свойство, в котором будет храниться сообщение, отображаемое при выполнении данного действия, добавив строковое свойство по имени Message, как показано ниже: public class DebugActivity : Activity { public string Message { get; set; } } 3. При настройке действия на выполнение исполняющей средой рабочего потока вызывается его метод Execute. Поэтому добавьте такой метод, как показано в приведенном ниже коде, или же просто введите слова override Execute и нажмите клавишу <Enter>, если хотите, чтобы этот метод был создан автоматически. Этот метод должен возвращать исполняющей среде рабочего потока какое-то значение для уведомления о том, что он завершил выполнение, и метод
1190 Часть V. Дополнительные технологии Execute базового класса уже это делает, из-за чего и вызывается в конце данного метода: public class DebugActivity : Activity { public string Message { get; set; } protected override ActivityExecutionStatus Execute (ActivityExecutionContext executionContext) { return base.Execute(executionContext); } } 4. Теперь добавьте код, способный отображать сообщение во время отладки, но пропускать его при выполнении без присоединенного отладчика. Для вывода текста можно было бы использовать метод Debug.WriteLine (), но это метод условно компилируется в рабочих сборках, из-за чего в случае сборки рабочей версии библиотеки, содержащей DebugActivity, не будет отображаться никакого вывода. Поэтому вместо него необходим какой-то другой способ для определения того, выполняется ли рабочий поток с присоединенным отладчиком. У класса Debugger в пространстве имен System. Diagnostics имеется свойство IsAttached, для которого значение true устанавливается только в том случае, если текущее приложение проходит отладку, поэтому можно просто обеспечить проверку этого свойства и сделать так, чтобы сообщение выводилось на экран только в случае, если значение этого свойства равно true. Ниже приведен весь необходимый готовый код для DebugActivity: public class DebugActivity : Activity { public string Message { get; set; } protected override ActivityExecutionStatus Execute (ActivityExecutionContext executionContext) { if (Debugger.IsAttached) { Console.WriteLine(Message); Trace.WriteLine(Message); } return base.Execute(executionContext); } } Как видно, в этом коде для вывода сообщения используются два метода — Console.WriteLine() и Trace.WriteLine(), один из которых выводит сообщение в окно консоли, а второй — в окно Output в Visual Studio. При перетаскивании действия DebugActivity из панели инструментов в область визуального конструктора рабочего потока, оно будет выглядеть так, как показано на рис. 36.15. Это действие выглядит несколько просто по сравнению со стандартными действиями и не показывает, что свойство Message является обязательным, из-за чего можно забыть установить это свойство и потом удивляться, почему во время отладки не появлялся никакой вывод. То, как можно устранить обе эти проблемы, будет показано в следующем практическом занятии. Sequential Workflow I ( ^ -Jj debugActivityl V / 1 В | Рис. 36.15. Действие DebugActivity после перетаскивания в область визуального конструктора рабочего потока
Глава 36. Windows Workflow Foundation 1191 Верификация действий У многих из встроенных действий имеются обязательные свойства, обозначаемые на поверхности визуального конструктора восклицательным знаком, который отображается в правом верхнем углу действия. Для обеспечения подобной возможностью специальных действий потребуется создать специальный класс проверки достоверности (validator). Такой класс должен наследоваться от System. Workflow . ComponentModel. Compiler.ActivityValidtor и реализовать метод Validate. Этот метод будет вызываться как при первоначальном перетаскивании действий на поверхность визуального конструктора, так и при изменении их свойств. Метод Validate может возвращать любое количество ошибок проверки достоверности, ведь у проверяемого свойства может быть и несколько обязательных свойств. После написания верификатор ассоциируется с действием с применением атрибута ActivityValidator. Увидеть, как это выглядит на практике, можно в следующем разделе, где приводится пример добавления верификатора действий. Добавление верификатора В следующем коде показана реализация верификатора для действия DebugActivity. При вызове метода Validate ему в качестве второго параметра передается действие, а поскольку это должно быть действие DebugActivity, выполняется соответствующая проверка и в случае, если верификатор был по ошибке присоединен к другому классу, генерируется исключение: public class DebugActivityValidator : ActivityValidator { public override ValidationErrorCollection Validate (ValidationManager manager, object obj) { DebugActivity act = ob] as DebugActivity; if (null == act) throw new ArgumentException("This validator is only designed to validate DebugActivity instances"); // Этот верификатор предназначен // только для проверки // экземпляров DebugActivity // Извлечение списка возможных ошибок из базового класса ValidationErrorCollection errors = base.Validate(manager, obj) ; // Выполнение проверки на предмет наличия родителя if (null != act.Parent) { // OK - проверка свойства Message if (string.IsNullOrEmpty(act.Message)) errors.Add( ValidationError.GetNotSetValidationError("Message")); } return errors; } } После проверки допустимости параметра obj, вызывается метод Validate базового класса. Это гарантирует выполнение верификации также и любых наследуемых свойств.
1192 Часть V. Дополнительнее технологии Верификаторы обычно запускаются при компиляции кода и без проверки, подтверждающей наличие у действия родителя, могут на самом деле даже выдавать ошибки и просто во время компиляции класса, от которых нет никакой пользы. Поскольку у действия всегда есть родитель, в данном коде первым делом, до осуществления любых последующих операций по верификации, выполняется именно проверка наличия такового. Далее необходимо удостовериться в том, что пользователь действительно ввел какое-нибудь значение в свойстве Message действия DebugActivity, поэтому затем в коде выполняется проверка на предмет того, не содержится ли в этом свойстве значение null или пустая строка. Если никакого сообщения не было задано, вызывается метод ValidationError. GetNotSetValidationError () с именем незаданного свойства в качестве параметра. Напоследок остается только связать этот верификатор с действием DebugActivity путем добавления соответствующего атрибута в само действие. Чтобы сделать это, введите следующий код: [ActivityValidator(typeof(DebugActivityValidator))] public class DebugActivity : Activity После добавления такого кода вы можете скомпилировать приложение и убедиться в том, что верификация свойства Message действительно происходит. Перетащите на поверхность визуального конструктора новое действие DebugActivity— напротив него должен появиться указывающий на ошибку верификации символ восклицательного знака (рис. 36.16). Щелкните на нем. Появится раскрывающееся меню с наименованием ошибки или ошибок, которые были выявлены в действии. Поскольку в данном примере был использован метод Sequential Workflow J 1 * \ С i \i debugActivityl V ) 1 Рис 36 16 GetNotSetValidationError (), вы можете щелкнуть на каком- ТестиЬовапие Ъабо- то ваРианте в этом меню и в результате перейти прямо в ячейку тоспособности веЬи- соответствующего свойства. Обратите внимание то, что ошибка фикатора действий верификации будет также отображаться и в ячейке свойства, DebuqActivity и что ПРИ навеДении курсора мыши на восклицательный знак внутри такой ячейки будет появляться всплывающая подсказка, содержащая сообщение об ошибке. Если вы теперь определите значение для свойства Message, верификатор будет вызван снова: он удостоверится в том, что на этот раз были определены все обязательные свойства, и удалит ошибку верификации из пользовательского интерфейса. Обратите внимание, что при наличии ошибок верификации в каких-нибудь действиях успешно скомпилировать само приложение тоже не получится. Улучшение внешнего вида на этапе проектирования По сравнению со встроенными действиями действие DebugActivity выглядит на этапе проектирования заметно проще: оно отображается на белом фоне и сопровождается стандартной обобщенной пиктограммой. В настоящем разделе будет показано, как улучшить его внешний вид, добавляя код для прорисовывания фона другого цвета и замены стиля контура пунктиром. Изменение поведения, отвечающего за прорисовывание действия, осуществляется подобно добавлению верификатора, а именно — созданием пары классов и связыва-
Глава 36. Windows Workflow Foundation 1193 нием их с действием (каковым в данном случае является DebugActivity) с помощью соответствующих атрибутов. В WWF за визуализацию действия отвечают два класса: Activity Designer и ActivityDesignerTheme. Класс ActivityDesigner позволяет иметь полный контроль над всеми аспектами визуализации и, как правило, применяется для улучшения общего поведения на этапе проектирования за счет добавления дополнительных глифов (графических элементов) на поверхность действия. Путем написания класса конструктора можно полностью изменять визуальное представление действия, но для целей, преследуемых в данном примере, т.е. для изменения цвета фона и кое-каких других простых аспектов действия, вполне хватит класса ActivityDesignerTheme. практическое занятие Добавление конструктора и темы Выполните следующие шаги, чтобы добавить конструктор и тему для действия DebugActivity и тем самым улучшить его внешний вид на этапе проектирования. 1. Создайте класс темы, как показано в следующем коде: public class DebugActivityTheme : ActivityDesignerTheme { public DebugActivityTheme(WorkflowTheme theme) : base(theme) { this.BackColorStart = Color.Green; this.BackColorEnd = Color.Yellow; this.BackgroundStyle = LinearGradientMode.ForwardDiagonal; this.BorderStyle = DashStyle.Dot; Здесь создается класс, унаследованный от ActivityDesignerTheme, вместе с конструктором, принимающим в качестве единственного параметра экземпляр класса Workf lowTheme, что является обязательным для всех классов тем. В рамках конструктора устанавливаются свойства вроде начального и конечного цвета фона и стиля. Они обеспечивают визуализацию данного действия с использованием градиентной кисти и пунктирного штриха для границ. 2. После написания класса темы можно переходить к написанию класса конструктора. В данном случае нужен не более чем просто пустой класс, поскольку перекрывать какие-то методы конструктора с целью предоставления темы для действия не требуется. Наличие такого класса, однако, все-таки является необходимым. Поэтому создайте его так, как показано ниже: [ActivityDesignerTheme(typeof(DebugActivityTheme))] public class DebugActivityDesigner : ActivityDesigner { } Обратите внимание на присутствие в этом коде атрибута ActivityDesignerTheme: он как раз и служит гарантом того, что поверхность визуального конструктора рабочего потока сможет находить соответствующий класс темы на этапе проектирования. 3. Напоследок осталось только связать специальный конструктор с самим действием. Для этого необходимо использовать атрибут:
1194 Часть V. Дополнительные технологии DebugActivity [ActivityValidator(typeof(DebugActivityValidator))] [Designer(typeof(DebugActivityDesigner))] public class DebugActivity : Activity Здесь был добавлен атрибут Designer типа DebugActivity Designer. Если вы теперь скомпилируете приложение и перетащите на поверхность визуального конструктора действие DebugActivity (или просто откроете какой-то существующий рабочий поток, содержащий такое действие), оно будет иметь более совершенный вид, как показано на рис. 36.17. В случае внесения в тему изменений и перекомпиляции действия может возникнуть вопрос о том, почему эти изменения не отражаются сразу же внутри Visual Studio. Дело в том, что когда Рис 36 17 среда Visual Studio обнаруживает конструктор для конкретного Улучшенный внеш- типа, она кэширует его внутренним образом, поэтому единст- ний вид действия венным способом обновить представление является перезапуск Visual Studio. В случае удаления из кода действия атрибута Designer и его повторной компиляция в пользовательском интерфейсе (UI) все равно будет отображаться старый конструктор, поэтому перезапуск Visual Studio является единственным возможным вариантом. Исполняющая среда рабочего потока Класс Workf lowRuntime может размещаться в консольном приложении, как показывалось до сих пор в этой главе, но вообще может размещаться в любом .NET-прило- жении, например, в приложении Windows Forms на клиентской машине или внутри сайта ASP.NET для того, чтобы рабочие потоки могли выполняться на основании действий, которые пользователь предпринимает на Web-сайте. Исполняющая среда рассчитана на использование во всех этих средах и обладает специфической поддержкой для каждого типа приложения. Помимо рабочих потоков, исполняющая среда еще обслуживает ряд служб, которыми пользуется сама и которые также могут использовать и рабочие потоки по мере своего выполнения. Каждая такая служба представляет собой экземпляр .NET-класса, который был зарегистрирован с исполняющей средой. Пожалуй, наиболее полезной из этих служб является служба обеспечения постоянства (persistence service), которая берет простаивающий рабочий поток и сохраняет его на диске. Подобное обычно происходит, когда в рабочем потоке присутствует задержка, ведь хранить рабочий поток в памяти, пока он находится в спящем состоянии, нет никакой необходимости, а раз так, значит, служба обеспечения постоянства может упаковывать состояние рабочего потока и сохранять его на диске. В плане программирования в .NET служба обеспечения постоянства следует общей модели, т.е. подразумевает использование абстрактного класса, от которого путем наследования можно создавать другой класс и предоставлять специфическую реализацию. Абстрактным классом в данном случае является Workf lowPersistenceService, а унаследованным классом, обеспечивающим специфическую реализацию, например, сохранение данных службы в базе данных SQL— SqlWorkf lowPersistenceService.
Глава 36. Windows Workflow Foundation 1195 При желании, чтобы рабочие потоки сохранялись в каком-то другом хранилище, можно создавать свои собственные экземпляры службы обеспечения постоянства и подключать их к исполняющей среде рабочих потоков либо в коде, либо в конфигурационном файле. В следующем практическом занятии демонстрируется пример создания рабочего потока с действием DelayActivity и обеспечением возможности его сохранения в базе данных SQL Server. Сначала создается новая база данных, а затем выполняются два поставляемых с WF сценария для создания таблиц и хранимых процедур, необходимых службе обеспечения постоянства. Практическое занятие Использование службы обеспечения постоянства Существуют несколько разных способов для создания новой базы данных SQL Server, но в данном примере предполагается, что на компьютере установлена версия SQL Express и потому создавать базу данных и запускать сценарии можно только из Visual Studio. 1. В окне Server Explorer (Проводник сервера) выберите внутри узла Data Connections (Подключения данных) опцию Create New SQL Server Database (Создать новую базу данных SQL Server). Это приведет к отображению диалогового окна Create New SQL Server Database (Создание новой базы данных SQL Server), показанного на рис. 36.18. [ Create New SQL Server Database ЫЫШ] Enter information to connect to a SQL Server, then specify the name of a database to create. Server name Asqlexpress ▼ [ Log on to the server ♦ Use Windows Authentication © Use SQL Server Authentication Use/nama: | Password: 1 П 5*ve my password New database name r 1 0* li у >> **■* ' Cancel Рис. 36.18. Диалоговое окно Create New SQL Server Database 2. В поле для имени сервера базы данных введите . \sqlexpress, а в поле для имени самой базы данных — WF. После этого в окне Server Explorer появится элемент, представляющий только что созданную базу данных. 3. Выполните в отношении этой базы данных два сценария, которые хранятся в каталоге %windir%\Microsoft.NET\Framework\v3.5\SQL\EN. Для этого откройте в Visual Studio два показанных в диалоговом окне на рис. 36.19 файла, а именно— SqlPersistenceProviderSchema.sql и SqlPersistenceProviderLogic.sql. В одном содержится схема для службы обеспечения постоянства, а в другом — логика (хранимые процедуры).
1196 Часть V. Дополнительные технологии </> Open File QO^ I « v35 ► SQL > EN ▼ *f j| Seorth ;.1 w it oirte Links Documents Projects *, Recently Changed ■ к DevJrtop Recent Places |ф Computer ■ ■ Pictures Music More » DropSqIPererstenceProvidertogrc.sql Date modified Type 23/07/2007 0ОЮ6 SQL File DropSqlPersistenceProviderSchema.sql 31/01/200709:28 SQL File SqlPersistenceProviderlogic sql SqlPersistenceProviderSchema.sql 23/07/2007 00Ю6 SQL File 19/06/2007 00:11 SQL File Rtename SqlPerrtenceProwJerSchema aol" ЧШ.С1 bm H J Рис. 36.19. Открытие файлов SqlPersistenceProviderSchema. sql и SqlPersistenceProviderLogic. sql в Visual Studio 4. После открытия этих файлов в Visual Studio можно переходить к их поочередному выполнению. Сначала запустите файл SqlPersistenceProviderSchema.sql, для чего просто нажмите клавишу <F5> и выберите созданную в предыдущем шаге базу данных WE Далее точно таким же образом запустите файл SqlPersistenceProviderLogic. sql. 5. Теперь, когда база данных SQL подготовлена, можно переходить к созданию нового проекта и применению в нем службы постоянства. Создайте новый проект типа Sequential Workflow Console Application (Консольное приложение последовательного рабочего потока), перетащите в него действие DelayActivity и установите для его свойства TimeoutDuration значение, равное пяти секундам — чуть позже вы увеличите этот интервал. 6. Чтобы иметь возможность просматривать состояние рабочего потока по мере его выполнения, нужно добавить в основную программу несколько обработчиков событий. Для этого откройте файл Program.cs и добавьте в него обработчики событий Workf1owldied, WorkflowUnloaded, WorkflowPersisted и Workf lowLoaded так, чтобы они просто выводили информацию на консоль: static void Main(string[] args) { using(WorkflowRuntime workflowRuntime = new WorkflowRuntime()) workflowRuntime.Workflowldled += new EventHandler<WorkflowEventArgs>(Workflowldled); workflowRuntime.WorkflowPersisted += new EventHandler<WorkflowEventArgs>(WorkflowPersisted); workflowRuntime.WorkflowLoaded += new EventHandler<WorkflowEventArgs>(WorkflowLoaded); workflowRuntime.WorkflowUnloaded += new EventHandler<WorkflowEventArgs>(WorkflowUnloaded);
Глава 36. Windows Workflow Foundation 1197 static void WorkflowLoaded(object sender, WorkflowEventArgs e) { Console.WriteLine("Workflow {0} Loaded", // Рабочий поток загружен e.Workflowlnstance.Instanceld); } static void WorkflowUnloaded(object sender, WorkflowEventArgs e) { Console.WriteLine("Workflow {0} Unloaded", // Рабочий поток выгружен e.WorkflowInstance.Instanceld); } static void WorkflowPersisted(object sender, WorkflowEventArgs e) { Console.WriteLine("Workflow {0} Persisted", // Рабочий поток сохранен e.WorkflowInstance.Instanceld) ; } static void Workflowldled(object sender, WorkflowEventArgs e) { Console.WriteLine("Workflow {0} Idled", // Рабочий поток простаивает e.WorkflowInstance.Instanceld) ; } Каждое из событий предусматривает просто отображение вывода в окне консоли. Если вы теперь запустите приложение, на экране должно появиться следующее сообщение: Workflow 9223fe0b-5a3f-4e93-a5e0-f5852bllb006 Idled Здесь виден только уникальный идентификатор экземпляра рабочего потока и название того события, которое произошло. Без службы обеспечения постоянства все, что вы получаете — это лишь уведомление о том, что рабочий поток простаивает. Если же вы теперь добавите код для добавления службы постоянства в исполняющую среду, вы сможете видеть, как рабочий поток сохраняется и выгружается из памяти, а чуть позже снова загружается и продолжает выполнение. Добавлять службу в исполняющую среду рабочего потока можно двумя способами — либо с помощью кода, либо с помощью конфигурационного файла. Здесь будут продемонстрированы оба способа, но вообще рекомендуется добавлять все службы в исполняющую среду с применением второго подхода, т.е. с помощью конфигурационного файла. Такой подход упрощает внесение изменений в службы в будущем, поскольку не требует компилировать приложение заново. У SqlWorkf lowPersistenceService имеется несколько конструкторов, но наиболее полезный из них принимает четыре параметра: строку подключения, булевское значение, указывающее на то, должна ли служба выгружать простаивающие экземпляры, и два значения, представляющих временные интервалы— instanceOwnershipDuration и loadinglnterval. Значением параметра unloadOnldle обычно должно быть true; он отвечает за то, будут ли простаивающие рабочие потоки выгружаться из памяти, и доступен для настройки только в случае использования такого принимающего четыре параметра конструктора. При установке для него значения false (которое является значением по умолчанию) рабочие потоки будут сохраняться лишь в случае их завершения или в случае специального запрашивания действием точки постоянного хранения.
1198 Часть V. Дополнительные технологии Параметр instanceOwnershipDuration применяется для указания того, насколько долго рабочему потоку разрешено выполняться в среде с несколькими машинами. При наличии нескольких машин, отвечающих за обработку рабочих потоков, необходимо обязательно задавать это значение для обеспечения возможности выявления сбоев в процессе обработки рабочего потока тем или иным узлом. Когда машина выбирает пригодный для обработки по расписанию рабочий поток из базы данных, он помечается как заблокированный на указанный период. Если по истечении этого периода времени данный рабочий поток так и не сохраняется снова, тогда какой-то другой узел может разблокировать его и обработать. Что касается параметра loadinglnterval, то он позволяет указывать, насколько часто служба обеспечения постоянства должна опрашивать базу данных в поисках рабочих потоков с истекшими таймерами (т.е. готовыми к выполнению). По умолчанию для параметра instanceOwnershipDuration устанавливается значение TimeSpan .MaxValue, а для параметра loadlnterval — две минуты. 7. Теперь добавьте в приложение приведенный ниже код для добавления в исполняющую среду рабочего потока службы обеспечения постоянства. Строка подключения к базе данных здесь жестко кодируется, хотя лучше определять все это поведение внутри конфигурационного файла для упрощения внесения изменений в будущем: static void Main(string[] args) { using(WorkflowRuntime workflowRuntime = new WorkflowRuntime()) { string conn = @"Data Source=.\sqlexpress;Initial Catalog=WF; Integrated Security=True"; workflowRuntime.AddService (new SqlWorkflowPersistenceService (conn, true, TimeSpan.FromMinutesB), TimeSpan.FromSecondsC0))); } } Здесь вы создали SqlWorkf lowPersistenceService и указали строку подключения, значение true для параметра unloadOnldle, две минуты для параметра длительности владения экземпляром (instanceOwnershipDuration) и 30 секунд для параметра интервала загрузки (loadinglnterval). 8. Чтобы определить точно такое же поведение с помощью конфигурационного файла, добавьте в решение сначала ссылку на сборку System.Configuration, a затем — конфигурационный файл приложения, как показано ниже: <?xml version=.0" encoding="utf-8" ?> <configuration> <configSections> <section name="WF" type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </configSections> <WF> <CommonParameters> <add name="ConnectionString" value="Initial Catalog=WF;Data Source=.\sqlexpress; Integrated Security=SSPI;" /> </CommonParameters >
Глава 36. Windows Workflow Foundation 1199 <Services> <add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.0 . 0, Culture=neutral/ PublicKeyToken=31bf3856ad364e35" UnloadOnIdle="true" /> </Services> </WF> </configuration> Этот файл включает определение для обработчика раздела конфигурации Workf lowRuntimeSection, который применяется для считывания раздела по имени WF. В разделе WF вы сначала определили ConnectionString как совместно используемый параметр, а затем определили службу постоянства внутри раздела Services путем добавления класса SqlWorkf lowPersistenceService и указания значения true для его атрибута UnloadOnldle. 9. Чтобы заставить исполняющую среду рабочего потока считывать данные из этого раздела конфигурации, в код приложения необходимо внести одно небольшое изменение: static void Main(string [ ] args) { // Считывать раздел config для конфигурирования служб using (WorkflowRuntime workflowRuntime = new WorkflowRuntime("WF")) { } } После внесения таких изменений запуск приложения приведет к отображению в окне консоли следующего вывода: Workflow 755348bc-3e86-46c4-99a0-3552a2ba7e6f Idled Workflow 755348bc-3e86-46c4-99a0-3552a2ba7e5f Persisted Workflow 755348bc-3e86-46c4-99a0-3552a2ba7e5f Unloaded Workflow 755348bc-3e86-46c4-99a0-3552a2ba7e5f Loaded Workflow 755348bc-3e86-46c4-99a0-3552a2ba7e5f Persisted Первое сообщение показывает, что выполняется действие DelayActivity, поскольку именно при выполнении такого действия рабочий поток попадает в состояние простоя. Поскольку сейчас в приложении есть установленная служба обеспечения постоянства с настроенным параметром UnloadOnldle, далее рабочий поток сохраняется (Persisted) и на этом этапе его текущее состояние упаковывается и сохраняется в базе данных. Следующим происходит событие выгрузки (Unload) рабочего потока и на этом этапе рабочий поток больше не находится в памяти, а существует исключительно на диске внутри базы данных. Еще оно приводит к добавлению в базу данных записи, определяющей, должен ли рабочий поток возобновляться снова для продолжения обработки. Если на этом этапе заглянуть в таблицу InstanceState базы данных, можно увидеть строку, представляющую экземпляр рабочего потока (рис. 36.20). I • uidlmUncdD state 73B4Sbc-3«IMfk4-9M>-35S2t2b*7«3f <B,n*ryd«t*> ! MAI NULL 0 NULL unlocked blocked info 1 1 NULL NULL NULL modified owneriD ownedUntri nextTimer 26Л0/20071*47:49 NULL NULL 2610/200719:48:49 NULL NULL NULL NULL Рис. 36.20. Строка, представляющая экземпляр рабочего потока, в таблице InstanceState
1200 Часть V. Дополнительные технологии В роли первичного ключа для этой строки выступает идентификатор рабочего потока, а в ее втором столбце содержатся бинарные сериализованные данные состояния рабочего потока на момент его сохранения. Значение в столбце nextTimer показывает, когда завершится выполнение действия DelayActivity, и рабочий поток будет сразу же (или некоторое время спустя, в зависимости от интервала опроса) загружен снова. По истечении периода времени, определенного в DelayActivity, рабочий поток загружается снова, на что указывает сообщение Loaded, после этого он завершается, что приводит к генерации еще одного события обеспечения постоянства (Persisted). При сохранении завершившегося рабочего потока, представлявшая его строка в таблице InstanceState удаляется. При желании принудительно применить точку обеспечения постоянства в специальном действии, вроде показанного выше действия DebugActivity, можно добавить к действию атрибут PersistOnCloseAttribute: [ActivityValidator(typeof(DebugActivityValidator) ) ] [Designer(typeof(DebugActivityDesigner) ) ] [PersistOnClose] public class DebugActivity : Activity { } Добавление двух отладочных действий в рабочий поток — одного выше и одного ниже DelayActivity— приведет к отображению в окне консоли такого вывода: Workflow Workflow Workflow Workflow Workflow Workflow Workflow 005f5924- 005f5924- 005f5924- 005f5924- 005f5924- 005f5924- 005f5924- -fb49- -fb49- -fb49- -fb49- -fb49- -fb49- -fb49- -4d60- -4d60- -4d60- -4d60- -4d60- -4d60- -4d60- -a8bd- -a8bd- -a8bd- -a8bd- -a8bd- -a8bd- -a8bd- -f572al9ald39 -f572al9ald39 -f572al9ald39 -f572al9ald39 -f572al9ald39 -f572al9ald39 -f572al9ald39 Persisted Idled Persisted Unloaded Loaded Persisted Persisted Здесь видны два дополнительных события обеспечения постоянства (Persisted), помеченные символами А и В. Они соответствуют тем двум отладочным действиям, которые были добавлены в рабочий поток. Данным атрибутом, однако, следует пользоваться осторожно: слишком большое количество точек обеспечения постоянства в рабочем потоке может навредить его производительности. Обратите внимание, что в случае использования действия с определенным атрибутом PersistOnClose, обязательно требуется наличие и определенной службы обеспечения постоянства. Главным преимуществом присутствия службы постоянства является возможность возобновления выполнения рабочего потока после его прерывания, например, в результате перебоев в электропитании на сервере, который отвечает за хостинг рабочих потоков. При запуске исполняющей среды рабочих потоков снова, что может произойти как через несколько минут, так и через несколько часов или даже дней, служба постоянства будет отыскивать любые пригодные для запуска рабочие потоки и загружать их для выполнения. То есть в случае нахождения в памяти нескольких рабочих потоков на момент отключения электроэнергии, каждый из них будет возвращаться к тому состоянию, в котором он находился на последнем этапе его сохранения, т.е. к предыдущей точке постоянства, и начинать выполняться оттуда снова. Это позволяет создавать программы (ведь рабочие потоки, по сути, тоже представляют собой программы), способные продолжать свою работу и после прерывания с того этапа, на котором было прервано их выполнение, что является очень мощной концепцией.
Глава 36. Windows Workflow Foundation 1201 Привязка данных В завершение этой главы давайте посмотрим, какую роль играет понятие привязки данных (data binding) в контексте рабочих потоков. Типичным сценарием при запуске рабочего потока является необходимость в передаче вместе с потоком по мере его выполнения данных о состоянии. Например, в случае рабочего потока, предназначенного для обслуживания страховых полисов, который описывался ранее в этой главе, вы наверняка бы захотели, чтобы в него передавалась какая-то информация вроде уникального номера страхового полиса, которая могла бы применяться во входящих в состав этого рабочего потока действиях. Для передачи параметров в рабочий поток требуется вносить в код два изменения. Во-первых, необходимо определять в рабочем потоке свойства для каждого параметра, который должен ему передаваться. Во-вторых, нужно передавать эти параметры при запуске рабочего потока. Еще одним важным вопросом, который до этого момента пока не рассматривался, является то, как определять свойства в рабочем потоке. Поскольку рабочий поток представляет собой просто .NET-класс, можно было бы использовать и синтаксис обычных свойств С#. Но другой прием в рабочих потоках позволяет делать кое-какие дополнительные вещи с данными, помимо тех, которые допускается делать с обычными свойствами. За счет применения так называемого свойства зависимостей (DependencyProperty) данные, поступающие в рабочий поток, можно связывать с другими действиями в этом же потоке. Синтаксис, необходимый для определения свойства рабочего потока, выглядит довольно-таки сложно, но того фрагмента кода, который поставляется с .NET, будет вполне достаточно. В следующем практическом занятии демонстрируется пример добавления свойства в рабочий поток и передачи ему значения при запуске этого рабочего потока, а также привязки к этому свойству кое-каких данных. Практическое занятие Определение СВЯЗЭННЫХ СВОЙСТВ 1. Создайте еще один новый проект типа рабочего потока и отобразите для него окно кода. Затем отобразите внутри этого окна контекстное меню и выберите в нем пункт Insert Snippet (Вставить фрагмент). В списке типов, который появится далее, выберите тип Workflow (Рабочий поток). Далее выберите из всплывающего меню пункт Dependency Property — Property (Свойство зависимостей — Свойство). Это приведет к добавлению показанного ниже фрагмента кода (в качестве альтернативного варианта, вместо выбора всех этих пунктов из меню добиться создания такого же фрагмента кода можно и просто путем ввода wdp и двух нажатий клавиши <ТаЬ>). public static DependencyProperty {NAME}Property = DependencyProperty.Register("{NAME}", typeof({TYPE}), typeof(Workflowl)); [DescriptionAttribute("{NAME}")] [CategoryAttribute("{NAME} Category")] [BrowsableAttribute(true)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] public {TYPE} {NAME} { get { return ((string)(base.GetValue(Workflowl.{NAME}Property)));
1202 Часть V. Дополнительные технологии set { base.SetValue(Workflowl.{NAME}Property, value); } } Этот фрагмент кода включает два раздела, в которых можно выполнять ввод: раздел {NAME}, в котором можно вводить желаемое имя для свойства, каковым в данном примере должно быть Pol icy Id, и раздел {TYPE}, в котором можно вводить тип данных свойства, каковым в данном примере должен быть тип string. Еще в коде для свойства бросается в глаза то, что вместо использования переменной экземпляра для сохранения значения, выполняется вызов такого метода базового класса, как GetValue () или SetValue (). Класс Activity (от которого наследуется рабочий поток) сам наследуется от класса DependencyObject, и в этом классе как раз и определяются данные методы. На самом деле, в классе DependencyObject содержится словарная коллекция пар типа "ключ-значение", которая и служит для сохранения фактического значения свойства. Хотя может показаться, что такого количества кода многовато для определения свойства, есть одно важное преимущество и состоит оно в поддержке привязки данных, которую оно обеспечивает. 2. Действие DebugActivity снова встречается в этом разделе, поскольку первоначально его свойство Message было определено как обычное свойство .NET. А для поддержки привязки данных его определение необходимо обновить. Чтобы сделать это, удалите из определения DebugActivity свойство Message и затем замените его свойством зависимостей. Весь необходимый для этого код выглядит так: [ActivityValidator(typeof(DebugActivityValidator))] [Designer(typeof(DebugActivityDesigner))] [PersistOnClose] public class DebugActivity : Activity { public static DependencyProperty MessageProperty = DependencyProperty.Register("Message", typeof(string), [DescriptionAttribute("Message")] [CategoryAttribute("Details")] [BrowsableAttribute(true)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible) ] public string Message { get { return ((string)(base.GetValue(DebugActivity.MessageProperty))); } set { base.SetValue(DebugActivity.MessageProperty, value); } } } В этом коде определение свойства Message просто заменяется свойством зависимостей с таким же именем и типом данных. Однако перед его использованием в рабочем потоке необходимо еще одно небольшое изменение, которое потребуется внести в код верификатора.
Глава 36. Windows Workflow Foundation 1203 3. Поскольку свойство зависимостей используется для привязки данных одного элемента в рабочем потоке к другому, выполнение проверки на предмет того, не содержится ли в свойстве Message значение null, более не является достаточно мерой. Вместо этого теперь сначала должна выполняться проверка на предмет того, не привязано ли свойство к какому-то другому свойству в рабочем потоке, и только в том случае, если это так — проверка его текстового значения. Чтобы обеспечить такое поведение, удалите ту логику, которая сейчас содержится в разделе if (null ! = act. Parent), и замените ее следующим кодом: public class DebugActivityValidator : ActivityValidator { public override ValidationErrorCollection Validate (ValidationManager manager, object obj) { if (null != act.Parent) { //OK - проверка свойства Message if (!act.IsBindingSet(DebugActivity.MessageProperty)) { if (string.IsNullOrEmpty(act.Message)) errors.Add( ValidationError.GetNotSetValidationError("Message")); } } return errors; } } Здесь перед выполнением проверки на предмет того, не является ли значение свойства Message пустым или нулевым (null), вызывается метод IsBindingSet. Если привязка была установлена, тогда генерировать ошибку верификации нет никакой необходимости. Определив все это, вы теперь можете подключать свойство Message действия DebugActivity к любому другому строковому свойству в рабочем потоке. В случае, когда свойство определяется как свойство зависимостей, в представляющей его ячейке это отражается с помощью соответствующего небольшого значка, как показано на рис. 36.21. ! debugActivftyl ADataBmding.DebuoActrv т | тШш (Name) debogActMtyl Description Enabled True Рис. 36.21. Значок в ячейке свойства, указывающий на то, что оно является свойством зависимостей 4. После компиляции решения и добавления действия DebugActivity в свой рабочий поток щелкните на этом действии, чтобы отобразить его свойства, и выберите свойство Message. Помимо пиктограммы рядом с именем этого свойства еще также будет отображаться кнопка с изображением символа троеточия.
1204 Часть V. Дополнительные технологии Введите какое-нибудь значение для этого свойства или щелкните на кнопке с изображением троеточия (или же дважды щелкните на значке привязки), чтобы отобразить диалогового окно, показанное на рис. 36.22. j Bind Message to *n activity s property LkJttfll j Bind to an existing member Bind to < f Workflo«l £ ф ctebugActivityl у ЕЕЗЗЭ £B Г«р Dynamic UpdateCondrtion r*f Name fit Description The selected property of type System.String can be assigned to the target property of type System.String'. I <* I I <*«* I Puc. 36.22. Диалоговое окно для привязки свойства В этом окне вы можете выбрать свойство с таким же типом данных, как и у того, привязку которого выполняете: например, в данном примере значение свойства Message было привязано к определенному в рабочем потоке свойству Pol icy Id. Во время выполнения эти два свойства будут соединяться, благодаря чему на этапе использования действием DebugActivity свойства Message его значение будет совпадать со значением свойства Pol icy Id в рабочем потоке. Никакого копирования данных происходить не будет— свойство Message будет, по сути, представлять собой то же свойство Pol icy Id. На рис. 36.23 показано, что будет отображаться в ячейке связанного свойства. На нем видно, что в ячейке такого свойства будет отображаться имя того объекта, в котором содержится определение связанного свойства и путь к нему, в роли которого в данном случае выступает имя свойства Pol icy Id. Properties debocjA< tivity 1 у&Щт (Name) Description Enabled В Message Name Path «r « X _9_DataBinding.DebugActiv • debuqAttivityl True J AttivityWorkflowl. P. WorkHowl Poficyld Puc. 36.23. Внешний вид ячейки привязанного свойства
Глава 36. Windows Workflow Foundation 1205 5. Теперь осталось только передать свойство Pol icy Id рабочему потоку. Чтобы сделать это, необходимо создать словарную коллекцию пар типа "имя-значение" и передать ее вызову CreateWorkf low в Workf lowRuntime. Поэтому для создания такой словарной коллекции и запуска рабочего потока введите следующий код: Dictionary<string, object> parms = new Dictionary<string, object>(); parms.AddC'Policyld", "xx-99-2293382") ; Workflowlnstance instance = workflowRuntime.CreateWorkflow (typeof(_9_DataBinding.Workflowl), parms); instance.Start (); В этом коде сначала определяется словарная коллекция params, в которую добавляется значение Policyld. Далее создается сам рабочий поток, методу CreateWorkf low которого эта коллекция params и передается в качестве второго параметра. Это дает возможность передавать в рабочий поток различные параметры и использовать их внутри него. Резюме В этой главе было рассказано о Windows Workflow Foundation и показано, как создавать рабочие потоки и выполнять их с помощью Workf lowRuntime. Рабочие потоки могут служить способом расширения приложения и упрощать конечным пользователям работу с ним, позволяя добавлять действия в рабочие потоки путем перетаскивания. В любом рабочем потоке содержится ряд действий, каковые являются строительными блоками WF, а для улучшения поведения рабочих потоков и обеспечения их дополнительной функциональностью можно создавать специальные действия. Например, можно создавать специальные действия для отправки электронных сообщений или обновления информации о продажах в базе данных и тем самым предоставлять конечным пользователям возможность просто перетаскивать их в рабочий поток и подключать их свойства. В WF имеется множество точек расширения: например, она позволяет создавать специальную службу обеспечения постоянства, способную сохранять данные о состоянии выполняющихся рабочих потоков в базе данных SQL Server, а не на диске. WF уже является центральным компонентом в SharePoint Server 2007 и продолжает встраиваться во многие продукты как от Microsoft, так и от других производителей, так что наверняка очень скоро будет адаптирована в большинстве, если не во всех, серверных продуктах Microsoft. Более подробную информацию о Windows Workflow Foundation можно найти на сайте http: //wf. netfxe. com. Там доступно несколько полезных ресурсов, в том числе и замечательная подборка экранных снимков, иллюстрирующих различные сценарии работы в WF, а также сведения о специальных действиях, которые предлагаются для загрузки. Добавление возможности использования рабочих потоков в приложения осуществляется довольно легко, а признательность конечных пользователей за это может быть просто безграничной.
Предметный указатель А ADO.NET, 891 ADO.NET for Entities, 892 ASP.NET, 615 С CLR (Common Language Runtime), 32 CTS (Common Type System), 31 D Dbml (data base mapping language), 862 DNS (Domain Name System), 1017 DTD (Document Type Definitions), 795 F FTP (File Transfer Protocol), 692; 1017 G GAC (Global Assembly Cache), 33; 979 GDI (Graphics Device Interface), 552 GDI+, 1042 H HTTP (Hypertext Transfer Protocol), 692; 1017 I IDE (Integrated Development Environment), 37 IDL (Interface Definition Language), 692 IIS (Internet Information Services), 736 IL (Intermediate Language), 967 j JIT (Just-in-Time compiler), 32 JSON (JavaScript Object Notation), 719 L LINQ (Language-Integrated Query), 818; 937 LINQtoSQL,858 LINQtoXML,942 M MDI (Multiple Document Interface), 490 MTOM (Message Transmission Optimization Mechanism), 1147 N .NET Framework, 31 о ORM (Object-relational mapping), 859 OSI (Open Systems Interconnection), 1015 Q QOTD (quote of the day), 1020 R RAD (Rapid Application Development), 434 RPC (Distributed Computing Environment), 692 s SDI (Single Document Interface), 490 SMTP (Simple Mail Transfer Protocol), 1017 T TCP (Transmission Control Protocol), 1017 и UDP (User Datagram Protocol), 1017 UML (Unified Modeling Language), 205 URI (Uniform Resource Identifier), 1019 V Visual C# 2008 Express Edition, 44 Visual Studio 2008, 37; 41 w WAS (Windows Activation Service), 1152 WCF (Windows Communication Foundation), 1145 Web-служба, 728 WF (Windows Workflow Foundation), 1176 Windows Forms, 614 WPF (Windows Presentation Foundation), 1075 WSDL (Web Service Description Language), 696 X XAML (Extensible Application Markup Language), 1076 XDR (XML-Data Reduced), 796 XML (extensible Markup Language), 790 XML-документация, 991 XSD (XML Schema Definition), 796
А Авторизация, 638 Адрес широковещательный, 1020 Анимация, 1122 Атрибут, 968 встроенный, 971 пользовательский, 964; 983 синтаксис, 1092 Аутентификация, 638 Б Буферизация двойная, 1062 в Векторная графика, 1072 Венгерская нотация, 67 Выражение, 56; 70 лямбда, 420; 825 простое, 422 Д Данные привязка данных (data binding), 1201 Действие (activity), 1180 верификация действий, 1191 Декапирование (pickling), 965 Делегат, 145; 167 обобщенный, 374 Деструктор, 209; 234 Диалоговое окно, 529 Документ XML, 791; 956 Документация NDoc, 1012 XML, 991 3 Заглушка (stub), 692 Запрос оптимизатор запросов, 880 и Имя квалифицированное, 78 Индексатор, 298; 304 Инициализатор, 403 коллекций, 405 объектов, 404 вложенный, 405 Предметный указатель 1207 Интерфейс, 212; 229; 248; 272 ICollection, 295; 297 IComparable, 329 IComparer, 329 IDictionary, 303 IEnumerable, 295; 297 IFormatter, 778 IList, 297 Исключение обработка исключений, 196 Итератор, 305 К Класс, 205 Activity, 1180 Bitmap, 1062 Brush, 1056 Button, 441 CheckBox, 456 CheckedListBox, 468 ColorDialog, 568 ColumnHeader, 477 CommonDialog, 529 Control, 436 DataSet, 904; 919 Directory, 754; 755 Directorylnfo, 754; 758 File, 754; 755 Filelnfo, 754; 756; 757 FileStream, 754 FileSystemlnfo, 754; 757 FileSystemWatcher, 755 FolderBrowserDialog, 569 Font, 1058 FontDialog, 566 Graphics, 1043 GraphicsPath, 1049 GroupBox, 457 Image, 1062 ImageList, 477 Label, 444 LabelTextbox, 519 LinkLabel, 444 ListBox, 468 ListView, 473; 476 ListViewItem, 476 MenuStrip, 491 Metafile, 1062 Object, 232 OpenFileDialog, 532; 541 PageSettings, 550 PageSetupDialog, 557
1208 Предметный указатель ' Path, 754 Pen,1053;1055 PrintDialog, 550; 560; 563 PrinterSettings, 550 PrintPreviewControl, 564 PrintPreviewDialog, 564 RadioButton, 455 Rectangle, 1049 Region, 1051 RichTextBox, 461; 462 SaveFileDialog, 544; 546 ScriptManager, 722 SoapHttpClientProtocol, 702 StatusStrip, 504 StatusStripStatusLabel, 505 StreamReader, 754; 767 Stream Writer, 755 StringFormat, 1059 TabControl, 485 TcpClient, 1033 TcpListener, 1033 TextBox, 445 TextureBrush, 1064 ToolStrip, 498 ToolStripMenuItem, 495 WebClient, 1023 WebMethodAttribute, 701 WebRequest, 1025 WebResponse, 1025 WebService, 700 WorkflowRuntime, 1194 XmlDocument, 799; 805; 806 XmlElement, 800 XmlNode, 805; 806 абстрактный, 227; 248 библиотека классов, 245 внутренний, 227 герметизированный, 227 диаграммы классов, 243 добавление, 242 модификатор класса, 229 обобщенный, 341 статический, 211 Код родной,32 Коллекция,289 Компилятор, 32 JIT, 32 Компиляция, 32 Конструктор, 234 инициализатор конструктора, 237 по умолчанию, 209 Конструкторы LINQ to XML, 943 Контракт, 1146 данных, 1151 неполадок, 1151 операции,1150 службы, 1150 сообщений, 1150 Кэш (cache), 637 сборок глобальный, 33; 979 Л Литерал, 68 строковый, 68 Лямбда-выражение, 420; 825 простое, 422 м Массив, 114; 131 многомерный, 134 параметров, 151 Мастер Data Source Configuration Wizard, 648 Maintenance Wizard, 588 Publish Wizard, 580 Мастер-страница, 657 Меню, 491 Метаданные, 33 Метаинформация, 33 Метафайл, 1072 Метод, 208 abstract (абстрактный), 257 extern (внешний), 257 override (переопределенный), 257 virtual (виртуальный), 257 анонимный, 391 Модель OSI, 1016 н Наследование, 213 о Обобщения (generics), 340 Объект, 205; 208 Объектно-реляционное отображение (ORM),859 Окно Class View, 48 Error List, 49 Properties, 49 Solution Explorer, 48 диалоговое, 529
Предметный указатель 1209 Оператор break, 111 continue, 111 do...while, 102 for, 106 foreach, 134 goto, 92; 111 if, 94 return, 111 switch, 98 try...catch...finally, 194 while, 104 Операция, 71 !,84 К 83 %,71 %=, 76 &,84 &&,85 &=,89 *, 71 *=,76 +,71 ++,72 +=,76 -,71 -72 -=,76 A 71 /=,76 ::, 376 <,83 «,88 «=, 90 <=,83 =,76 ==,83 >,83 >=,83 »,88 »=, 90 ?:,93 ??, 345 Л,84 л=,89 1,84 I =,89 11,85 as, 336 агрегатная, 833 бинарная, 71 декремента, 72 инкремента, 72 математическая, 71 перегрузка операций, 220; 319; 322 приоритеты операций, 76 тернарная, 71; 93 унарная, 71 Оптимизатор запросов, 880 Отладка точка останова, 181; 184 точка трассировки, 180 Ошибка обработка ошибок, 194 семантическая, 172 синтаксическая, 172 фатальная, 172 п Панель инструментов, 498 Переменная, 56; 61 глобальная, 157 локальная, 157 область видимости, 156 Перечисления, 114; 123 Платформа .NET Framework, 31 Поле общедоступное (public), 256 приватное (private), 256 Полиморфизм, 216 Поток (stream), 753 входной, 753 выходной, 753 Привязка данных (data binding), 1201 Приложение Windows Forms, 51 консольное, 40; 45 Программирование объектно-ориентированное, 203 с использованием WCF, 1152 Проект, 39 Проекция, 839 Пространство имен, 77 System.Collections.Generics, 349 System.Data, 896 System.Diagnostics, 972 System.Drawing.Drawing2D, 1072 System.Drawing.Imaging, 1072 System.IO, 753 System.Reflection, 979 глобальное, 77 Протокол DCOM, 693 FTP, 1017 HTTP, 1017; 1147
1210 Предметный указатель IP, 1017 MSMQ, 1148 QOTD, 1020 SMTP, 1017 TCP, 1017; 1020; 1147 UDP, 1017; 1020 Профили, 667 Псевдоним (alias), 79 Публикация Web-сайта, 743 p Раскадровка, 1122 Распаковка (unboxing), 314 Рассылка широковещательная, 1014 Редактор File System Editor, 595 File Types Editor, 598 Launch Condition Editor, 600 User Interface Editor, 601 Рефлексия, 968 Решения (solutions), 39 с Сборка (assembly), 33 отложенное подписание сборки, 980 Сборка мусора, 34 Свойство (property) автоматическое, 267 зависимости (dependency), 1097 подключаемое (attached), 1098 Связывание (linking), 35 Сеанс, 635 Сервер QOTD, 1020 Сериализация, 976 Сигнатура функции, 166 Символ пробельный, 57 Система доменных имен (DNS), 1017 Система общих типов (CTS), 31 Служба WAS, 1152 WCF, 1169 Web, 728 Windows, 1152 Событие (event), 220; 380 маршрутизируемое, 1099 обработчик событий, 220 подключаемое, 1104 пузырьковое, 1100 туннельное, 1100 Сообщение SOAP, 697 Среда общеязыковая исполняющая, 32 Страница мастер, 657 Структура, 114; 128; 221 т Таблица, 859 пропуска проверки подлинности, 982 Технология Ajax, 719 ASP.NET, 615 DHTML, 719 JavaScript, 719 Windows Communication Foundation (WCF), 1145 Windows Forms, 614 Windows Presentation Foundation (WPF), 1075 Тип bool, 63 byte, 62 char, 63 decimal, 63 double, 63 float, 63 int, 62 long, 62 sbyte, 62 short, 62 string, 63 uint, 62 ulong, 62 ushort, 62 анонимный, 411 нулевой (null), 342 ограничение типов, 362 преобразование типов, 75; 115 неявное, 115 явное, 115; 120 Трансформация XSLT, 1011 Триггер, 1121; 1127 У Унифицированный идентификатор ресурса (URI), 1019 Упаковка (boxing), 314 Управляющая последовательность, 68 V, 68 \\68 \\,68
Предметный указатель 1211 \0,68 \а,68 \Ь,68 V.69 \п,69 \г,69 V.69 \v,69 Утилита Gacutil, 981 nslookup, 1017 sn.exe, 979 ф Файл метафайл, 1072 Функция, 144 Main(), 162 перегрузка функции, 145; 165 передача параметра по ссылке, 154 сигнатура функции, 166 х Хостинг, 1152 ц Цикл, 101 do, 102 for, 106 foreach, 134 while, 104 бесконечный, 112 прерывание циклов, 111 ш Шаблон передачи сообщений, 1151 я Язык dbml, 862 IDL, 692 IL, 967 LINQ, 818 UML, 205 WSDL, 696 XAML, 1076; 1091 XML, 790; 942 XPath, 809 XSD, 796 безопасный к типам (type-safe), 36 с блочной структурой, 57