Об авторе
О техническом редакторе
Благодарности
Введение
Автор и читатели — одна команда
Краткий обзор содержания
Исходный код примеров
От издательства
Часть I. Общие сведения о языке С# и платформе .NET
Глава 1. Философия .NET
Подход с применением языка C++ и платформы MFC
Подход с применением Visual Basic 6.0
Подход с применением Java
Подход с применением СОМ
Сложность представления типов данных СОМ
Решение .NET
Роль библиотек базовых классов
Что привносит язык С#
Другие языки программирования с поддержкой .NET
Что собой представляют сборки в .NET
Роль CIL
Преимущества CIL
Компиляция CIL-кода в инструкции, ориентированные на конкретную платформу
Роль метаданных типов в .NET
Роль манифеста сборки
Типы классов
Типы интерфейсов
Типы структур
Типы перечислений
Типы делегатов
Члены типов
Встроенные типы данных
Забота о соответствии правилам CLS
Различия между сборками, пространствами имен и типами
Получение доступа к пространствам имен программным образом
Добавление ссылок на внешние сборки
Изучение сборки с помощью утилиты ildasm. exe
Просмотр метаданных типов
Изучение сборки с помощью утилиты Reflector
Развертывание исполняющей среды .NET
Не зависящая от платформы природа .NET
Резюме
Глава 2. Создание приложений на языке С#
Создание приложений на С# с использованием csc.exe
Добавление ссылок на внешние сборки
Добавление ссылок на несколько внешних сборок
Компиляция нескольких файлов исходного кода
Работа с ответными файлами в С#
Создание приложений .NET с использованием Notepad++
Создание приложений.NET с помощью SharpDevelop
Создание приложений .NET с использованием Visual С# 2010 Express
Создание приложений .NET с использованием Visual Studio 2010
Некоторые уникальные функциональные возможности Visual Studio 2010
Ориентирование на .NET Framework в диалоговом окне New Project
Использование утилиты Solution Explorer
Утилита Class View
Утилита Object Browser
Встроенная поддержка рефакторинга программного кода
Возможности для расширения и окружения кода
Утилита Class Designer
Интегрируемая система документации . NET Framework 4.0
Резюме
Часть II. Главные конструкции программирования на С#
Глава 3. Главные конструкции программирования на С#: часть I
Спецификация кода ошибки в приложении
Обработка аргументов командной строки
Указание аргументов командной строки в Visual Studio 2010
Интересное отклонение от темы: некоторые дополнительные члены класса System.Environment
Класс System.Console
Форматирование вывода, отображаемого в окне консоли
Форматирование числовых данных г
Форматирование числовых данных в приложениях, отличных от консольных
Системные типы данных и их сокращенное обозначение в С#
Внутренние типы данных и операция new
Иерархия классов типов данных
Члены числовых типов данных
Члены System.Boolean
Члены System.Char
Синтаксический разбор значений из строковых данных
Типы System.DateTimeи System.TimeSpan
Пространство имен System.Numerics в .NET4.0
Работа со строковыми данными
Конкатенация строк
Управляющие последовательности символов
Определение дословных строк
Строки и равенство
Неизменная природа строк
Тип System.Text.StringBuilder
Сужающие и расширяющие преобразования типов данных
Настройка проверки на предмет возникновения условий переполнения в масштабах проекта
Ключевое слово unchecked
Роль класса System. Convert
Неявно типизированные локальные переменные
Неявно типизированные данные являются строго типизированными
Польза от неявно типизированных локальных переменных
Итерационные конструкции в С#
Цикл foreach
Использование var в конструкциях foreach
Конструкции whilendo/while
Конструкции принятия решений и операции сравнения
Оператор switch
Резюме
Глава 4. Главные конструкции программирования на С#: часть II
Модификатор out
Модификатор геf
Модификатор params
Определение необязательных параметров
Вызов методов с использованием именованных параметров
Перегрузка методов
Массивы в С#
Неявно типизированные локальные массивы
Определение массива объектов
Работа с многомерными массивами
Использование массивов в качестве аргументов и возвращаемых значений
Базовый класс System. Array
Тип enum
Объявление переменных типа перечислений
Тип System.Enum
Динамическое обнаружение пар \
Типы структур
Создание переменных типа структур
Типы значения, ссылочные типы и операция присваивания
Типы значения, содержащие ссылочные типы
Передача ссылочных типов по значению
Передача ссылочных типов по ссылке
Заключительные детали относительно типов значения и ссылочных типов
Нулевые типы в С #
Операция ??
Резюме
Глава 5. Определение инкапсулированных типов классов
Понятие конструктора
Определение специальных конструкторов
Еще раз о конструкторе по умолчанию
Роль ключевого слова this
Обзор потока конструктора
Еще раз об необязательных аргументах
Понятие ключевого слова static
Определение статических полей данных
Определение статических конструкторов
Определение статических классов
Основы объектно-ориентированного программирования
Роль наследования
Роль полиморфизма
Модификаторы доступа С#
Модификаторы доступа и вложенные типы
Первый принцип: службы инкапсуляции С#
Инкапсуляция с использованием свойств .NET
Использование свойств внутри определения класса
Внутреннее представление свойств
Управление уровнями видимости операторов get/set свойств
Свойства, доступные только для чтения и только для записи
Статические свойства
Понятие автоматических свойств
Замечания относительно автоматических свойств и значений по умолчанию
Понятие синтаксиса инициализации объектов
Инициализация вложенных типов
Работа с данными константных полей
Статические поля только для чтения
Понятие частичных типов
Резюме
Глава 6. Понятия наследования и полиморфизма
О множественном наследовании
Ключевое слово sealed
Изменение диаграмм классов Visual Studio
Второй принцип ООП: подробности о наследовании
Хранение фамильных тайн: ключевое слово protected
Добавление запечатанного класса
Реализация модели включения/делегации
Третий принцип ООП: поддержка полиморфизма в С#
Переопределение виртуальных членов в Visual Studio 2010
Запечатывание виртуальных членов
Абстрактные классы
Полиморфный интерфейс
Сокрытие членов
Правила приведения к базовому и производному классу
Ключевое слово is
Родительский главный класс System.Object
Тестирование модифицированного класса Регson
Статические члены System.Object
Резюме
Глава 7. Структурированная обработка исключений
Роль обработки исключений в .NET
Базовый класс System. Exception
Простейший пример
Перехват исключений
Конфигурирование состояния исключения
Свойство StackTrace
Свойство НеlpLink
Свойство Data
Создание специальных исключений, способ первый
Создание специальных исключений, способ второй
Создание специальных исключений, способ третий
Обработка многочисленных исключений
Передача исключений
Внутренние исключения
Блок finally
Какие исключения могут выдавать методы
К чему приводят необрабатываемые исключения
Отладка необработанных исключений с помощью Visual Studio
Несколько слов об исключениях, связанных с поврежденным состоянием
Резюме
Глава 8. Время жизни объектов
Базовые сведения о времени жизни объектов
Установка объектных ссылок в пи 11
Роль корневых элементов приложения
Поколения объектов
Параллельная сборка мусора в версиях .NET 1.0 — .NET 3.5
Тип System. GC
Создание финализируемых объектов
Описание процесса финализации
Создание высвобождаемых объектов
Создание финализируемых и высвобождаемых типов
Отложенная инициализация объектов
Резюме
Часть III. Дополнительные конструкции программирования на С#
Глава 9. Работа с интерфейсами
Определение специальных интерфейсов
Реализация интерфейса
Вызов членов интерфейса на уровне объектов
Получение ссылок на интерфейсы с помощью ключевого слова i s
Использование интерфейсов в качестве параметров
Использование интерфейсов в качестве возвращаемых значений
Массивы типов интерфейсов
Реализация интерфейсов с помощью Visual Studio 2010
Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом
Проектирование иерархий интерфейсов
Создание методов итератора с помощью ключевого слова yield
Создание именованного итератора
Внутреннее представление метода итератора
Более сложный пример клонирования
Использование специальных свойств и специальных типов для сортировки
Резюме
Глава 10. Обобщения
Проблемы с безопасностью типов
Роль параметров обобщенных типов
Указание параметров типа для обобщенных членов
Указание параметров типов для обобщенных интерфейсов
Пространство имен System.Collections.Generic
Работа с классом List<T>
Работа с классом Stack<T>
Работа с классом Queue<T>
Работа с классом SortedSet<T>
Создание специальных обобщенных методов
Создание специальных обобщенных структур и классов
Обобщенные базовые классы
Ограничение параметров типа
Недостаток ограничений операций
Резюме
Глава 11. Делегаты, события и лямбда-выражения
Определение типа делегата в С#
Базовые классы System.MulticastDelegate и System.Delegate
Простейший пример делегата
Отправка уведомлений о состоянии объекта с использованием делегатов
Удаление целей из списка вызовов делегата
Синтаксис групповых преобразований методов
Понятие ковариантности делегатов
Понятие обобщенных делегатов
Понятие событий С#
\
Прослушивание входящих событий
Упрощенная регистрация событий с использованием Visual Studio 2010
Создание специальных аргументов событий
Обобщенный делегат Event Handle r<T>
Понятие анонимных методов С#
Понятие лямбда-выражений
Обработка аргументов внутри множества операторов
Лямбда-выражения с несколькими параметрами и без параметров
Усовершенствование примера PrimAndProperCarEvents за счет использования лямбда-выражений
Резюме
Глава 12. Расширенные средства языка С#
Перегрузка методов-индексаторов
Многомерные индексаторы
Определения индексаторов в интерфейсных типах
Понятие перегрузки операций
А как насчет операций += и -=?
Перегрузка унарных операций
Перегрузка операций эквивалентности
Перегрузка операций сравнения
Внутреннее представление перегруженных операций
Финальные соображения относительно перегрузки операций
Понятие преобразований пользовательских типов
Преобразования между связанными типами классов
Создание специальных процедур преобразования
Дополнительные явные преобразования типа Square
Определение процедур неявного преобразования
Внутреннее представление процедур пользовательских преобразований
Понятие расширяющих методов
Понятие частичных методов
Понятие анонимных типов
Анонимные типы, содержащие другие анонимные типы
Работа с типами указателей
Работа с операциями * и &
Небезопасная и безопасная функция обмена значений
Ключевое слово stackalloc
Закрепление типа ключевым словом fixed
Ключевое слово sizeof
Резюме
Глава 13. LINQ to Objects
Синтаксис инициализации объектов и коллекций
Лямбда-выражения
Расширяющие методы
Анонимные типы
Роль LINQ
Основные сборки LINQ
Применение запросов LINQ к элементарным массивам
Рефлексия результирующего набора LINQ
LINQ и неявно типизированные локальные переменные
LINQ и расширяющие методы
Роль отложенного выполнения
Роль немедленного выполнения
Возврат результата запроса LINQ
Применение запросов LINQ к объектам коллекций
Применение запросов LINQ к необобщенным коллекциям
Исследование операций запросов LINQ
Получение подмножества данных
Проекция новых типов данных
Получение счетчиков посредством Enumerable •
Обращение результирующих наборов
Выражения сортировки
LINQ как лучшее средство построения диаграмм
Исключение дубликатов
Агрегатные операции LINQ
Внутреннее представление операторов запросов LINQ
Построение выражений запросов с использованием типа Enumerable и лямбда-выражений
Построение выражений запросов с использованием типа Enumerable и анонимных методов
Построение выражений запросов с использованием типа Enumerable и низкоуровневых делегатов
Резюме
Часть IV. Программирование с использованием сборок .NET
Глава 14. Конфигурирование сборок .NET
Устранение конфликтов на уровне имен за счет использования псевдонимов
Создание вложенных пространств имен
Пространство имен, используемое по умолчанию в Visual Studio 2010
Роль сборок .NET
Сборки определяют границы типов
Сборки являются единицами, поддерживающими версии
Сборки являются самоописываемыми
Сборки поддаются конфигурированию
Формат сборки .NET
Заголовок файла CLR
CIL-код, метаданные типов и манифест сборки
Необязательные ресурсы сборки
Однофайловые и многофайловые сборки
Создание и использование однофайловой сборки
Исследование CIL-кода
Исследование метаданных типов
Создание клиентского приложения на С#
Создание клиентского приложения на Visual Basic
Межъязыковое наследование в действии
Создание и использование многофайловой сборки
Исследование файла airvehiсles .dll
Использование многофайловой сборки
Приватные сборки
Процесс зондирования
Конфигурирование приватных сборок
Конфигурационные файлы и Visual Studio 2010
Разделяемые сборки
Генерирование строгих имен в командной строке
Генерирование строгих имен с помощью Visual Studio 2010
Установка сборок со строгими именами в GAC
Просмотр содержимого GAC с помощью проводника Windows
Использование разделяемой сборки
Конфигурирование разделяемых сборок
Создание разделяемой сборки версии 2.0.0.0
Динамическое перенаправление на конкретную версию разделяемой сборки
Сборки политик издателя
Элемент <codeBase>
Пространство имен System. Configuration
Резюме
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов
Изучение блока TypeRef
Просмотр метаданных самой сборки
Просмотр метаданных внешних сборок, на которые имеются ссылки в текущей сборке
Просмотр метаданных строковых литералов
Рефлексия
Создание специальной программы для просмотра метаданных
Рефлексия полей и свойств
Рефлексия реализуемых интерфейсов
Отображение различных дополнительных деталей
Рефлексия обобщенных типов
Рефлексия параметров и возвращаемых значений методов
Динамически загружаемые сборки
Рефлексия разделяемых сборок
Позднее связывание
Вызов методов без параметров
Вызов методов с параметрами
Роль атрибутов .NET
Применение предопределенных атрибутов в С#
Сокращенное обозначение атрибутов в С#
Указание параметров конструктора для атрибутов
Атрибут [Obsolete] в действии
Создание специальных атрибутов
Синтаксис именованных свойств
Ограничение использования атрибутов
Атрибуты уровня сборки и модуля
Рефлексия атрибутов с использованием раннего связывания
Рефлексия атрибутов с использованием позднего связывания
Возможное применение на практике рефлексии, позднего связывания и специальных атрибутов
Создание расширяемого приложения
Создание оснастки на С#
Создание оснастки на Visual Basic
Создание расширяемого приложения Windows Fbrms
Резюме
Глава 16. Процессы, домены приложений и контексты объектов
Взаимодействие с процессами в рамках платформы .NET
Изучение конкретного процесса
Изучение набора потоков процесса
Изучение набора модулей процесса
Запуск и останов процессов программным образом
Управление запуском процесса с использованием класса ProcessStartlnfo
Домены приложений .NET
Взаимодействие с используемым по умолчанию доменом приложения
Получение уведомлений о загрузке сборок
Создание новых доменов приложений
Выгрузка доменов приложений программным образом
Границы контекстов объектов
Определение контекстно-зависимого объекта
Инспектирование контекста объекта
Итоговые сведения о процессах, доменах приложений и контекстах
Резюме
Глава 17. Язык CIL и роль динамических сборок
Директивы, атрибуты и коды операций в CIL
Роль атрибутов CIL
Роль кодов операций CIL
Разница между кодами операций и их мнемоническими эквивалентами в CIL
Помещение и извлечение данных из стека в CIL
Двунаправленное проектирование
Взаимодействие с CIL: модификация файла * . i 1
Компиляция CIL-кодас помощью ilasm.exe
Создание CIL-кода с помощью SharpDevelop
Роль peverify.exe
Использование директив и атрибутов в CIL
Определение текущей сборки в CIL
Определение пространств имен в CIL
Определение типов классов в CIL
Определение и реализация интерфейсов в CIL
Определение структур в CIL
Определение перечислений в CIL
Определение обобщений в CIL
Компиляция файла CILTypes . il
Соответствия между типами данных в библиотеке базовых классов .NET, C# и CIL
Определение членов типов в CIL
Определение конструкторов для типов в CIL
Определение свойств в CIL
Определение параметров членов
Изучение кодов операций в CIL
Объявление локальных переменных в CIL
Отображение параметров на локальные переменные в CIL
Скрытая ссылка this
Представление итерационных конструкций в CIL
Создание сборки . NET на CIL
Создание CILCarClient.exe
Динамические сборки
Роль типа System. Reflection. Emit. ILGenerator
Создание динамической сборки
Генерация сборки и набора модулей
Роль типа Module Builder
Генерация типа HelloClassn принадлежащей ему строковой переменной
Генерация конструкторов
Использование динамически сгенерированной сборки
Резюме
Глава 18. Динамические типы и исполняющая среда динамического языка
Роль сборки Microsoft.CSharp.dll
Область применения ключевого слова dynamic
Ограничения ключевого слова dynamic
Практическое применение ключевого слова dynamic
Роль деревьев выражений
Роль пространства имен System. Dynamic
Динамический поиск в деревьях выражений во время выполнения
Упрощение вызовов позднего связывания с использованием динамических типов
Упрощение взаимодействия с СОМ посредством динамических данных
Роль первичных сборок взаимодействия
Встраивание метаданных взаимодействия
Общие сложности взаимодействия с СОМ
Взаимодействие с СОМ с использованием средств языка С# 4.0
Резюме
Часть V. Введение в библиотеки базовых классов .NET
Глава 19. Многопоточность и параллельное программирование
Роль синхронизации потоков
Краткий обзор делегатов .NET
Асинхронная природа делегатов
Интерфейс System.IAsyncResult
Асинхронный вызов метода
Роль делегата AsyncCallback
Роль класса AsyncResult
Передача и прием специальных данных состояния
Пространство имен System.Threading
Класс System.Threading.Thread
Свойство Name
Свойство Priority
Программное создание вторичных потоков
Работа с делегатом ParametrizedThreadStart
Класс AutoResetEvent
Потоки переднего плана и фоновые потоки
Пример проблемы, связанной с параллелизмом
Синхронизация с использованием типа System.Threading.Monitor
Синхронизация с использованием типа System.Threading. Interlocked
Синхронизация с использованием атрибута [Synchronization]
Программирование с использованием обратных вызовов Timer
Пул потоков CLR
Параллельное программирование на платформе .NET
Роль класса Parallel
Понятие параллелизма данных
Класс Task
Обработка запроса на отмену
Понятие параллелизма задач
Выполнение запроса PLINQ
Отмена запроса PLINQ
Резюме
Глава 20. Файловый ввод-вывод и сериализация объектов
Абстрактный базовый класс FileSystemlnfo
Работа с типом Directorylnfo
Создание подкаталогов с помощью типа Directorylnfo
Работа с типом Directory
Работа с типом Drive Info
Работа с классом File Info

Работа с типом File
Абстрактный класс Stream
Работа с классами StreamWriter и StreamReader
Чтение из текстового файла
Прямое создание экземпляров классов StreamWriter/StreamReader
Работа с классами StringWriter и StringReader
Работа с классами BinaryWriter и BinaryReader
Программное отслеживание файлов
Понятие сериализации объектов
Конфигурирование объектов для сериализации
Общедоступные поля, приватные поля и общедоступные свойства
Выбор форматера сериализации
Точность типов среди форматеров
Сериализация объектов с использованием BinaryFormatter
Сериализация объектов с использованием SoapFormatter
Сериализация объектов с использованием XmlSerializer
Сериализация коллекций объектов
Настройка процессов сериализации SOAP и двоичной сериализации
Настройка сериализации с использованием интерфейса ISerializable
Настройка сериализации с использованием атрибутов
Резюме
Глава 21. AD0.NET, часть I: подключенный уровень
Поставщики данных ADO.NET
О сборке System.Data.OracleClient.dll
Получение сторонних поставщиков данных ADO.NET
Дополнительные пространства имен ADO.NET
Типы из пространства имен System.Data
Роль интерфейса I DbTrans act ion
Роль интерфейса IDbCommand
Роль интерфейсов IDbDataParameter и IDataParameter
Роль интерфейсов IDbDataAdapter и IDataAdapter
Роль интерфейсов IDataReader и IDataRecord
Абстрагирование поставщиков данных с помощью интерфейсов
Создание базы данных AutoLot
Создание таблиц Customers и Orders
Визуальное создание отношений между таблицами
Модель генератора поставщиков данных ADO. NET
Возможные трудности с моделью генератора поставщиков
Элемент<connectionStrings>
Подключенный уровень ADO.NET
Работа с объектами ConnectionStringBuilder
Работа с объектами команд
Работа с объектами чтения данных
Создание повторно используемой библиотеки доступа к данным
Добавление логики вставки
Добавление логики удаления
Добавление логики изменения
Добавление логики выборки
Работа с параметризованными объектами команд
Выполнение хранимой процедуры
Создание консольного пользовательского интерфейса
Транзакции баз данных
Добавление таблицы CreditRisksB базу данных AutoLot
Добавление метода транзакции в InventoryDAL
Тестирование транзакции в нашей базе данных
Резюме
Глава 22. AD0.NET, часть II: автономный уровень
Роль объектов Data Set
Основные методы класса Data Set
Создание DataSet
Работа с объектами DataColumn
Включение автоинкрементных полей
Добавление объектов DataColumn в DataTable
Работа с объектами DataRow
Свойство DataRowVersion
Работа с объектами DataTable
Получение данных из объекта DataSet
Обработка данных из DataTable с помощью объектов DataTableReader
Сериализация объектов DataTable и DataSet в формате XML
Сериализация объектов DataTable и DataSet в двоичном формате
Привязка объектов DataTable к графическим интерфейсам Windows Forms
Удаление строк из DataTable
Выборка строк с помощью фильтра
Изменение строк в DataTable
Работа с типом DataView
Работа с адаптерами данных
Замена имен из базы данных более понятными названиями
Добавление в AutoLotDAL. dl 1 возможности отключения
Настройка адаптера данных с помощью SqlCommandBuilder
Установка номера версии
Тестирование автономной функциональности
Объекты DataSet для нескольких таблиц и взаимосвязь данных
Создание отношений между таблицами
Изменение таблиц базы данных
Переходы между взаимосвязанными таблицами
Средства конструктора баз данных в Windows Forms
Сгенерированный файл арр. conf ig
Анализ строго типизированного DataSet
Анализ строго типизированного DataTable
Анализ строго типизированного DataRow
Анализ строго типизированного адаптера данных
Завершение приложения Windows Forms
Выделение строго типизированного кода работы с базами данных в библиотеку классов
Выборка данных с помощью сгенерированного кода
Вставка данных с помощью сгенерированного кода
Удаление данных с помощью сгенерированного кода
Вызов хранимой процедуры с помощью сгенерированного кода
Программирование с помощью LINQ to DataSet
Получение DataTable, совместимого с LINQ
Заполнение новых объектов DataTable с помощью LINQ-запросов
Резюме
Глава 23. ADO.NET, часть III: Entity Framework
Строительные блоки Entity Framework
Роль служб объектов
Роль клиента сущности
Роль файла *.edmx
Роль классов ObjectContext и ObjectSet<T>
Собираем все вместе
Построение и анализ первой модели EDM
Изменение формы сущностных данных
Просмотр отображений
Просмотр данных сгенерированного файла *. еdmx
Просмотр сгенерированного исходного кода
Улучшение сгенерированного исходного кода
Программирование с использованием концептуальной модели
Обновление записи
Запросы с помощью LINQ to Entities
Запросы с помощью Entity SQL
Работа с объектом EntityDataReader
Проект AutoLotDAL версии 4.0, теперь с сущностями
Роль навигационных свойств
Использование навигационных свойств внутри запросов LINQ to Entity
Вызов хранимой процедуры
Привязка данных сущностей к графическим пользовательским интерфейсам Windows Fbrms
Резюме
Глава 24. Введение в LINQ to XML
Синтаксис литералов Visual Basic как наилучший интерфейс LINQ to XML
Члены пространства имен System.Xml.Linq
Работа с XElement HXDocument
Загрузка и разбор XML-содержимого
Манипулирование XML-документом в памяти
Импорт файла Inventory, xml
Определение вспомогательного класса LINQ to XML
Оснащение пользовательского интерфейса вспомогательными методами
Резюме
Глава 25. Введение в Windows Communication Foundation
Роль служб СОМ+/Enterprise Services
Роль MSMQ
Роль .NET Remotlng
Роль веб-служб XML
Именованные каналы, сокеты и Р2Р
Роль WCF
Обзор архитектуры, ориентированной на службы
WCF: итоги
Исследование основных сборок WCF
Шаблоны проектов WCF в Visual Studio
Базовая композиция приложения WCF
Понятие ABC в WCF
Понятие привязок WCF
Понятие адресов WCF
Построение службы WCF
Атрибут [OperationContract]
Служебные типы как контракты операций
Хостинг службы WCF
Кодирование с использованием типа ServiceHost
Указание базового адреса
Подробный анализ типа ServiceHost
Подробный анализ элемента <system.serviceModel>
Включение обмена метаданными
Построение клиентского приложения WCF
Генерация кода прокси с использованием Visual Studio 2010
Конфигурирование привязки на основе TCP
Упрощение конфигурационных настроек в WCF 4.0
Предоставление одной службы WCF с использованием множества привязок
Изменение установок для привязки WCF
Конфигурация поведения МЕХ по умолчанию в WCF 4.0
Обновление клиентского прокси и выбор привязки
Использование шаблона проекта WCF Service Library
Тестирование службы WCFс помощью WcfTestClient.exe
Изменение конфигурационных файлов с помощью SvcConfigEditor.exe
Хостинг службы WCF в виде службы Windows
Включение МЕХ
Создание программы установки для службы Windows
Установка службы Windows
Асинхронный вызов службы на стороне клиента
Проектирование контрактов данных WCF
Реализация контракта службы
Роль файла *.svc
Содержимое файла Web. с on fig
Тестирование службы
Резюме
Глава 26. Введение в Windows Workflow Foundation 4.0
Построение простого рабочего потока
Исполняющая среда WF 4.0
Хостинг рабочего потока с использованием класса Workf lowlnvoker
Хостинг рабочего потока с использованием класса Workf lowApplication
Переделка первого рабочего потока
Знакомство с действиями Windows Workflow 4.0
Действия блок-схемы
Действия обмена сообщениями
Действия исполняющей среды и действия-примитивы
Действия транзакций
Действия над коллекциями и действия обработки ошибок
Построение рабочего потока в виде блок-схемы
Работа с действием InvokeMethod
Определение переменных уровня рабочего потока
Работа с действием FlowDecision
Работа с действием TerminateWorkf low
Построение условия \
Работа с действием ForEach<T>
Завершение приложения
Промежуточные итоги
Изоляция рабочих потоков в выделенных библиотеках
Импорт сборок и пространств имен
Определение аргументов рабочего потока
Определение переменных рабочего потока
Работа с действием Assign
Работа с действиями IfnSwitch
Построение специального действия кода
Использование библиотеки рабочего потока
Резюме
Часть VI. Построение настольных пользовательских приложений с помощью WPF
Глава 27. Введение в Windows Presentation Foundation и XAML
Обеспечение разделения ответственности через XAML
Обеспечение оптимизированной модели визуализации
Упрощение программирования сложных пользовательских интерфейсов
Различные варианты приложений WPF
WPF-приложения на основе навигации
Приложения ХВАР
Отношения между WPF и Silverlight
Исследование сборок WPF
Роль класса Wi ndow
Роль класса System.Windows.Controls.ContentControl
Роль класса System.Windows.Controls.Control
Роль класса System.Windows.FrameworkElement
Роль класса System.Windows.UIElement
Роль класса System.Windows.Media.Visual
Роль класса System. Windows. DependencyObject
Роль класса System.Windows.Threading.DispatcherObject
Построение приложения WPF без XAML
Создание простого пользовательского интерфейса
Взаимодействие с данными уровня приложения
Обработка закрытия объекта Window
Перехват событий мыши
Перехват клавиатурных событий
Построение приложения WPF с использованием только XAML
Определение объекта Application в XAML
Обработка файлов XAML с помощью msbuild.exe
Трансформация разметки в сборку .NET
Poль BAML
Отображение XAML-данных приложения на код С#
Итоговые замечания о процессе трансформирования XAML в сборку
Синтаксис XAML для WPF
Пространства имен XAML XML и \
Управление объявлениями классов и переменных-членов
Элементы XAML, атрибуты XAML и преобразователи типов
Понятие синтаксиса XAML \
Понятие присоединяемых свойств XAML
Понятие расширений разметки XAML
Построение приложений WPF с использованием файлов отделенного кода
Добавление файла кода для класса МуАрр
Обработка файлов кода с помощью msbuild.exe
Построение приложений WPF с использованием Visual Studio 2010
Знакомство с инструментами визуального конструктора WPF
Проектирование графического интерфейса окна
Реализация события Loaded
Реализация события Click объекта Button
Реализация события Closed
Тестирование приложения
Резюме
Глава 28. Программирование с использованием элементов управления WPF
Элементы управления Ink API
Элементы управления документами WPF
Общие диалоговые окна WPF
Подробные сведения находятся в документации
Управление компоновкой содержимого с использованием панелей
Позиционирование содержимого внутри панелей WrapPanel
Позиционирование содержимого внутри панелей StackPanel
Позиционирование содержимого внутри панелей Grid
Позиционирование содержимого внутри панелей DockPanel
Включение прокрутки в типах панелей
Построение главного окна с использованием вложенных панелей
Построение панели инструментов
Построение строки состояния
Завершение дизайна пользовательского интерфейса
Реализация обработчиков событий MouseEnter/MouseLeave
Реализация логики проверки правописания
Понятие управляющих команд WPF
Подключение команд к свойству Command
Подключение команд к произвольным действиям
Работа с командами Open и Save
Построение пользовательского интерфейса WPF с помощью Expression Blend
Использование элемента TabControl
Построение вкладки Ink API
Элемент управления RadioButton
Элемент управления InkCanvas
Элемент управления С оmboB ox
Сохранение, загрузка и очистка данных InkCanvas
Введение в интерфейс Documents API
Диспетчеры компоновки документа
Построение вкладки Documents
Наполнение FlowDocument с помощью кода
Включение аннотаций и \
Сохранение и загрузка потокового документа
Введение в модель привязки данных WPF
Установка привязки данных с использованием Blend
Свойство DataContext
Преобразование данных с использованием IValueConverter
Установка привязок данных в коде
Построение вкладки DataGrid
Резюме
Глава 29. Службы визуализации графики WPF
Визуализация графических данных с использованием фигур
Удаление прямоугольников, эллипсов и линий с поверхности Canvas
Работа с элементами Polyline и Polygon
Работа с элементом Path
Кисти и перья WPF
Конфигурирование кистей в коде
Конфигурирование перьев
Применение графических трансформаций
Трансформация данных Canvas
Работа с фигурами в Expression Blend
Преобразование фигур в пути
Комбинирование фигур
Редакторы кистей и трансформаций
Визуализация графических данных с использованием рисунков и геометрий
Рисование с помощью DrawingBrush
Включение типов Drawing в Drawinglmage
Генерация сложной векторной графики с использованием Expression Design
Визуализация графических данных с использованием визуального уровня
Первый взгляд на класс DrawingVisual
Визуализация графических данных в специальном диспетчере компоновки
Реагирование на операции проверки попадания
Резюме
Глава 30. Ресурсы, анимация и стили WPF
Программная загрузка изображения
Роль свойства Resources
Определение ресурсов уровня окна
Расширение разметки {StaticResource}
Изменение ресурса после извлечения
Расширение разметки {DynamicResource}
Ресурсы уровня приложения
Определение объединенных словарей ресурсов
Определение сборки из одних ресурсов
Извлечение ресурсов в Expression Blend
Службы анимации WPF
Свойства То, From и By
Роль базового класса Timeline
Написание анимации в коде С#
Управление темпом анимации
Запуск в обратном порядке и циклическое выполнение анимации
Описание анимации в XAML
Роль триггеров событий
Анимация с использованием дискретных ключевых кадров s
Роль стилей WPF
Переопределение настроек стиля
Автоматическое применение стиля с помощью TargetType
Создание подклассов существующих стилей
Роль безымянных стилей
Определение стилей с триггерами
Определение стилей с множеством триггеров
Анимированные стили
Программное применение стилей
Генерация стилей с помощью Expression Blend
Резюме
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления
Важные замечания относительно оболочек свойств CLR
Построение специального свойства зависимости
Реакция на изменение свойства
Маршрутизируемые события
Продолжение или прекращение пузырькового распространения
Роль маршрутизируемых туннелируемых событий
Логические деревья, визуальные деревья и шаблоны по умолчанию
Программный просмотр визуального дерева
Программный просмотр шаблона по умолчанию для элемента управления
Построение специального шаблона элемента управления в Visual Studio 2010
Включение визуальных подсказок с использованием триггеров
Роль расширения разметки {TemplateBinding}
Роль класса ContentPresenter
Включение шаблонов в стили
Построение специальных элементов UserControl с помощью Expression Blend
Создание WPF-приложения JackpotDeluxe
Роль визуальных состояний .NET 4.0
Завершение приложения JackpotDeluxe
Резюме
Часть VII. Построение веб-приложений с использованием ASP.NET
Глава 32. Построение веб-страниц ASP.NET
HTTP — протокол без поддержки состояния
Веб-приложения и веб-серверы
Веб-сервер разработки ASP NET
Роль языка HTML
Роль формы HTML
Инструменты визуального конструктора HTML в Visual Studio 2010
Построение формы HTML
Роль сценариев клиентской стороны
Обратная отправка веб-серверу
Набор средств API-интерфейса ASP NET
Основные средства ASP.NET 2.0
Основные средства ASP.NET 4.0
Построение однофайловой веб-страницы ASP.NET
Проектирование пользовательского интерфейса
Добавление логики доступа к данным
Роль директив ASP NET
Анализ блока script
Анализ объявлений элементов управления ASP NET
Цикл компиляции для однофайловых страниц
Построение веб-страницы ASP.NET с использованием файлов кода
Обновление файла кода
Цикл компиляции многофайловых страниц
Отладка и трассировка страниц ASP NET
Веб-сайты и веб-приложения ASP.NET
Структура каталогов веб-сайта ASP.NET
Роль папки App_Code
Цепочка наследования типа Page
Взаимодействие с входящим запросом HTTP
Доступ к входным данным формы
Свойство IsPostBack
Взаимодействие с исходящим ответом HTTP
Перенаправление пользователей
Жизненный цикл веб-страницы ASP.NET
Событие Error
Роль файла Web.config
Резюме
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET
Свойство AutoPostBack
Базовые классы Control и WebControl
Динамическое добавление и удаление элементов управления
Взаимодействие с динамически созданными элементами управления
Функциональность базового класса WebControl
Основные категории веб-элементов управления ASP NET
Документация по веб-элементам управления
Построение веб-сайта ASPNET Cars
Определение страницы содержимого Default.aspx
Проектирование страницы содержимого Inventory, aspx
Проектирование страницы содержимого BuildCar. aspx
Роль элементов управления проверкой достоверности
Класс RegularExpressionValidator
Класс RangeValidator
Класс CompareValidator
Создание итоговой панели проверки достоверности
Определение групп проверки достоверности
Работа с темами
Применение тем ко всему сайту
Применение тем на уровне страницы
Свойство SkinID
Программное назначение тем
Резюме
Глава 34. Управление состоянием в ASP.NET
Приемы управления состоянием ASP. NET
Роль состояния представления ASP NET
Добавление специальных данных в состояние представления
Роль файла Global.asax
Базовый класс HttpApplication
Различие между свойствами Application и Session
Модификация данных приложения
Обработка останова веб-приложения
Работа с кэшем приложения
Модификация файла *.asрх
Поддержка данных сеанса
Cookie-наборы
Чтение входящих cookie-данных
Роль элемента <sessionState>
Хранение информации о сеансах в выделенной базе данных
Интерфейс ASP.NET Profile API
Определение пользовательского профиля BWeb.config
Программный доступ к данным профиля
Группирование данных профиля и сохранение специальных объектов
Резюме
Часть VIII. Приложения
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono
Предметный указатель
Текст
                    ЯЗЫК ПРОГРАММИРОВАНИЯ
С#2010
И ПЛАТФОРМА .NET 4
5-е издание


PRO C# 2010 AND THE .NET 4 PLATFORM Fifth Edition Andrew Troelsen Apress*
ЯЗЫК ПРОГРАММИРОВАНИЯ С#2010 И ПЛАТФОРМА .NET 4 5-е издание Эндрю Троелсен Москва • Санкт-Петербург • Киев 2011
ББК 32.973.26-018.2.75 Т70 УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией С.Н. Тригуб Перевод с английского Я.П. Волковой, А.А. Моргунова, Н.А. Мухина Под редакцией Ю.Н. Артпеменко По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу: info@williamspublishing.com, http://www.williamspublishing.com Трое л сен, Эндрю. Т70 Язык программирования С# 2010 и платформа .NET 4.0, 5-е изд. : Пер. с англ. — М. : ООО "И.Д. Вильяме", 2011. — 1392 с. : ил. — Парал. тит. англ. ISBN 978-5-8459-1682-2 (рус.) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства APress, Berkeley, CA. Authorized translation from the English language edition published by APress, Inc., Copyright © 2010. All rights reserved. No part of this work may be reproduced or transmitted In any form or by any means, electronic or mechanical, Including photocopying, recording, or by any Information storage or retrieval system, without the prior written permission of the copyright owner and the publisher. Trademarked names may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, we use the names only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark. Russian language edition is published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2011. Научно-популярное издание Эндрю Троелсен Язык программирования С# 2010 и платформа .NET 4.0 5-е издание Верстка Т.Н. Артпеменко Художественный редактор С.А. Чернокозинский Подписано в печать 28.10.2010. Формат 70x100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 112,23. Уч.-изд. л. 87,1. Тираж 2000 экз. Заказ № 24430. Отпечатано по технологии CtP в ОАО "Печатный двор" им. А. М. Горького 197110, Санкт-Петербург, Чкаловский пр., 15. ООО "И. Д. Вильяме", 127055, г. Москва, ул. Лесная, д. 43, стр. 1 ISBN 978-5-8459-1682-2 (рус.) © Издательский дом "Вильяме", 2011 ISBN 978-1-43-022549-2 (англ.) © by Andrew TYoelsen, 2010
Оглавление Часть I. Общие сведения о языке С# и платформе .NET 43 Глава 1. Философия .NET 44 Глава 2. Создание приложений на языке С# 80 Часть II. Главные конструкции программирования на С# 105 Глава 3. Главные конструкции программирования на С#: часть Г 106 Глава 4. Главные конструкции программирования на С#: часть ГГ 150 Глава 5. Определение инкапсулированных типов классов 185 Глава 6. Понятия наследования и полиморфизма 231 Глава 7. Структурированная обработка исключений 265 Глава 8. Время жизни объектов 292 Часть III. Дополнительные конструкции программирования на С# 319 Глава 9. Работа с интерфейсами 320 Глава 10. Обобщения 356 Глава 11. Делегаты, события и лямбда-выражения 386 Глава 12. Расширенные средства языка С# 421 Глава 13. L1NQ to Objects 463 Часть IV. Программирование с использованием сборок .NET 493 Глава 14. Конфигурирование сборок .NET 494 Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 542 Глава 16. Процессы, домены приложений и контексты объектов 582 Глава 17. Язык C1L и роль динамических сборок 607 Глава 18. Динамические типы и исполняющая среда динамического языка 648 Часть V. Введение в библиотеки базовых классов .NET 669 Глава 19. Многопоточность и параллельное программирование 670 Глава 20. Файловый ввод-вывод и сериализация объектов 711 Глава 21. ADO.NET, часть Г: подключенный уровень 754 Глава 22. ADO.NET, часть ГГ: автономный уровень 804 Глава 23. ADO.NET, часть ПГ: Entity Framework 857 Глава 24. Введение в UNQ to XML 891 Глава 25. Введение в Windows Communication Foundation 906 Глава 26. Введение в Windows Workflow Foundation 4.0 961 Часть VI. Построение настольных пользовательских приложений с помощью WPF 995 Глава 27. Введение в Windows Presentation Fbundation и XAML 996 Глава 28. Программирование с использованием элементов управления WPF 1048 Глава 29. Службы визуализации графики WPF 1103 Глава 30. Ресурсы, анимация и стили WPF 1137 Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1170 Часть VII. Построение веб-приложений с использованием ASP.NET 1213 Глава 32. Построение веб-страниц ASP.NET 1214 Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1257 Глава 34. Управление состоянием в ASP.NET 1294 Часть VIII. Приложения 1327 Приложение А. Программирование с помощью Windows Forms 1328 Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1369 Предметный указатель 1386
Содержание Об авторе 31 О техническом редакторе 31 Благодарности 31 Введение 32 Автор и читатели — одна команда 33 Краткий обзор содержания 33 Исходный код примеров 42 От издательства 42 Часть I. Общие сведения о языке С# и платформе .NET 43 Глава 1. Философия .NET 44 Предыдущее состояние дел 44 Подход с применением языка С и API-интерфейса Windows 45 Подход с применением языка C++ и платформы MFC 45 Подход с применением Visual Basic 6.0 45 Подход с применением Java 46 Подход с применением СОМ 46 Сложность представления типов данных СОМ 47 Решение .NET 48 Главные компоненты платформы .NET (CLR, CTS и CLS) 49 Роль библиотек базовых классов 50 Что привносит язык С# 50 Другие языки программирования с поддержкой .NET 52 Жизнь в многоязычном окружении 54 Что собой представляют сборки в .NET 54 Однофайловые и многофайловые сборки 56 Роль CIL 56 Преимущества CIL 58 Компиляция CIL-кода в инструкции, ориентированные на конкретную платформу 58 Роль метаданных типов в .NET 59 Роль манифеста сборки 60 Что собой представляет общая система типов (CTS) 60 Типы классов 61 Типы интерфейсов 61 Типы структур 62 Типы перечислений 62 Типы делегатов 63 Члены типов 63 Встроенные типы данных 63 Что собой представляет общеязыковая спецификация (CLS) 64 Забота о соответствии правилам CLS 66 Что собой представляет общеязыковая исполняющая среда (CLR) 66 Различия между сборками, пространствами имен и типами 67 Роль корневого пространства Microsoft 71 Получение доступа к пространствам имен программным образом 71 Добавление ссылок на внешние сборки 73 Изучение сборки с помощью утилиты ildasm. exe 74 Просмотр CIL-кода 74 Просмотр метаданных типов 74 Просмотр метаданных сборки (манифеста) 74 Изучение сборки с помощью утилиты Reflector 75
Содержание 7 Развертывание исполняющей среды .NET 76 Клиентский профиль исполняющей среды .NET 77 Не зависящая от платформы природа .NET 77 Резюме 79 Глава 2. Создание приложений на языке С# 80 Роль комплекта . NET Framework 4.0 SDK 80 Окно командной строки в Visual Studio 2010 81 Создание приложений на С# с использованием csc.exe 81 Указание целевых входных и выходных параметров 82 Добавление ссылок на внешние сборки 84 Добавление ссылок на несколько внешних сборок 84 Компиляция нескольких файлов исходного кода 85 Работа с ответными файлами в С# 85 Создание приложений .NET с использованием Notepad++ 87 Создание приложений.NET с помощью SharpDevelop 88 Создание простого тестового проекта 89 Создание приложений .NET с использованием Visual С# 2010 Express 90 Некоторые уникальные функциональные возможности Visual С# 2010 Express 91 Создание приложений .NET с использованием Visual Studio 2010 92 Некоторые уникальные функциональные возможности Visual Studio 2010 92 Ориентирование на .NET Framework в диалоговом окне New Project 93 Использование утилиты Solution Explorer 93 Утилита Class View 95 Утилита Obj ect Browser 9 5 Встроенная поддержка рефакторинга программного кода 96 Возможности для расширения и окружения кода 98 Утилита Class Designer 100 Интегрируемая система документации . NET Framework 4.0 102 Резюме 104 Часть II. Главные конструкции программирования на С# Ю5 Глава 3. Главные конструкции программирования на С#: часть I 106 Разбор простой программы на С# 106 Варианты метода Ma i n () 108 Спецификация кода ошибки в приложении 109 Обработка аргументов командной строки 110 Указание аргументов командной строки в Visual Studio 2010 111 Интересное отклонение от темы: некоторые дополнительные члены класса System.Environment 112 Класс System.Console 113 Базовый ввод-вывод с помощью класса Console 114 Форматирование вывода, отображаемого в окне консоли 115 Форматирование числовых данных г 116 Форматирование числовых данных в приложениях, отличных от консольных 117 Системные типы данных и их сокращенное обозначение в С# 117 Объявление и инициализация переменных 119 Внутренние типы данных и операция new 120 Иерархия классов типов данных 121 Члены числовых типов данных 123 Члены System.Boolean 123 Члены System.Char 124 Синтаксический разбор значений из строковых данных 125
8 Содержание Типы System.DateTimeи System.TimeSpan 125 Пространство имен System.Numerics в .NET4.0 126 Работа со строковыми данными 127 Базовые операции манипулирования строками 128 Конкатенация строк 129 Управляющие последовательности символов 130 Определение дословных строк 131 Строки и равенство 131 Неизменная природа строк 132 Тип System.Text.StringBuilder 133 Сужающие и расширяющие преобразования типов данных 135 Перехват сужающих преобразований данных 137 Настройка проверки на предмет возникновения условий переполнения в масштабах проекта 139 Ключевое слово unchecked 139 Роль класса System. Convert 140 Неявно типизированные локальные переменные 140 Ограничения, связанные с неявно типизированными переменными 142 Неявно типизированные данные являются строго типизированными 143 Польза от неявно типизированных локальных переменных 144 Итерационные конструкции в С# 144 Цикл for 145 Цикл f oreach 145 Использование var в конструкциях foreach 146 Конструкции whilendo/while 146 Конструкции принятия решений и операции сравнения 147 Оператор if /else 147 Оператор switch 148 Резюме 149 Глава 4. Главные конструкции программирования на С#: часть II 150 Методы и модификаторы параметров 150 Стандартное поведение при передаче параметров 151 Модификатор out 152 Модификатор г е f 153 Модификатор par ams 154 Определение необязательных параметров 156 Вызов методов с использованием именованных параметров 157 Перегрузка методов 158 Массивы в С# 160 Синтаксис инициализации массивов в С # 161 Неявно типизированные локальные массивы 162 Определение массива объектов 163 Работа с многомерными массивами 163 Использование массивов в качестве аргументов и возвращаемых значений 165 Базовый класс System. Array 165 Тип enum 167 Управление базовым типом, используемым для хранения значений перечисления 168 Объявление переменных типа перечислений 168 Тип System.Enum 169 Динамическое обнаружение пар "имя/значение" перечисления 170 Типы структур 172 Создание переменных типа структур 173
Содержание 9 Типы значения и ссылочные типы 174 Типы значения, ссылочные типы и операция присваивания 175 Типы значения, содержащие ссылочные типы 177 Передача ссылочных типов по значению 178 Передача ссылочных типов по ссылке 179 Заключительные детали относительно типов значения и ссылочных типов 180 Нулевые типы в С # 181 Работа с нулевыми типами 183 Операция?? 184 Резюме 184 Глава 5. Определение инкапсулированных типов классов 185 Знакомство с типом класса С# 185 Размещение объектов с помощью ключевого слова new 187 Понятие конструктора 188 Роль конструктора по умолчанию 188 Определение специальных конструкторов 189 Еще раз о конструкторе по умолчанию 190 Роль ключевого слова this 191 Построение цепочки вызовов конструкторов с использованием this 193 Обзор потока конструктора 195 Еще раз об необязательных аргументах 196 Понятие ключевого слова static 197 Определение статических методов 198 Определение статических полей данных 198 Определение статических конструкторов 201 Определение статических классов 202 Основы объектно-ориентированного программирования 203 Роль инкапсуляции 204 Роль наследования 204 Роль полиморфизма 206 Модификаторы доступа С# 207 Модификаторы доступа по умолчанию 208 Модификаторы доступа и вложенные типы 208 Первый принцип: службы инкапсуляции С# 209 Инкапсуляция с использованием традиционных методов доступа и изменения 210 Инкапсуляция с использованием свойств .NET 212 Использование свойств внутри определения класса 214 Внутреннее представление свойств 215 Управление уровнями видимости операторов get/set свойств 217 Свойства, доступные только для чтения и только для записи 218 Статические свойства 218 Понятие автоматических свойств 219 Взаимодействие с автоматическими свойствами 221 Замечания относительно автоматических свойств и значений по умолчанию 221 Понятие синтаксиса инициализации объектов 223 Вызов специальных конструкторов с помощью синтаксиса инициализации 224 Инициализация вложенных типов 225 Работа с данными константных полей 226 Понятие полей только для чтения 228 Статические поля только для чтения 228 Понятие частичных типов 229 Резюме 230
10 Содержание Глава 6. Понятия наследования и полиморфизма 231 Базовый механизм наследования 231 Указание родительского класса для существующего класса 232 О множественном наследовании 234 Ключевое слово sealed 234 Изменение диаграмм классов Visual Studio 235 Второй принцип ООП: подробности о наследовании 236 Управление созданием базового класса с помощью ключевого слова base 238 Хранение фамильных тайн: ключевое слово protected 240 Добавление запечатанного класса 241 Реализация модели включения/делегации 242 Определения вложенных типов 243 Третий принцип ООП: поддержка полиморфизма в С# 245 Ключевые слова virtual и override 245 Переопределение виртуальных членов в Visual Studio 2010 247 Запечатывание виртуальных членов 248 Абстрактные классы 249 Полиморфный интерфейс 250 Сокрытие членов 253 Правила приведения к базовому и производному классу 255 Ключевое слово as 257 Ключевое слово is 257 Родительский главный класс System.Object 258 Переопределение System.Object.ToStringO 261 Переопределение System.Object.Equals() 261 Переопределение System.Object.GetHashCodeO 262 Тестирование модифицированного класса Ре г s on 263 Статические члены System.Object 264 Резюме 264 Глава 7. Структурированная обработка исключений 265 Ода ошибкам и исключениям 265 Роль обработки исключений в .NET 266 Составляющие процесса обработки исключений в .NET 267 Базовый класс System. Exception 268 Простейший пример 269 Генерация общего исключения 271 Перехват исключений 272 Конфигурирование состояния исключения 273 Свойство TargetSite 273 Свойство StackTrace 274 Свойство НеlpLink 275 Свойство Data 275 Исключения уровня системы (System. SystemException) 277 Исключения уровня приложения (System. ApplicationException) 278 Создание специальных исключений, способ первый 278 Создание специальных исключений, способ второй 280 Создание специальных исключений, способ третий 281 Обработка многочисленных исключений 282 Общие операторы catch 284 Передача исключений 285 Внутренние исключения 286 Блок finally 287 Какие исключения могут выдавать методы 287
Содержание 11 К чему приводят необрабатываемые исключения 288 Отладка необработанных исключений с помощью Visual Studio 289 Несколько слов об исключениях, связанных с поврежденным состоянием 290 Резюме 291 Глава 8. Время жизни объектов 292 Классы, объекты и ссылки 292 Базовые сведения о времени жизни объектов 293 CIL-код, генерируемый для ключевого слова new 294 Установка объектных ссылок в пи 11 296 Роль корневых элементов приложения 297 Поколения объектов 298 Параллельная сборка мусора в версиях .NET 1.0 — .NET 3.5 299 Фоновая сборка мусора в версии . NET 4.0 300 Тип System. GC 300 Принудительная активизация сборки мусора 302 Создание финализируемых объектов 304 Переопределение System.Object .Finalize () 305 Описание процесса финализации 307 Создание высвобождаемых объектов 307 Повторное использование ключевого слова using в С# 310 Создание финализируемых и высвобождаемых типов 311 Формализованный шаблон очистки 312 Отложенная инициализация объектов 314 Настройка процесса создания данных L a z у о 317 Резюме 318 Часть III. Дополнительные конструкции программирования на С# зш Глава 9. Работа с интерфейсами 320 Что собой представляют типы интерфейсов 320 Сравнение интерфейсов и абстрактных базовых классов 321 Определение специальных интерфейсов 324 Реализация интерфейса 326 Вызов членов интерфейса на уровне объектов 328 Получение ссылок на интерфейсы с помощью ключевого слова a s 329 Получение ссылок на интерфейсы с помощью ключевого слова i s 329 Использование интерфейсов в качестве параметров 330 Использование интерфейсов в качестве возвращаемых значений 332 Массивы типов интерфейсов 332 Реализация интерфейсов с помощью Visual Studio 2010 333 Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом 334 Проектирование иерархий интерфейсов 337 Множественное наследование в случае типов интерфейсов 338 Создание перечислимых типов (IE numerable и I Enumerator) 340 Создание методов итератора с помощью ключевого слова yield 343 Создание именованного итератора 344 Внутреннее представление метода итератора 345 Создание клонируемых объектов (ICloneable) 346 Более сложный пример клонирования 348 Создание сравнимых объектов (IComparable) 350. Указание множества критериев для сортировки (I Compare г) 353 Использование специальных свойств и специальных типов для сортировки 354 Резюме 355
12 Содержание Глава 10. Обобщения 356 Проблемы, связанные с необобщенными коллекциями 356 Проблема производительности 358 Проблемы с безопасностью типов 362 Роль параметров обобщенных типов 365 Указание параметров типа для обобщенных классов и структур 366 Указание параметров типа для обобщенных членов 367 Указание параметров типов для обобщенных интерфейсов 367 Пространство имен System.Collections.Generic 369 Синтаксис инициализации коллекций 370 Работа с классом List<T> 371 Работа с классом Stack<T> 373 Работа с классом Queue<T> 374 Работа с классом SortedSet<T> 375 Создание специальных обобщенных методов 376 Выведение параметра типа 378 Создание специальных обобщенных структур и классов 379 Ключевое слово default в обобщенном коде 380 Обобщенные базовые классы 381 Ограничение параметров типа 382 Примеры использования ключевого слова where 383 Недостаток ограничений операций 384 Резюме 385 Глава 11. Делегаты, события и лямбда-выражения 386 Понятие типа делегата .NET 386 Определение типа делегата в С# 387 Базовые классы System.MulticastDelegate и System.Delegate 389 Простейший пример делегата 391 Исследование объекта делегата 392 Отправка уведомлений о состоянии объекта с использованием делегатов 393 Включение группового вызова 396 Удаление целей из списка вызовов делегата 397 Синтаксис групповых преобразований методов 398 Понятие ковариантности делегатов 400 Понятие обобщенных делегатов 402 Эмуляция обобщенных делегатов без обобщений 403 Понятие событий С# 404 Ключевое слово event 405 "За кулисами" событий 406 Прослушивание входящих событий 407 Упрощенная регистрация событий с использованием Visual Studio 2010 408 Создание специальных аргументов событий 409 Обобщенный делегат Event Handle r<T> 410 Понятие анонимных методов С# 411 Доступ к локальным переменным 413 Понятие лямбда-выражений 413 Анализ лямбда-выражения 416 Обработка аргументов внутри множества операторов 417 Лямбда-выражения с несколькими параметрами и без параметров 418 Усовершенствование примера PrimAndProperCarEvents за счет использования лямбда-выражений 419 Резюме 419
Содержание 13 Глава 12. Расширенные средства языка С# 421 Понятие методов-индексаторов 421 Индексация данных с использованием строковых значений 423 Перегрузка методов-индексаторов 424 Многомерные индексаторы 425 Определения индексаторов в интерфейсных типах 425 Понятие перегрузки операций 426 Перегрузка бинарных операций 427 А как насчет операций += и -=? 429 Перегрузка унарных операций 429 Перегрузка операций эквивалентности 430 Перегрузка операций сравнения 431 .внутреннее представление перегруженных операций 432 Финальные соображения относительно перегрузки операций 433 Понятие преобразований пользовательских типов 434 Числовые преобразования 434 Преобразования между связанными типами классов 434 Создание специальных процедур преобразования 435 Дополнительные явные преобразования типа Square 437 Определение процедур неявного преобразования 438 Внутреннее представление процедур пользовательских преобразований 439 Понятие расширяющих методов 440 Понятие частичных методов 448 Понятие анонимных типов 450 Анонимные типы, содержащие другие анонимные типы 454 Работа с типами указателей 455 Ключевое слово un s a f e 456 Работа с операциями * и & 458 Небезопасная и безопасная функция обмена значений 459 Доступ к полям через указатели (операция ->) 459 Ключевое слово stackalloc 460 Закрепление типа ключевым словом fixed 460 Ключевое слово sizeof 461 Резюме 462 Глава 13. LINQ to Objects 463 Программные конструкции, специфичные для LINQ 463 Неявная типизация локальных переменных 464 Синтаксис инициализации объектов и коллекций 464 Лямбда-выражения 465 Расширяющие методы 466 Анонимные типы 466 Роль LINQ 467 Выражения LINQ строго типизированы 468 Основные сборки LINQ 468 Применение запросов LINQ к элементарным массивам 469 Решение без использования LINQ 470 Рефлексия результирующего набора LINQ 471 LINQ и неявно типизированные локальные переменные 472 LINQ и расширяющие методы 473 Роль отложенного выполнения 474 Роль немедленного выполнения 475 Возврат результата запроса LINQ 475 Возврат результатов LINQ через немедленное выполнение 476
14 Содержание Применение запросов LINQ к объектам коллекций 477 Доступ к содержащимся в контейнере подобъектам 478 Применение запросов LINQ к необобщенным коллекциям 478 Фильтрация данных с использованием OfType<T>() 479 Исследование операций запросов LINQ 480 Базовый синтаксис выборки 481 Получение подмножества данных 482 Проекция новых типов данных 482 Получение счетчиков посредством Enumerable • 484 Обращение результирующих наборов 484 Выражения сортировки 484 LINQ как лучшее средство построения диаграмм 485 Исключение дубликатов 486 Агрегатные операции LINQ 486 Внутреннее представление операторов запросов LINQ 487 Построение выражений запросов с использованием операций запросов 488 Построение выражений запросов с использованием типа Enumerable и лямбда-выражений 488 Построение выражений запросов с использованием типа Enumerable и анонимных методов 490 Построение выражений запросов с использованием типа Enumerable и низкоуровневых делегатов 490 Резюме 491 Часть IV. Программирование с использованием сборок .NET 493 Глава 14. Конфигурирование сборок .NET 494 Определение специальных пространств имен 494 Устранение конфликтов на уровне имен за счет использования полностью уточненных имен 496 Устранение конфликтов на уровне имен за счет использования псевдонимов 497 Создание вложенных пространств имен 498 Пространство имен, используемое по умолчанию в Visual Studio 2010 499 Роль сборок .NET 500 Сборки повышают возможность повторного использования кода 500 Сборки определяют границы типов 501 Сборки являются единицами, поддерживающими версии 501 Сборки являются самоописываемыми 501 Сборки поддаются конфигурированию 501 Формат сборки .NET 502 Заголовок файла Windows 502 Заголовок файла CLR 504 CIL-код, метаданные типов и манифест сборки 505 Необязательные ресурсы сборки 505 Однофайловые и многофайловые сборки 505 Создание и использование однофайловой сборки 506 Исследование манифеста 510 Исследование CIL-кода 512 Исследование метаданных типов 512 Создание клиентского приложения на С# 513 Создание клиентского приложения на Visual Basic 514 Межъязыковое наследование в действии 515 Создание и использование многофайловой сборки 516 Исследование файла uf о .netmodule 517
Содержание 15 Исследование файла airvehiсles .dll 518 Использование многофайловой сборки 518 Приватные сборки 519 Идентификационные данные приватной сборки 520 Процесс зондирования 520 Конфигурирование приватных сборок 521 Конфигурационные файлы и Visual Studio 2010 522 Разделяемые сборки 524 Строгие имена 524 Генерирование строгих имен в командной строке 526 Генерирование строгих имен с помощью Visual Studio 2010 528 Установка сборок со строгими именами в GAC 529 Просмотр содержимого GAC с помощью проводника Windows 530 Использование разделяемой сборки 531 Исследование манифеста SharedCarLibClient 532 Конфигурирование разделяемых сборок 533 Фиксация текущей версии разделяемой сборки 533 Создание разделяемой сборки версии 2.0.0.0 533 Динамическое перенаправление на конкретную версию разделяемой сборки 536 Сборки политик издателя 537 Отключение политик издателя 538 Элемент <codeBase> 538 Пространство имен System. Configuration 540 Резюме 541 Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 542 Необходимость в метаданных типов 542 Просмотр (части) метаданных перечисления EngineState 543 Просмотр (части) метаданных типа Саг 544 Изучение блока TypeRef 546 Просмотр метаданных самой сборки 546 Просмотр метаданных внешних сборок, на которые имеются ссылки в текущей сборке 546 Просмотр метаданных строковых литералов 547 Рефлексия 547 Класс System.Type 548 Получение информации о типе с помощью System. Ob j ect. GetType () 549 Получение информации о типе с помощью typeof () 549 Получение информации о типе с помощью System. Туре . GetType () 549 Создание специальной программы для просмотра метаданных 550 Рефлексия методов 550 Рефлексия полей и свойств 551 Рефлексия реализуемых интерфейсов 552 Отображение различных дополнительных деталей 552 Реализация метода Ma in () 552 Рефлексия обобщенных типов 554 Рефлексия параметров и возвращаемых значений методов 554 Динамически загружаемые сборки 556 Рефлексия разделяемых сборок 558 Позднее связывание 560 Класс System.Activator 560 Вызов методов без параметров 562 Вызов методов с параметрами 563 Роль атрибутов .NET 564
16 Содержание Потребители атрибутов 565 Применение предопределенных атрибутов в С# 565 Сокращенное обозначение атрибутов в С# 567 Указание параметров конструктора для атрибутов 567 Атрибут [Obsolete] в действии 567 Создание специальных атрибутов 568 Применение специальных атрибутов 569 Синтаксис именованных свойств 569 Ограничение использования атрибутов 570 Атрибуты уровня сборки и модуля 571 Файл Ass emb lylnfo.cs, генерируемый Visual Studio 2010 572 Рефлексия атрибутов с использованием раннего связывания 572 Рефлексия атрибутов с использованием позднего связывания 573 Возможное применение на практике рефлексии, позднего связывания и специальных атрибутов 575 Создание расширяемого приложения 576 Создание сборки CommonSnappableTypes . dll 576 Создание оснастки на С# 577 Создание оснастки на Visual Basic 577 Создание расширяемого приложения Windows Fbrms 578 Резюме 581 Глава 16. Процессы, домены приложений и контексты объектов 582 Роль процесса Windows 582 Роль потоков 583 Взаимодействие с процессами в рамках платформы .NET 585 Перечисление выполняющихся процессов 587 Изучение конкретного процесса 588 Изучение набора потоков процесса 588 Изучение набора модулей процесса 590 Запуск и останов процессов программным образом 592 Управление запуском процесса с использованием класса ProcessStartlnfo 592 Домены приложений .NET 594 Класс System.AppDomain 594 Взаимодействие с используемым по умолчанию доменом приложения 596 Перечисление загружаемых сборок 597 Получение уведомлений о загрузке сборок 598 Создание новых доменов приложений 599 Загрузка сборок в специальные домены приложений 600 Выгрузка доменов приложений программным образом 601 Границы контекстов объектов 602 Контекстно-свободные и контекстно-зависимые типы 603 Определение контекстно-зависимого объекта 604 Инспектирование контекста объекта 604 Итоговые сведения о процессах, доменах приложений и контекстах 606 Резюме 606 Глава 17. Язык CIL и роль динамических сборок 607 Причины для изучения грамматики языка CIL 607 Директивы, атрибуты и коды операций в CIL 608 Роль директив CIL 609 Роль атрибутов CIL ; 609 Роль кодов операций CIL 609 Разница между кодами операций и их мнемоническими эквивалентами в CIL 609
Содержание 17 Помещение и извлечение данных из стека в CIL 610 Двунаправленное проектирование 612 Роль меток в коде CIL 615 Взаимодействие с CIL: модификация файла * . i 1 616 Компиляция CIL-кодас помощью ilasm.exe 617 Создание CIL-кода с помощью SharpDevelop 618 Рольpeverify.exe 619 Использование директив и атрибутов в CIL 619 Добавление ссылок на внешние сборки в CIL 619 Определение текущей сборки в CIL 620 Определение пространств имен в CIL 620 Определение типов классов в CIL 621 Определение и реализация интерфейсов в CIL 622 Определение структур в CIL 623 Определение перечислений в CIL 623 Определение обобщений в CIL 623 Компиляция файла CILTypes . il 624 Соответствия между типами данных в библиотеке базовых классов .NET, C# и CIL 625 Определение членов типов в CIL 626 Определение полей данных в CIL 626 Определение конструкторов для типов в CIL 627 Определение свойств в CIL 627 Определение параметров членов 628 Изучение кодов операций в CIL 628 Директива .maxstack 631 Объявление локальных переменных в CIL 631 Отображение параметров на локальные переменные в CIL 632 Скрытая ссылка this 633 Представление итерационных конструкций в CIL 633 Создание сборки . NET на CIL 634 Создание С ILCars.dll 634 СозданиеCILCarClient.exe 637 Динамические сборки 638 Пространство имен System. Re f lection. Emit 639 Роль типа System. Reflection. Emit. ILGenerator 640 Создание динамической сборки 641 Генерация сборки и набора модулей 642 Роль типа Module Builder 643 Генерация типа HelloClassn принадлежащей ему строковой переменной 644 Генерация конструкторов 645 Генерация метода S а у Н е 11 о () 646 Использование динамически сгенерированной сборки 646 Резюме 647 Глава 18. Динамические типы и исполняющая среда динамического языка 648 Роль ключевого слова С# dynamic 648 Вызов членов на динамически объявленных данных 650 Роль сборки Microsoft.CSharp.dll 651 Область применения ключевого слова dynamic 652 Ограничения ключевого слова dynamic 653 Практическое применение ключевого слова dynamic 653 Роль исполняющей среды динамического языка (DLR) 654 Роль деревьев выражений 654 Роль пространства имен System. Dynamic 655
18 Содержание Динамический поиск в деревьях выражений во время выполнения 655 Упрощение вызовов позднего связывания с использованием динамических типов 656 Использование ключевого слова dynamic для передачи аргументов 657 Упрощение взаимодействия с СОМ посредством динамических данных 659 Роль первичных сборок взаимодействия 660 Встраивание метаданных взаимодействия 661 Общие сложности взаимодействия с СОМ 661 Взаимодействие с СОМ с использованием средств языка С# 4.0 662 Взаимодействие с СОМ без использования средств языка С# 4.0 666 Резюме 667 Часть V. Введение в библиотеки базовых классов .NET 669 Глава 19. Многопоточность и параллельное программирование 670 Отношения между процессом, доменом приложения, контекстом и потоком 670 Проблема параллелизма 671 Роль синхронизации потоков 672 Краткий обзор делегатов .NET 672 Асинхронная природа делегатов 674 Методы BeginlnvokeO и EndlnvokeO ч 675 Интерфейс System.IAsyncResult 675 Асинхронный вызов метода 676 Синхронизация вызывающего потока 676 Роль делегата AsyncCallback 678 Роль класса AsyncResult 679 Передача и прием специальных данных состояния 680 Пространство имен System.Threading 681 Класс System.Threading.Thread 682 Получение статистики о текущем потоке 683 Свойство Name 684 Свойство Priority 684 Программное создание вторичных потоков 685 Работа с делегатом ThreadStart 685 Работа с делегатом ParametrizedThreadStart 687 Класс AutoResetEvent 688 Потоки переднего плана и фоновые потоки 689 Пример проблемы, связанной с параллелизмом 690 Синхронизация с использованием ключевого слова С# 1о с к 692 Синхронизация с использованием типа System.Threading.Monitor 694 Синхронизация с использованием типа System.Threading. Interlocked 695 Синхронизация с использованием атрибута [Synchronization] 696 Программирование с использованием обратных вызовов Timer 697 Пул потоков CLR 698 Параллельное программирование на платформе .NET 700 Интерфейс Tksk Parallel Library API 700 Роль класса Parallel 701 Понятие параллелизма данных 702 Класс Task 703 Обработка запроса на отмену 704 Понятие параллелизма задач 705 Запросы параллельного LINQ (PLINQ) 708 Выполнение запроса PLINQ 709 Отмена запроса PLINQ 709 Резюме 710
Содержание 19 Глава 20. Файловый ввод-вывод и сериализация объектов 711 Исследование пространства имен System.10 711 Классы Directory (Directorylnfo) и File (FileInfo) 712 Абстрактный базовый класс FileSystemlnfo 713 Работа с типом Directorylnfo 714 Перечисление файлов с помощью типа Directorylnfo 715 Создание подкаталогов с помощью типа Directorylnfo 716 Работа с типом Directory 717 Работа с типом Drive Info < 717 Работа с классом File Info 719 Метод Filelnfo.Create () 719 Метод FileJnfo.OpenO 720 Методы FileOpen.OpenRead() и Filelnfo.OpenWrite() 721 Метод Filelnfo.OpenText() 722 Методы Filelnfо.CreateTextO и Filelnfo.AppendTextO 722 Работа с типом File 722 Дополнительные члены File 723 Абстрактный класс Stream 724 Работа с классом FileStream 725 Работа с классами StreamWriter и StreamReader 726 Запись в текстовый файл 727 Чтение из текстового файла 728 Прямое создание экземпляров классов StreamWriter/StreamReader 729 Работа с классами StringWriter и StringReader 730 Работа с классами BinaryWriter и BinaryReader 731 Программное отслеживание файлов 732 Понятие сериализации объектов 734 Роль графов объектов 736 Конфигурирование объектов для сериализации 737 Определение сериализуемых типов 737 Общедоступные поля, приватные поля и общедоступные свойства 738 Выбор форматера сериализации 738 Интерфейсы IFormatter и IRemotingFormatter 739 Точность типов среди форматеров 740 Сериализация объектов с использованием BinaryFormatter 741 Десериализация объектов с использованием BinaryFormatter 742 Сериализация объектов с использованием SoapFormatter 743 Сериализация объектов с использованием XmlSerializer 743 Управление генерацией данных XML 744 Сериализация коллекций объектов 746 Настройка процессов сериализации SOAP и двоичной сериализации 747 Углубленный взгляд на сериализацию объектов 748 Настройка сериализации с использованием интерфейса ISerializable 749 Настройка сериализации с использованием атрибутов 751 Резюме 752 Глава 21. AD0.NET, часть I: подключенный уровень 754 Высокоуровневое определение ADO. NET 754 Три стороны ADO.NET 755 Поставщики данных ADO.NET 756 Поставщики данных ADO.NET от Microsoft 757 О сборке System.Data.OracleClient.dll 759 Получение сторонних поставщиков данных ADO.NET 759 Дополнительные пространства имен ADO.NET 759
20 Содержание Типы из пространства имен System.Data 760 Роль интерфейса IDbConпесtion 761 Роль интерфейса I DbTrans act ion 762 Роль интерфейса IDbCommand 762 Роль интерфейсов IDbDataParameter и IDataParameter 762 Роль интерфейсов IDbDataAdapter и IDataAdapter 763 Роль интерфейсов IDataReader и IDataRecord 763 Абстрагирование поставщиков данных с помощью интерфейсов 764 Повышение гибкости с помощью конфигурационных файлов приложения 766 Создание базы данных AutoLot 767 Создание таблицы Inventory 767 Создание хранимой процедуры GetPetName () 769 Создание таблиц Customers и Orders 770 Визуальное создание отношений между таблицами 772 Модель генератора поставщиков данных ADO. NET 111 Полный пример генератора поставщиков данных 774 Возможные трудности с моделью генератора поставщиков 776 Элемент<connectionStrings> 777 Подключенный уровень ADO.NET 778 Работа с объектами подключения 779 Работа с объектами ConnectionStringBuilder 781 Работа с объектами команд 782 Работа с объектами чтения данных 783 Получение множественных результатов с помощью объекта чтения данных 784 Создание повторно используемой библиотеки доступа к данным 785 Добавление логики подключения 786 Добавление логики вставки 787 Добавление логики удаления 788 Добавление логики изменения 788 Добавление логики выборки 789 Работа с параметризованными объектами команд 790 Выполнение хранимой процедуры 792 Создание консольного пользовательского интерфейса 793 Реализация метода М a i n () 794 Реализация метода Showlnstructions () 795 Реализация метода List Inventory () 795 Реализация метода DeleteCarO 796 Реализация метода InsertNewCar () 797 Реализация метода UpdateCarPetName () 797 Реализация метода LookUpPetName () 798 Транзакции баз данных 799 Основные члены объекта транзакции ADO.NET 800 Добавление таблицы CreditRisksB базу данных AutoLot 800 Добавление метода транзакции в InventoryDAL 801 Тестирование транзакции в нашей базе данных 802 Резюме 803 Глава 22. AD0.NET, часть II: автономный уровень 804 Знакомство с автономным уровнем ADO. NET 804 Роль объектов Data Set 805 Основные свойства класса Data Set 806 Основные методы класса Data Set 807 Создание DataSet 807 Работа с объектами DataColumn 808
Содержание 21 Создание объекта DataColumn 809 Включение автоинкрементных полей 810 Добавление объектов DataColumn в DataTable 810 Работа с объектами DataRow 810 Свойство RowState 812 Свойство DataRowVersion 813 Работа с объектами DataTable 814 Вставка объектов DataTable в DataSet 815 Получение данных из объекта DataSet 815 Обработка данных из DataTable с помощью объектов DataTableReader 816 Сериализация объектов DataTable и DataSet в формате XML 817 Сериализация объектов DataTable и DataSet в двоичном формате 818 Привязка объектов DataTable к графическим интерфейсам Windows Forms 819 Заполнение DataTable из обобщенного List<T> 820 Удаление строк из DataTable 822 Выборка строк с помощью фильтра 823 Изменение строк в DataTable 826 Работа с типом DataView 826 Работа с адаптерами данных 828 Простой пример адаптера данных 829 Замена имен из базы данных более понятными названиями 829 Добавление в AutoLotDAL. dl 1 возможности отключения 830 Определение начального класса 831 Настройка адаптера данных с помощью SqlCommandBuilder 831 Реализация метода GetAlllnventory () 833 Реализация метода Updatelnventory () 833 Установка номера версии 833 Тестирование автономной функциональности 833 Объекты DataSet для нескольких таблиц и взаимосвязь данных 834 Подготовка адаптеров данных 835 Создание отношений между таблицами 836 Изменение таблиц базы данных 837 Переходы между взаимосвязанными таблицами 837 Средства конструктора баз данных в Windows Forms 839 Визуальное проектирование элементов DataGridView 840 Сгенерированный файл арр. conf ig 843 Анализ строго типизированного DataSet 843 Анализ строго типизированного DataTable 845 Анализ строго типизированного DataRow 845 Анализ строго типизированного адаптера данных 845 Завершение приложения Windows Forms 846 Выделение строго типизированного кода работы с базами данных в библиотеку классов 847 Просмотр сгенерированного кода 848 Выборка данных с помощью сгенерированного кода 849 Вставка данных с помощью сгенерированного кода 850 Удаление данных с помощью сгенерированного кода 850 Вызов хранимой процедуры с помощью сгенерированного кода 851 Программирование с помощью LINQ to DataSet 851 Библиотека расширений DataSet 853 Получение DataTable, совместимого с LINQ 853 Метод расширения DataRowExtensions.Field<T>() 855 Заполнение новых объектов DataTable с помощью LINQ-запросов 855 Резюме 856
22 Содержание Глава 23. ADO.NET, часть III: Entity Framework 857 Роль Entity Framework 857 Роль сущностей 859 Строительные блоки Entity Framework 860 Роль служб объектов 861 Роль клиента сущности 861 Роль файла *.edmx 863 Роль классов ObjectContext и ObjectSet<T> 863 Собираем все вместе 865 Построение и анализ первой модели EDM 866 Генерация файла *. е dmx 866 Изменение формы сущностных данных 869 Просмотр отображений 871 Просмотр данных сгенерированного файла *. еdmx 871 Просмотр сгенерированного исходного кода 873 Улучшение сгенерированного исходного кода 875 Программирование с использованием концептуальной модели 875 Удаление записи 876 Обновление записи 877 Запросы с помощью LINQ to Entities 878 Запросы с помощью Entity SQL 879 Работа с объектом EntityDataReader 880 Проект AutoLotDAL версии 4.0, теперь с сущностями 881 Отображение хранимой процедуры 881 Роль навигационных свойств 882 Использование навигационных свойств внутри запросов LINQ to Entity 884 Вызов хранимой процедуры 885 Привязка данных сущностей к графическим пользовательским интерфейсам Windows Fbrms 886 Добавление кода привязки данных 888 Резюме 890 Глава 24. Введение в LINQ to XML 891 История о двух API-интерфейсах XML 891 Интерфейс LINQ to XML как лучшая модель DOM 893 Синтаксис литералов Visual Basic как наилучший интерфейс LINQ to XML 893 Члены пространства имен System.Xml.Linq 895 Осевые методы LINQ to XML 895 Избыточность XName (и XNamespace) 897 Работа с XElement HXDocument 898 Генерация документов из массивов и контейнеров 899 Загрузка и разбор XML-содержимого 901 Манипулирование XML-документом в памяти 901 Построение пользовательского интерфейса приложения LINQ to XML 901 Импорт файла Inventory, xml 902 Определение вспомогательного класса LINQ to XML 902 Оснащение пользовательского интерфейса вспомогательными методами 904 Резюме 905 Глава 25. Введение в Windows Communication Foundation 906 API-интерфейсы распределенных вычислений 906 Роль DCOM 907 Роль служб СОМ+/Enterprise Services 908 Роль MSMQ 909
Содержание 23 Роль .NET Remotlng 909 Роль веб-служб XML 910 Именованные каналы, сокеты и Р2Р 913 Роль^УСК 913 Обзор средств WCF 914 Обзор архитектуры, ориентированной на службы 914 WCF: итоги 915 Исследование основных сборок WCF 916 Шаблоны проектов WCF в Visual Studio 917 Шаблон проекта WCF Service 918 Базовая композиция приложения WCF 919 Понятие ABC в WCF 920 Понятие контрактов WCF 920 Понятие привязок WCF 921 Понятие адресов WCF 924 Построение службы WCF 925 Атрибут [ServiceContract] 926 Атрибут [OperationContract] 927 Служебные типы как контракты операций 928 Хостинг службы WCF 928 УстановкаАВСвнутрифайлаАрр.соп:£1д 929 Кодирование с использованием типа ServiceHost 930 Указание базового адреса 930 Подробный анализ типа ServiceHost 932 Подробный анализ элемента <system.serviceModel> 933 Включение обмена метаданными 934 Построение клиентского приложения WCF 936 Генерация кода прокси с использованием svcutil.exe 937 Генерация кода прокси с использованием Visual Studio 2010 938 Конфигурирование привязки на основе TCP 939 Упрощение конфигурационных настроек в WCF 4.0 940 Конечные точки по умолчанию в WCF 4.0 941 Предоставление одной службы WCF с использованием множества привязок 942 Изменение установок для привязки WCF 943 Конфигурация поведения МЕХ по умолчанию в WCF 4.0 944 Обновление клиентского прокси и выбор привязки 945 Использование шаблона проекта WCF Service Library 946 Построение простой математической службы 947 Тестирование службы WCFс помощью WcfTestClient.exe 947 Изменение конфигурационных файлов с помощью SvcConfigEditor.exe 948 Хостинг службы WCF в виде службы Windows 949 Спецификация ABC в коде 950 Включение МЕХ 951 Создание программы установки для службы Windows 952 Установка службы Windows 953 Асинхронный вызов службы на стороне клиента 954 Проектирование контрактов данных WCF 955 Использование веб-ориентированного шаблона проекта WCF Service 956 Реализация контракта службы 958 Роль файла *.svc 959 Содержимое файла Web. с on fig 959 Тестирование службы 960 Резюме 960
24 Содержание Глава 26. Введение в Windows Workflow Foundation 4.0 961 Определение бизнес-процесса 962 Роль WF 4.0 962 Построение простого рабочего потока 963 Просмотр полученного кода XAML 965 Исполняющая среда WF 4.0 967 Хостинг рабочего потока с использованием класса Workf lowlnvoker 967 Хостинг рабочего потока с использованием класса Workf lowApplication 970 Переделка первого рабочего потока 971 Знакомство с действиями Windows Workflow 4.0 971 Действия потока управления 971 Действия блок-схемы 972 Действия обмена сообщениями 973 Действия исполняющей среды и действия-примитивы 973 Действия транзакций 974 Действия над коллекциями и действия обработки ошибок 974 Построение рабочего потока в виде блок-схемы 975 Подключение действий к блок-схеме 975 Работа с действием InvokeMethod 976 Определение переменных уровня рабочего потока 977 Работа с действием FlowDecision 978 Работа с действием TerminateWorkf low 978 Построение условия "true" 979 Работа с действием ForEach<T> 979 Завершение приложения 981 Промежуточные итоги 982 Изоляция рабочих потоков в выделенных библиотеках 984 Определение начального проекта 984 Импорт сборок и пространств имен 985 Определение аргументов рабочего потока 986 Определение переменных рабочего потока 986 Работа с действием Assign 987 Работа с действиями IfnSwitch 987 Построение специального действия кода 988 Использование библиотеки рабочего потока 991 Получение выходного аргумента рабочего потока 992 Резюме 993 Часть VI. Построение настольных пользовательских приложений с помощью WPF 995 Глава 27. Введение в Windows Presentation Foundation и XAML 996 Мотивация, лежащая в основе WPF 997 Унификация различных API-интерфейсов 997 Обеспечение разделения ответственности через XAML 998 Обеспечение оптимизированной модели визуализации 998 Упрощение программирования сложных пользовательских интерфейсов 999 Различные варианты приложений WPF 1000 Традиционные настольные приложения 1000 WPF-приложения на основе навигации 1001 Приложения ХВАР 1002 Отношения между WPF и Silverlight 1003 Исследование сборок WPF 1004
Содержание 25 Роль класса Application 1005 Роль класса Wi ndow 1007 Роль класса System.Windows.Controls.ContentControl 1007 Роль класса System.Windows.Controls.Control 1008 Роль класса System.Windows.FrameworkElement 1009 Роль класса System.Windows.UIElement 1010 Роль класса System.Windows.Media.Visual 1010 Роль класса System. Windows. DependencyObject 1010 Роль класса System.Windows.Threading.DispatcherObject 1011 Построение приложения WPF без XAML 1011 Создание строго типизированного окна 1013 Создание простого пользовательского интерфейса 1013 Взаимодействие с данными уровня приложения 1015 Обработка закрытия объекта Window 1016 Перехват событий мыши 1017 Перехват клавиатурных событий 1018 Построение приложения WPF с использованием только XAML 1019 Определение Ma inWindow в XAML 1020 Определение объекта Application в XAML 1021 Обработка файлов XAML с помощью msbuild.exe 1022 Трансформация разметки в сборку .NET 1023 Отображение XAML-данных окна на код С# 1024 PoльBAML 1025 Отображение XAML-данных приложения на код С# 1026 Итоговые замечания о процессе трансформирования XAML в сборку 1027 Синтаксис XAML для WPF 1027 Введение в Kaxaml 1027 Пространства имен XAML XML и "ключевые слова" XAML 1029 Управление объявлениями классов и переменных-членов 1031 Элементы XAML, атрибуты XAML и преобразователи типов 1032 Понятие синтаксиса XAML "свойство-элемент" 1033 Понятие присоединяемых свойств XAML 1034 Понятие расширений разметки XAML 1034 Построение приложений WPF с использованием файлов отделенного кода 1036 Добавление файла кода для класса MainWindow 1036 Добавление файла кода для класса МуАрр 1037 Обработка файлов кода с помощью msbuild.exe 1038 Построение приложений WPF с использованием Visual Studio 2010 1038 Шаблоны проектов WPF 1039 Знакомство с инструментами визуального конструктора WPF 1039 Проектирование графического интерфейса окна 1042 Реализация события Loaded 1044 Реализация события Click объекта Button 1045 Реализация события Closed 1046 Тестирование приложения 1046 Резюме 1047 Глава 28. Программирование с использованием элементов управления WPF 1048 Обзор библиотеки элементов управления WPF 1048 Работа с элементами управления WPF в Visual Studio 2010 1049 Элементы управления Ink API 1051 Элементы управления документами WPF 1051 Общие диалоговые окна WPF 1052 Подробные сведения находятся в документации 1052
26 Содержание Управление компоновкой содержимого с использованием панелей 1053 Позиционирование содержимого внутри панелей Canvas 1054 Позиционирование содержимого внутри панелей WrapPanel 1056 Позиционирование содержимого внутри панелей StackPanel 1058 Позиционирование содержимого внутри панелей Grid 1059 Позиционирование содержимого внутри панелей DockPanel 1060 Включение прокрутки в типах панелей 1061 Построение главного окна с использованием вложенных панелей 1062 Построение системы меню 1063 Построение панели инструментов 1064 Построение строки состояния 1065 Завершение дизайна пользовательского интерфейса 1065 Реализация обработчиков событий MouseEnter/MouseLeave 1066 Реализация логики проверки правописания 1066 Понятие управляющих команд WPF 1067 Внутренние объекты управляющих команд 1067 Подключение команд к свойству Command 1068 Подключение команд к произвольным действиям 1069 Работа с командами Open и Save 1071 Построение пользовательского интерфейса WPF с помощью Expression Blend 1072 Ключевые аспекты IDE-среды Expression Blend 1073 Использование элемента TabControl 1077 Построение вкладки Ink API 1079 Проектирование элемента Tool Bar 1080 Элемент управления RadioButton 1082 Элемент управления InkCanvas 1084 Элемент управления С оmboB ox 1086 Сохранение, загрузка и очистка данных InkCanvas 1087 Введение в интерфейс Documents API 1088 Блочные элементы и встроенные элементы 1088 Диспетчеры компоновки документа 1089 Построение вкладки Documents 1089 Наполнение FlowDocument с использованием Blend 1091 Наполнение FlowDocument с помощью кода 1091 Включение аннотаций и "клейких" заметок 1093 Сохранение и загрузка потокового документа 1094 Введение в модель привязки данных WPF 1095 Построение вкладки Data Binding 1096 Установка привязки данных с использованием Blend 1096 Свойство DataContext 1097 Преобразование данных с использованием IValueConverter 1099 Установка привязок данных в коде 1099 Построение вкладки DataGrid 1100 Резюме 1102 Глава 29. Службы визуализации графики WPF 1103 Службы графической визуализации WPF 1103 Опции графической визуализации WPF 1104 Визуализация графических данных с использованием фигур 1105 Добавление прямоугольников, эллипсов и линий на поверхность Canvas 1107 Удаление прямоугольников, эллипсов и линий с поверхности Canvas 1110 Работа с элементами Polyline и Polygon 1110 Работа с элементом Path 1111 Кисти и перья WPF 1115
Содержание 27 Конфигурирование кистей с использованием Visual Studio 2010 1115 Конфигурирование кистей в коде 1117 Конфигурирование перьев 1118 Применение графических трансформаций 1118 Первый взгляд на трансформации 1119 Трансформация данных Canvas 1120 Работа с фигурами в Expression Blend 1122 Выбор фигуры для визуализации из палитры инструментов 1122 Преобразование фигур в пути 1123 Комбинирование фигур 1123 Редакторы кистей и трансформаций 1123 Визуализация графических данных с использованием рисунков и геометрий 1125 Построение кисти DrawingBrush с использованием объектов Geometry 1126 Рисование с помощью DrawingBrush 1127 Включение типов Drawing в Drawinglmage 1128 Генерация сложной векторной графики с использованием Expression Design 1128 Экспорт документа Expression Design в XAML 1129 Визуализация графических данных с использованием визуального уровня 1130 Базовый класс Vi s u a 1 и производные дочерние классы 1130 Первый взгляд на класс DrawingVisual 1131 Визуализация графических данных в специальном диспетчере компоновки 1133 Реагирование на операции проверки попадания 1134 Резюме 1136 Глава 30. Ресурсы, анимация и стили WPF 1137 Система ресурсов WPF 1137 Работа с двоичными ресурсами 1138 Программная загрузка изображения 1139 Работа с объектными (логическими) ресурсами 1142 Роль свойства Resources 1143 Определение ресурсов уровня окна 1143 Расширение разметки {StaticResource} 1145 Изменение ресурса после извлечения 1145 Расширение разметки {DynamicResource} 1146 Ресурсы уровня приложения 1146 Определение объединенных словарей ресурсов 1147 Определение сборки из одних ресурсов 1149 Извлечение ресурсов в Expression Blend 1150 Службы анимации WPF 1152 Роль классов анимации 1152 Свойства То, From и By 1153 Роль базового класса Timeline 1154 Написание анимации в коде С# 1154 Управление темпом анимации 1155 Запуск в обратном порядке и циклическое выполнение анимации 1156 Описание анимации в XAML 1157 Роль раскадровки 1158 Роль триггеров событий 1158 Анимация с использованием дискретных ключевых кадров s 1159 Роль стилей WPF 1160 Определение и применение стиля 1160 Переопределение настроек стиля 1161 Автоматическое применение стиля с помощью TargetType 1161 Создание подклассов существующих стилей 1162
28 Содержание Роль безымянных стилей 1163 Определение стилей с триггерами 1164 Определение стилей с множеством триггеров 1164 Анимированные стили 1165 Программное применение стилей 1165 Генерация стилей с помощью Expression Blend 1166 Работа с визуальными стилями по умолчанию 1167 Резюме 1169 Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1170 Роль свойств зависимости 1170 Проверка существующего свойства зависимости 1172 Важные замечания относительно оболочек свойств CLR 1175 Построение специального свойства зависимости 1176 Добавление процедуры проверки достоверности данных 1179 Реакция на изменение свойства 1179 Маршрутизируемые события 1181 Роль маршрутизируемых пузырьковых событий 1182 Продолжение или прекращение пузырькового распространения 1182 Роль маршрутизируемых туннелируемых событий 1183 Логические деревья, визуальные деревья и шаблоны по умолчанию 1185 Программный просмотр логического дерева 1185 Программный просмотр визуального дерева 1187 Программный просмотр шаблона по умолчанию для элемента управления 1188 Построение специального шаблона элемента управления в Visual Studio 2010 1191 Шаблоны как ресурсы 1192 Включение визуальных подсказок с использованием триггеров 1193 Роль расширения разметки {TemplateBinding} 1194 Роль класса ContentPresenter 1196 Включение шаблонов в стили 1196 Построение специальных элементов UserControl с помощью Expression Blend 1197 Создание проекта библиотеки UserControl 1198 Создание WPF-приложения JackpotDeluxe 1204 Извлечение UserControl из геометрических объектов 1204 Роль визуальных состояний .NET 4.0 1205 Завершение приложения JackpotDeluxe 1209 Резюме 1212 Часть VII. Построение веб-приложений с использованием ASP.NET 1213 Глава 32. Построение веб-страниц ASP.NET 1214 Роль протокола HTTP 1214 Цикл запрос / ответ HTTP 1215 HTTP — протокол без поддержки состояния 1215 Веб-приложения и веб-серверы 1216 Роль виртуальных каталогов IIS 1216 Веб-сервер разработки ASP NET 1217 Роль языка HTML 1217 Структура HTML-документа 1218 Роль формы HTML 1219 Инструменты визуального конструктора HTML в Visual Studio 2010 1219 Построение формы HTML 1220 Роль сценариев клиентской стороны 1221 Пример сценария клиентской стороны 1223
Содержание 29 Обратная отправка веб-серверу 1224 Обратные отправки в ASP.NET 1225 Набор средств API-интерфейса ASP NET 1225 Основные средства ASP.NET 1.0-1.1 1225 Основные средства ASP.NET 2.0 1227 Основные средства ASP NET 3.5 (и .NET 3.5 SP1) 1228 Основные средства ASP.NET 4.0 1228 Построение однофайловой веб-страницы ASP.NET 1229 Ссылка на сборку AutoLotDAL.dll 1229 Проектирование пользовательского интерфейса 1230 Добавление логики доступа к данным 1231 Роль директив ASP NET 1233 Анализ блока script 1235 Анализ объявлений элементов управления ASP NET 1235 Цикл компиляции для однофайловых страниц 1236 Построение веб-страницы ASP.NET с использованием файлов кода 1237 ^Ссылка на сборку AutoLotDAL.dll 1239 Обновление файла кода 1240 Цикл компиляции многофайловых страниц 1240 Отладка и трассировка страниц ASP NET 1241 Веб-сайты и веб-приложения ASP.NET 1242 Структура каталогов веб-сайта ASP.NET 1243 Ссылаемые сборки 1244 Роль папки App_Code 1244 Цепочка наследования типа Page 1245 Взаимодействие с входящим запросом HTTP 1246 Получение статистики браузера 1247 Доступ к входным данным формы 1248 Свойство IsPostBack 1248 Взаимодействие с исходящим ответом HTTP 1249 Выдача HTML-содержимого 1250 Перенаправление пользователей 1250 Жизненный цикл веб-страницы ASP.NET 1251 Роль атрибута AutoEventWireup 1252 Событие Error 1253 Роль файла Web. con fig 1254 Утилита администрирования веб-сайтов ASP.NET 1255 Резюме 1256 Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1257 Природа веб-элементов управления 1257 Обработка событий серверной стороны 1258 Свойство AutoPostBack 1259 Базовые классы Control и WebControl 1260 Перечисление содержащихся элементов управления 1260 Динамическое добавление и удаление элементов управления 1262 Взаимодействие с динамически созданными элементами управления 1263 Функциональность базового класса WebControl 1264 Основные категории веб-элементов управления ASP NET 1265 Краткая информация о System.Web.UI.HtmlControls 1266 Документация по веб-элементам управления 1267 Построение веб-сайта ASPNET Cars e 1268 Работа с мастер-страницами 1268 Определение страницы содержимого Default.aspx 1274
30 Содержание Проектирование страницы содержимого Inventory, aspx 1276 Проектирование страницы содержимого BuildCar. aspx 1279 Роль элементов управления проверкой достоверности 1282 Класс RequiredFieldValidator 1283 Класс RegularExpressionValidator 1284 Класс RangeValidator 1284 Класс CompareValidator 1284 Создание итоговой панели проверки достоверности 1285 Определение групп проверки достоверности 1286 Работа с темами 1288 Файлы*, skin 1289 Применение тем ко всему сайту 1291 Применение тем на уровне страницы 1291 Свойство SkinID 1291 Программное назначение тем 1292 Резюме 1293 Глава 34. Управление состоянием в ASP.NET 1294 Проблема поддержки состояния 1294 Приемы управления состоянием ASP. NET 1296 Роль состояния представления ASP NET 1296 Демонстрация работы с состоянием представления 1297 Добавление специальных данных в состояние представления 1299 Роль файла Global.asax 1299 Глобальный обработчик исключений "последнего шанса" 1301 Базовый класс HttpApplication 1302 Различие между свойствами Application и Session 1303 Поддержка данных состояния уровня приложения 1303 Модификация данных приложения 1305 Обработка останова веб-приложения 1306 Работа с кэшем приложения 1307 Работа с кэшированием данных 1307 Модификация файла *. a sрх 1309 Поддержка данных сеанса 1311 Дополнительные члены HttpSessionState 1314 Cookie-наборы 1315 Создание cookie-наборов 1315 Чтение входящих cookie-данных 1316 Роль элемента <sessionState> 1317 Хранение данных сеанса на сервере состояния сеансов ASP NET 1317 Хранение информации о сеансах в выделенной базе данных 1318 Интерфейс ASPNET Profile API 1319 База данных ASPNETDB.mdf 1319 Определение пользовательского профиля BWeb.config 1320 Программный доступ к данным профиля 1322 Группирование данных профиля и сохранение специальных объектов 1323 Резюме 1325 Часть VIII. Приложения 1327 Приложение А. Программирование с помощью Windows Forms 1328 Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1369 Предметный указатель 1386
Об авторе Эндрю Троелсен (Andrew Trolesen) с любовью вспоминает свой самый первый компьютер Atari 400, оснащенный кассетным устройством хранения и черно-белым телевизионным монитором "(который его родители разрешили ему поставить у себя в спальне, за что им большое спасибо). Еще он благодарен ушедшему в небытие журналу Compute!, степени бакалавра в области математической лингвистики и трем годам формального изучения санскрита. Все это оказало значительное влияние на его сегодняшнюю карьеру. В настоящее время Эндрю работает в центре Intertech, занимающемся консультированием и обучением работе с .NET и Java (www .intertech. com). На его счету уже несколько написанных книг, в числе которых Developer's Workshop to COM and ATL 3.0 {Wordware Publishing', 2000 г.), COMand.NET'Interoperability (Apress, 2002 r.) и Visual Basic 2008 and the .NET 3.5 Platform: An Advanced Guide (Apress, 2008 г.). 0 техническом редакторе Энди Олсен (Andy Olsen) — независимый консультант и инструктор, проживающий в Великобритании. Энди работает с .NET, начиная с самой первой бета-версии этого продукта, и занимается активным исследованием новых функциональных возможностей, которые появились в .NET 4.O. Он живет у моря в городе Суонси вместе со своей женой Джейн и детьми Эмили и Томом. Любит делать пробежки вдоль побережья (регулярно останавливаясь на чашечку кофе по пути), кататься на лыжах и следить за лебедями и орликами. Связаться с ним можно по адресу andyo@olsensof t. com. Благодарности По какой-то непонятной причине (а может и целому ряду причин) настоящее издание книги оказалось гораздо более трудным в написании, чем ожидалось. Если бы не помощь и поддержка многих хороших людей, скорее всего, оно не вышло бы в свет так скоро. Прежде всего, огромное спасибо техническому редактору Энди Олсену. Помимо указания на пропущенные точки с запятой он привнес массу замечательных предложений, позволивших сделать первоначальные примеры кода более понятными и точными. Спасибо тебе, Энди! Далее хочу выразить благодарность всей команде литературных редакторов, а именно — Мэри Бер (Магу Вепг), Патрику Мидеру (Patrick Meader), Кэти Стэнс (Katie Stence) и Шэрон Тердеман (Sharon Terdeman), которые выявили и устранили массу грамматических ошибок. Особая благодарность Дебре Кэлли (Debra Kelly) из Apress. Это наш первый совместный проект и, несмотря на многочисленные опоздания с предоставлением глав, путаницу с электронными письмами и постоянное добавление обновлений, я все равно очень надеюсь, что она согласится работать со мной снова. Спасибо тебе, Дебра! И, наконец, напоследок спасибо моей жене Мэнди. Как всегда, ты поддерживаешь меня в здравом уме во время написания всех моих проектов.
Введение Первое издание настоящей книги появилось вместе с выпуском Microsoft второй бета-версии .NET 1.0 (летом 2001 г.) и с тех пор постоянно переиздавалось в том или ином виде. С того самого времени автор с чрезвычайным удовольствием и благодарностью наблюдал за тем, как данная работа продолжала пользоваться популярностью в прессе и, самое главное, среди читателей. Через некоторое время, в 2002 г., она даже была номинирована на премию Jolt Award (которую, увы, получить так и не удалось, но книга попала в число финалистов), а в 2003 г. — еще и на премию Referenceware Excellence Award, где стала лучшей книгой года по программированию. Даже еще более важно то, что автору стали приходить электронные письма от читателей со всего мира. Общаться с множеством людей и узнавать, что данная книга как-то помогла им в карьере, просто замечательно. В связи с этим, хотелось бы отметить, что настоящая книга с каждым разом становится все лучше именно благодаря читателям, которые присылают различные предложения по улучшению, указывают на допущенные в тексте опечатки и обращают внимание на прочие промахи. Автор был просто ошеломлен, узнав, что эта книга использовалась и продолжает использоваться на занятиях в колледжах и университетах и является обязательной для прочтения на многих предвыпускных и выпускных курсах в области вычислительной техники. Автор благодарит прессу, читателей, преподавателей и всех остальных и желает им успешного программирования! С самого первого выпуска настоящей книги автор прилагал все усилия и обновлял книгу так, чтобы она отражала текущие возможности каждой выходившей версии платформы .NET. В настоящем издании материал был полностью пересмотрен и расширен с целью охвата новых средств языка С# 2010 и платформы .NET 4.O. Вы найдете информацию по таким новым компонентам, как Dynamic Language Runtime (DLR), Task Parallel Library (TPL), Parallel LINQ (PLINQ) и ADO.NET Entity Framework (EF). Кроме того, в книге описан ряд менее значительных (но очень полезных) обновлений, наподобие именованных и необязательных аргументов в С# 2010, типа класса Lazy<T> и т.д. Помимо описания новых компонентов и возможностей, в книге по-прежнему предоставляется весь необходимый базовый материал по языку С# в целом, основам объектно-ориентированного программирования (ООП), конфигурированию сборок, получению доступа к базам данных (через ADO.NET), а также процессу построения настольных приложений с графическим пользовательским интерфейсом, веб-приложений и распределенных систем (и многим другим темам). Как и в предыдущих изданиях, в этом издании весь материал по языку программирования С# и библиотекам базовых классов .NET подается в дружественной и понятной читателю манере. Автор никогда не понимал, зачем другие технические авторы стараются писать свои книги так, чтобы те больше напоминали сложный научный труд, а не легкое для восприятия пособие. В новом издании основное внимание уделяется предоставлению информации, которой необходимо владеть для того, чтобы разрабатывать программные решения прямо сегодня, а не глубокому изучению малоинтересных эзотерических деталей.
Введение 33 Автор и читатели - одна команда Авторам книг по технологиям приходится писать для очень требовательной группы людей. Всем известно, что детали разработки программных решений с помощью любой платформы (.NET, Java и СОМ) очень сложны и сильно зависят от отдела, компании, клиентской базы и поставленной задачи. Кто-то работает в сфере электронных публикаций, кто-то занимается разработкой систем для правительства и региональных органов власти, а кто-то сотрудничает с НАСА или военными департаментами. Что касается автора настоящей книги, то сам он занимается разработкой детского образовательного ПО (возможно, вам приходилось слышать об Oregon Trail или Amazon Trail), а также различных многоуровневых систем и проектов в медицинской и финансовой сфере. А это значит, что код, который придется писать читателю, скорее всего, будет иметь мало чего общего с тем, с которым приходится иметь дело автору. Поэтому в настоящей книге автор специально старался избегать приведения примеров, свойственных только какой-то конкретной области производства или программирования. Из-за этого все концепции, связанные с С#, ООП, CLR и библиотеками базовых классов .NET, объясняются на общих примерах. В частности, здесь везде применяется одна и та же тема, которая так или иначе близка каждому — автомобили. Остальное остается за читателем. Задача автора состоит в том, чтобы максимально доступно объяснить читателю язык программирования С# и основные концепции платформы .NET настолько хорошо, а также описать все возможные инструменты и стратегии, которые могут потребоваться для продолжения обучения по прочтении данной книги. Задача читателя состоит в том, чтобы усвоить всю эту информацию и научиться применять ее на практике при разработке своих программных решений. Конечно, скорее всего, проекты, которые понадобится выполнять в будущем, не будут связаны с автомобилями и их дружественными именами, но именно в этом и состоит вся суть применения получаемых знаний на практике! После изучения представленных в этой книге концепций можно будет спокойно создавать решения .NET, удовлетворяющие требованиям любой среды программирования. Краткий обзор содержания Эта книга логически разделена на восемь частей, в каждой из которых содержится ряд взаимосвязанных между собой глав. Те, кто читал предыдущие издания данной книги, сразу же отметят ряд отличий. Например, новые средства языка С# больше не описываются в отдельной главе. Вместо этого они рассматриваются в тех главах, в которых их появление является вполне естественным. Кроме того, по просьбе читателей был значительно расширен материал по технологии Windows Presentation Foundation (WPF). Ниже приведено краткое описание содержимого каждой из частей и глав настоящей книги. Часть I. Общие сведения о языке С# и платформе .NET Назначением первой части этой книги является общее ознакомление читателя с природой платформы .NET и различными средствами разработки, которые могут применяться при построении приложений .NET (многие из которых распространяются с открытым исходным кодом), а также некоторыми основными концепциями языка программирования С# и системы типов .NET.
34 Введение Глава 1. Философия .NET Первая глава выступает в роли своего рода основы для изучения всего остального излагаемого в данной книге материала. В начале в ней рассказывается о традиционной разработке приложений Windows и недостатках, которые существовали в этой сфере ранее. Главной целью данной главы является ознакомление читателя с набором ключевых составляющих .NET: общеязыковой исполняющей средой (Common Language Runtime — CLR), общей системой типов (Common Type System — CTS), общеязыковой спецификацией (Common Language Specification — CLRS) и библиотеками базовых классов. Здесь читатель сможет получить первоначальное впечатление о том, что собой представляет язык программирования С#, и том, как выглядит формат сборок .NET, а также узнать о независимой от платформы природе .NET (о которой более' детально рассказывается в приложении Б). Глава 2. Создание приложений на языке С# Целью этой главы является ознакомление читателя с процессом компиляции файлов исходного кода на С# с применением различных средств и методик. Будет показано, как использовать компилятор командной строки С# (csc.exe) и файлы ответов, а также различные редакторы кода и интегрированные среды разработки, в том числе Notepad++, SharpDevelop, Visual C# 2010 Express и Visual Studio 2010. Кроме того, вы узнаете о том, как устанавливать на машину разработки локальную копию документации .NET Framework 4.0 SDK. Часть II. Главные конструкции программирования на С# Темы, представленные в этой части книги, довольно важны, поскольку подходят для разработки приложений .NET любого типа (т.е. веб-приложений, настольных приложений с графическим пользовательским интерфейсом, библиотек кода и служб Windows). Здесь читатель ознакомится с основными конструкциями языка С# и некоторыми деталями объектно-ориентированного программирования (ООП), обработкой исключений на этапе выполнения, а также автоматическим процессом сборки мусора в .NET. Глава 3. Главные конструкции программирования на С#; часть I В этой главе начинается формальное изучение языка программирования С#. Здесь читатель узнает о роли метода Main () и многочисленных деталях работы с внутренними типами данных в .NET, в том числе — о манипуляциях текстовыми данными с помощью System. String и System. Text. StringBuilder. Кроме того, будут описаны итерационные конструкции и конструкции принятия решений, операции сужения и расширения, а также ключевое слово unchecked. Глава 4. Главные конструкции программирования на С#; часть II В этой главе завершается рассмотрение ключевых аспектов С#. Будет показано, как создавать перегруженные методы в типах и определять параметры с использованием ключевых слов out, ref и par am s. Также рассматриваются появившиеся в С# 2010 концепции именованных аргументов и необязательных параметры. Кроме того, будут описаны создание и манипулирование массивами данных, определение нулевых типов (с помощью операций ? и ??) и отличия типов значения (включающих перечисления и специальные структуры) от ссылочных типов. Глава 5. Определение инкапсулированных типов классов В этой главе начинается рассмотрение концепций объектно-ориентированного программирования (ООП) в языке С#. Вначале объясняются базовые понятия ООП (та-
Введение 35 кие как инкапсуляция, наследование и полиморфизм). Затем показано, как создавать надежные типы классов с применением конструкторов, свойств, статических членов, констант и доступных только для чтения полей. Наконец, рассматриваются частичные определения типов, синтаксис инициализации объектов и автоматические свойства. Глава 6. Понятия наследования и полиморфизма Здесь читатель сможет ознакомиться с двумя такими основополагающими концепциями ООП, как наследование и полиморфизм, которые позволяют создавать семейства взаимосвязанных типов классов. Будет описана роль виртуальных и абстрактных методов (а также абстрактных базовых классов) и полиморфных интерфейсов. И, наконец, в главе рассматривается роль одного из главных базовых классов в .NET — System.Object. Глава 7. Структурированная обработка исключений В этой главе рассматривается решение проблемы аномалий, возникающих в коде во время выполнения, за счет применения методики структурированной обработки исключений. Здесь описаны ключевые слова, предусмотренные для этого в С# (try, catch, throw и finally), а также отличия между исключениями уровня приложения и уровня системы. Кроме того, рассматриваются различные инструменты, предлагаемые в Visual Studio 2010, которые предназначены для проведения отладки исключений. Глава 8. Время жизни объектов В этой главе рассказывается об управлении памятью CLR-средой с использованием сборщика мусора .NET. Будет описана роль корневых элементов приложений, поколений объектов и типа System.GC. Будет показано, как создавать самоочищаемые объекты (с применением интерфейса IDisposable) и обеспечивать процесс финализации (с помощью метода System. Object. Finalize ()). Вы узнаете о появившемся в .NET 4.0 классе Lazy<T>, который позволяет определять данные так, чтобы они не размещались в памяти до тех пор, пока вызывающая сторона не запросит их. Этот класс позволяет не загромождать память такими объектами, которые пока не требуются. Часть III. Дополнительные конструкции программирования на С# В этой части читателю предоставляется возможность углубить знания языка С# за счет изучения других более сложных (но очень важных) концепций. Здесь завершается ознакомление с системой типов .NET описанием типов делегатов и интерфейсов. Кроме того, описана роль обобщений и дано краткое введение в язык LINQ (Language Integrated Query). Также рассматриваются некоторые более сложные средства С# (такие как методы расширения, частичные методы и приемы манипулирования указателями). Глава 9. Работа с интерфейсами Материал этой главы предполагает наличие понимания концепций объектно-ориентированной разработки и посвящен программированию с использованием интерфейсов. Здесь будет показано, как определять классы и структуры, поддерживающие множество поведений, как обнаруживать эти поведения во время выполнения и как выборочно скрывать какие-то из них за счет явной реализации интерфейсов. Помимо создания специальных интерфейсов, рассматриваются вопросы реализации стандартных интерфейсов из состава .NET и их применения для построения объектов, которые могут сортироваться, копироваться, перечисляться и сравниваться.
36 Введение Глава 10. Обобщения Эта глава посвящена обобщениям. Программирование с использованием обобщений позволяет создавать типы и члены типов, содержащие заполнители, которые заполняются вызывающим кодом. В целом, обобщения позволяют значительно улучшить производительность приложений и безопасность в отношении типов. В главе рассматриваются типы обобщений из пространства имен System.Collections .Generic, а также показано, как создавать собственные обобщенные методы и типы (с ограничениями и без). Глава 11. Делегаты, события и лямбда-выражения Благодаря этой главе, станет понятно, что собой представляет тип делегата. Любой делегат в .NET представляет собой объект, который указывает на другие методы в приложении. С помощью делегатов можно создавать системы, позволяющие многочисленным объектам взаимодействовать между собой в обоих направлениях. После изучения способов применения делегатов в .NET, будет показано, как применять в С# ключевое слово event, которое упрощает манипулирование делегатами. Кроме того, рассматривается роль лямбда-операции (=>) и связь между делегатами, анонимными методами и лямбда-выражениями. Глава 12. Расширенные средства языка С# В этой главе описаны расширенные средства языка С#, в том числе перегрузка операций, создание специальных процедур преобразования (явного и неявного) для типов, построение и взаимодействие с индексаторами типов, работа с расширяющими методами, анонимными типами, частичными методами, а также указателями С# с использованием в коде контекста unsafe. Глава 13. LINQ to Object В этой главе начинается рассмотрение LINQ (Language Integrated Query — язык интегрированных запросов). Эта технология позволяет создавать строго типизированные выражения запросов, применять их к ряду различных целевых объектов LINQ и тем самым манипулировать данными в самом широком смысле этого слова. Глава посвящена API-интерфейсу LINQ to Objects, который позволяет применять LINQ-выражения к контейнерам данных (т.е. массивам, коллекциям и специальным типам). Эта информация будет полезна позже при рассмотрении других дополнительных API-интерфейсов, таких как LINQ to XML, LINQ to DataSet, PLINQ и LINQ to Entities. Часть IV. Программирование с использованием сборок .NET Эта часть книги посвящена деталям формата сборок .NET. Здесь вы узнаете не только о способах развертывания и конфигурирования библиотек кода .NET, но также о внутреннем устройстве двоичного образа .NET. Будет описана роль атрибутов .NET и определения информации о типе во время выполнения. Кроме того, рассматривается роль среды DLR (Dynamic Language Runtime — исполняющая среда динамического языка) в .NET 4.0 и ключевого слова dynamic в С# 2010. Наконец, объясняется, что собой представляют контексты объектов, как устроен CIL-код и как создавать сборки в памяти. Глава 14. Конфигурирование сборок .NET На самом высоком уровне термин сборка применяется для описания любого двоичного файла * . dll или * . ехе, который создается с помощью компилятора .NET. В действительности возможности сборок намного шире. В этой главе будет показано, чем отличаются однофайловые и многофайловые сборки, как создавать и развертывать сборки обеих разновидностей, как делать сборки приватными и разделяемыми с использовани-
Введение 37 ем XML-файлов *.configH специальных сборок политик издателя. Кроме того, в главе описан глобальный кэш сборок (Global Assembly Cache — CAG), а также изменения CAG в версии .NET 4.0. Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов В главе 15 продолжается изучение сборок .NET. В ней показано, как обнаруживать типы во время выполнения с использованием пространства имен System.Reflection. Типы из этого пространства имен позволяют создавать приложения, способные считывать метаданные сборки на лету. Кроме того, в главе рассматривается динамическая загрузка и создание типов во время выполнения с помощью позднего связывания, а также роль атрибутов .NET (стандартных и специальных). Для закрепления материала в конце главы приводится пример построения расширяемого приложения Windows Forms. Глава 16. Процессы, домены приложений и контексты объектов В этой главе рассказывается о создании загруженных исполняемых файлов .NET. Целью главы является иллюстрация отношений между процессами, доменами приложений и контекстными границами. Все эти темы подготавливают базу для изучения процесса создания многопоточных приложений в главе 19. Глава 17. Язык CIL и роль динамических сборок В этой главе подробно рассматривается синтаксис и семантика языка CIL, а также роль пространства имен System. Re flection. Em it, с помощью типов из которого можно создавать программное обеспечение, способное генерировать сборки .NET в памяти во время выполнения. Формально сборки, которые определяются и выполняются в памяти, называются динамическими сборками. Их не следует путать с динамическими типами, которые являются темой главы 18. Глава 18. Динамические типы и исполняющая среда динамического языка В .NET 4.0 появился новый компонент исполняющей среды .NET, который называется исполняющей средой динамического языка (Dynamic Language Runtime — DLR). С помощью DLR в .NET и ключевого слова dynamic в С# 2010 можно определять данные, которые в действительности разрешаются во время выполнения. Использование этих средств значительно упрощает решение ряда очень сложных задач по программированию приложений .NET В главе рассматриваются некоторые практические способы применения динамических данных, включая более гладкое использование API- интерфейсы рефлексии .NET, а также упрощенное взаимодействие с унаследованными библиотеками СОМ. Часть V. Введение в библиотеки базовых классов .NET В этой части рассматривается ряд наиболее часто применяемых служб, поставляемых в составе библиотек базовых классов .NET, включая создание многопоточных приложений, файловый ввод-вывод и доступ к базам данных с помощью ADO.NET. Здесь также показано, как создавать распределенные приложения с помощью Windows Communication Foundation (WCF) и приложения с рабочими потоками, которые используют API-интерфейсы Windows Workflow Foundation (WF) и LINQ to XML. Глава 19. Многопоточность и параллельное программирование Эта глава посвящена созданию многопоточных приложений. В ней демонстрируется ряд приемов, которые можно применять для написания кода, безопасного в отноше-
38 Введение нии потоков. В начале главы кратко напоминается о том, что собой представляет тип делегата в .NET для упрощения понимания предусмотренной в нем внутренней поддержки для асинхронного вызова методов. Затем рассматриваются типы пространства имен System. Threading и новый API-интерфейс TPL (Task Parallel Library — библиотека параллельных задач), появившийся в .NET 4.O. С применением этого API-интерфейса можно создавать .NET-приложения, распределяющие рабочую нагрузку среди доступных ЦП в исключительно простой манере. В главе также описан API-интерфейс PINQ (Parallel LINQ), который позволяет создавать масштабируемые LINQ-запросы. Глава 20. Файловый ввод-вывод и сериализация объектов Пространство имен System. 10 позволяет взаимодействовать существующей структурой файлов и каталогов. В главе будет показано, как программно создавать (и удалять) систему каталогов и перемещать данные в различные потоки (файловые, строковые и находящиеся в памяти). Кроме того, рассматриваются службы .NET, предназначенные для сериализации объектов. Сериализация представляет собой процесс, который позволяет сохранять данные о состоянии объекта (или набора взаимосвязанных объектов) в потоке для использования в более позднее время, а десериализация — процесс извлечения данных о состоянии объекта из потока в память для последующего использования в приложении. В главе описана настройка процесса сериализации с применением интерфейса ISerializable и набора атрибутов .NET. Глава 21. AD0.NET, часть I: подключенный уровень В этой первой из трех посвященных базам данных главам дано введение в API- интерфейс ADO.NET. Рассматривается роль поставщиков данных .NET и взаимодействие с реляционной базой данных с применением так называемого подключенного уровня ADO.NET, который представлен объектами подключения, объектами команд, объектами транзакций и объектами чтения данных. В этой главе также приведен пример создания специальной базы данных и первой версии специальной библиотеки доступа к данным (AutoLotDAL. dll), неоднократно применяемой в остальных примерах книги. Глава 23. AD0.NET, часть II: автономный уровень В этой главе продолжается описание способов работы с базами данных и рассказывается об автономном ypoeHeADO.NET. Рассматривается роль типа DataSet и объектов адаптеров данных, а также многочисленных средств Visual Studio 2010, которые способны упростить процесс создания приложений, управляемых данными. Будет показано, как связывать объекты DataTable с элементами пользовательского интерфейса, а также как применять запросы LINQ к находящимся в памяти объектам DataSet с использованием API-интерфейса LINQ to DataSet. Глава 23. AD0.NET, часть III: Entity Framework В этой главе завершается изучение ADO.NET и рассматривается роль технологии Entity Framework (EF), которая позволяет создавать код доступа к данным с использованием строго типизированных классов, напрямую отображающихся на бизнес-модель. Здесь будут описаны роли таких входящих в состав EF компонентов, как службы объектов EF, клиент сущностей и контекст объектов. Будет показано устройство файла * . edmx, а также взаимодействие с реляционными базами данных с применением API- интерфейса LINQ to Entities. Кроме того, в главе создается последняя версия специальной библиотеки доступа к данным (AutoLotDAL.dll), которая будет использоваться в нескольких последних главах книги.
Введение 39 Глава 24. Введение в LINQ to XML В главе 14 были даны общие сведения о модели программирования LINQ и об API-интерфейсе LINQ to Objects. В этой главе читателю предлагается углубить свои знания о технологии LINQ и научиться применять запросы LINQ к XML-документам. Сначала будут рассмотрены сложности, которые существовали в .NET первоначально в области манипулирования XML-данными, на примере применения типов из сборки System.Xml. dll. Затем будет показано, как создавать XML-документы в памяти, обеспечивать их сохранение на жестком диске и перемещаться по их содержимому с использованием модели программирования LINQ (LINQ to XML). Глава 25. Введение в Windows Communication Foundation В этой главе читатель узнает об API-интерфейсе Windows Communication Foundation (WCF), который позволяет создавать распределенные приложения симметричным образом, какими бы не были лежащие в их основе низкоуровневые детали. Будет показано, как создавать службы, хосты и клиентские приложения WCF. Службы WCF являются чрезвычайно гибкими, поскольку позволяют использовать для клиентов и хостов конфигурационные файлы на основе XML, в которых декларативно задаются необходимые адреса, привязки и контракты. Кроме того, рассматриваются полезные сокращения, которые появились в .NET 4.O. Глава 26. Введение в Windows Workflow Foundation 4.0 API-интерфейс Windows Workflow Foundation (WF) вызывает больше всего путаницы у разработчиков-новичков. В версии .NET 4.0 первоначальный вариант API-интерфейса WF (появившийся в .NET 3.0) полностью переделан. В этой главе описана роль приложений, поддерживающих рабочие потоки, и способы моделирования бизнес-процессов с применением API-интерфейса WF 4.O. Рассматривается библиотека действий, поставляемая в составе WF 4.0, а также показано, как создавать специальные действия. Часть VI. Построение настольных пользовательских приложений с помощью WPF В .NET 3.0 был предложен замечательный API-интерфейс под названием Windows Presentation Foundation (WPF). Он быстро стал заменой модели программирования настольных приложений Windows Forms. WPF позволяет создавать настольные приложения с векторной графикой, интерактивной анимацией и операциями привязки данных с использованием декларативной грамматики разметки XAML. Более того, архитектура элементов управления WPF позволяет легко изменять внешний вид и поведение любого элемента управления с помощью правильно оформленного XAML-кода. В настоящем издании модели программирования WPF посвящено целых пять глав. Глава 27. Введение в Windows Presentation Foundation и XAML Технология WPF позволяет создавать чрезвычайно интерактивные и многофункциональные интерфейсы для настольных приложений (и косвенно для веб-приложений). В отличие от Windows Forms, в WPF множество ключевых служб (наподобие двухмерной и трехмерной графики, анимации, форматированных документов и т.п.) интегрируется в одну универсальную объектную модель. В главе предлагается введение в WPF и язык XAML (Extendable Application Markup Language — расширяемый язык разметки приложений). Будет показано, как создавать WPF-приложения без использования только кода без XAML, с использованием одного лишь XAML и с применением обоих подходов вместе, а также приведен пример создания специального XAML-редактора, который пригодиться при изучении остальных глав, посвященных WPF
40 Введение Глава 28. Программирование с использованием элементов управления WPF В этой главе читатель научится работать с предлагаемыми в WPF элементами управления и диспетчерами компоновки. Будет показано, как создавать системы меню, разделители окон, панели инструментов и строки состояния. Также в главе рассматриваются API-интерфейсы (и связанные с ними элементы управления), входящие в состав WPF — Documents API, Ink API и модель привязки данных. В главе приводится начальное описание IDE-среды Expression Blend, которая значительно упрощает процесс создания многофункциональных пользовательских интерфейсов для приложений WPF. Глава 29. Службы визуализации графики WPF В API-интерфейсе WPF интенсивно используется графика, в связи с чем WPF предоставляет три пути визуализации графических данных — фигуры, рисунки и визуальные объекты. В главе подробно описаны все эти пути. Кроме того, рассматривается набор важных графических примитивов (таких как кисти, перья и трансформации), а применение Expression Blend для создания графики. Также показано, как выполнять операции проверки попадания (hit-testing) в отношении графических данных. Глава 30. Ресурсы, анимация и стили WPF В этой главе освещены три важных (и связанных между собой) темы, которые позволят углубить знания API-интерфейса Windows Presentation Foundation. В первую очередь рассказывается о роли логических ресурсов. Система логических (также называемых объектными) ресурсов позволяет назначать наиболее часто используемым в WPF- приложении объектам имена и затем ссылаться на них. Кроме того, будет показано, как определять, выполнять и управлять анимационной последовательностью. И, наконец, в главе рассматривается роль стилей в WPF Подобно тому, как для веб-страниц могут применяться таблицы стилей CSS и механизм тем ASP.NET, в приложениях WPF для набора элементов управления может быть определен общий вид и поведение. Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления В этой главе завершается изучение модели программирования WPF и демонстрируется процесс создания специализированных элементов управления. Сначала рассматриваются две важных темы, связанные с созданием любого специального элемента — свойства зависимости и маршрутизируемые событиях. Затем описывается роль шаблонов по умолчанию и способы их просмотра в коде во время выполнения. Наконец, рассказывается о том, как создавать специальные классы UserControl с помощью Visual Studio 2010 и Expression Blend, в том числе и с применением .NET 4.0 Visual State Manager (VSM). Часть VII. Построение веб-приложений с использованием ASP.NET Эта часть посвящена деталям построения веб-приложений с применением API- интерфейса ASP.NET. Данный интерфейс был разработан Microsoft специально для предоставления возможности моделировать процесс создания настольных пользовательских интерфейсов путем наложения стандартного объектно-ориентированной, управляемой событиями платформы поверх стандартных запросов и ответов HTTP. Глава 32. Построение веб-страниц ASP.NET В этой главе начинается изучение процесса разработки веб-приложений с помощью ASP.NET. Как будет показано, вместо кода серверных сценариев теперь применяются самые настоящие объектно-ориентированные языки (наподобие С# и VB.NET). В главе
Введение 41 рассматривается типовой процесс создания веб-страницы ASP.NET, лежащая в основе модель программирования и другие важные аспекты ASP.NET, вроде того, как выбира- еть веб-сервер и работать с файлами Web. с on fig. Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET В отличие от предыдущей главы, посвященной созданию объектов Page из ASP NET, в данной главе рассказывается об элементах управления, которые заполняют внутреннее дерево элементов ASP.NET. Здесь описаны основные веб-элементы управления ASP.NET, включая элементы управления проверкой достоверности, элементы управления навигацией по сайту и различные операции привязки данных. Кроме того, рассматривается роль мастер-страниц и механизма применения тем ASP.NET, который является серверным аналогом традиционных таблиц стилей. Глава 34. Управление состоянием в ASP.NET В этой главе рассматриваются разнообразные способы управления состоянием в .NET. Как и в классическом ASP, в ASP.NET можно создавать cookie-наборы и переменные уровня приложения и уровня сеанса. Кроме того, в ASP.NET есть еще одно средство для управления состоянием, которое называется кэшем приложения. Вдобавок в главе рассказывается о роли базового класса HttpApplication и демонстрируется динамическое переключение поведения веб-приложения с помощью файла Web. с on fig. Часть VIII. Приложения В этой заключительной части книги рассматриваются две темы, которые не совсем вписывались в контекст основного материала. В частности, здесь кратко рассматривается более ранняя платформа Windows Forms для построения графических интерфейсов настольных приложений, а также использование платформы Mono для создания приложений .NET, функционирующих под управлением операционных систем, отличных от Microsoft Windows. Приложение А. Программирование с помощью Windows Forms Исходный набор инструментов для построения настольных пользовательских интерфейсов, который поставляется в рамках платформы .NET с самого начала, называется Windows Forms. В этом приложении описана роль этого каркаса и показано, как с его помощью создавать главные окна, диалоговые окна и системы меню. Кроме того, здесь рассматриваются вопросы наследования форм и визуализации двухмерной графики с помощью пространства имен System. Drawing. В конце приложения приводится пример создания программы для рисования (средней сложности), иллюстрирующий практическое применение всех описанных концепций. Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono Приложение Б посвящено использованию распространяемой с открытым исходным кодом реализации платформы .NET под названием Mono. Она позволяет разрабатывать многофункциональные приложения .NET, которые можно создавать, развертывать и выполнять под управлением самых разных операционных систем, включая Mac OS X, Solaris, AIX и многочисленные дистрибутивы Linux. Так как Mono в основном эмулирует платформу .NET от Microsoft, ее функциональные возможности вполне очевидны. Поэтому в приложении основное внимание уделено не возможностям платформы Mono, а процессу ее установки, предлагаемым в ее составе инструментам для разработки, а также используемому механизму исполняющей среды.
42 Введение Исходный код примеров Исходный код всех рассматриваемых в настоящей книге примеров доступен для загрузки на сайте издательства по адресу: http://www.williamspublishing.com От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: info@williamspublishing.com WWW: http://www.williamspublishing.com Информация для писем из: России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Украины: 03150, Киев, а/я 152
ЧАСТЬ I Общие сведения о языке С# и платформе .NET В этой части... Глава 1. Философия .NET ( Глава 2. Создание приложений на языке С#
ГЛАВА 1 Философия .NET Каждые несколько лет современному программисту требуется серьезно обновлять свои знания, чтобы оставаться в курсе всех последних новейших технологий. Языки (C++, Visual Basic 6.0, Java), библиотеки приложений (MFC, ATL, STL), архитектуры (COM, CORBA, EJB) и API-интерфейсы, которые провозглашались универсальными средствами для разработки программного обеспечения, постепенно начинают затмевать более совершенные или, в самом крайнем случае, более новые технологии. Какое бы чувство расстройства не возникало в связи с необходимостью обновления своей внутренней базы знаний, избежать его, честно говоря, не возможно. Поэтому целью данной книги является рассмотрение деталей тех решений, которые предлагаются Microsoft в сфере разработки программного обеспечения сегодня, а именно — деталей платформы .NET и языка программирования С#. Задача настоящей главы заключается в изложении концептуальных базовых сведений для обеспечения возможности успешного освоения всего остального материала книги. Здесь рассматриваются такие связанные с .NET концепции, как сборки, общий промежуточный язык (Common Intermediate Language — CIL) и оперативная компиляция (Just-In-Time Compilation — JIT). Помимо предварительного ознакомления с некоторыми ключевыми словами, также разъясняются взаимоотношения между различными компонентами платформы .NET, такими как общеязыковая исполняющая среда (Common Language Runtime — CLR), общая система типов (Common Type System — CTS) и общеязыковая спецификация (Common Language Specification — CLS). Кроме того, в настоящей главе описан набор функциональных возможностей, поставляемых в библиотеках базовых классов .NET 4.0, для обозначения которых иногда используют аббревиатуру BCL (Base Class Libraries — библиотеки базовых классов) или FCL (Framework Class Libraries — библиотеки классов платформы). И, наконец, напоследок в главе кратко затрагивается тема независимой от языков и платформ сущности .NET (да, это действительно так — .NET не ограничивается только операционной системой Windows). Как не трудно догадаться, многие из этих тем будут более подробно освещены в остальной части книги. Предыдущее состояние дел Прежде чем переходить к изучению специфических деталей мира .NET, не помешает узнать о некоторых вещах, которые послужили стимулом для создания Microsoft текущей платформы. Чтобы настроиться надлежащим образом, давайте начнем эту главу с краткого урока истории, чтобы вспомнить об истоках и ограничениях, которые существовали в прошлом (ведь признание существования проблемы является первым шагом на пути к ее решению). После завершения этого краткого экскурса в историю мы обратим наше внимание на те многочисленные преимущества, которые предоставляет язык С# и платформа .NET.
Глава 1. Философия .NET 45 Подход с применением языка С и API-интерфейса Windows Традиционно разработка программного обеспечения для операционных систем семейства Windows подразумевала использование языка программирования С в сочетании с API-интерфейсом Windows (Application Programming Interface — интерфейс прикладного программирования). И хотя то, что за счет применения этого проверенного временем подхода было успешно создано очень много приложений, мало кто станет спорить по поводу того, что процесс создания приложений с помощью одного только API-интерфейса является очень сложным занятием. Первая очевидная проблема состоит в том, что С представляет собой очень лаконичный язык. Разработчики программ на языке С вынуждены мириться с необходимостью "вручную" управлять памятью, безобразной арифметикой указателей и ужасными синтаксическими конструкциями. Более того, поскольку С является структурным языком программирования, ему не хватает преимуществ, обеспечиваемых объектно-ориентированным подходом (здесь можно вспомнить о спагетти-подобном коде). Из-за сочетания тысяч глобальных функций и типов данных, определенных в API-интерфейса Windows, с языком, который и без того выглядит устрашающе, совсем не удивительно, что сегодня в обиходе присутствует столь много дефектных приложений. Подход с применением языка C++ и платформы MFC Огромным шагом вперед по сравнению с подходом, предполагающим применение языка С прямо с API-интерфейсом, стал переход на использование языка программирования C++. Язык C++ во многих отношениях может считаться объектно-ориентированной надстройкой поверх языка С. Из-за этого, хотя в случае его применения программисты уже могут начинать пользоваться преимуществами известных "главных столпов ООП" (таких как инкапсуляция, наследование и полиморфизм), они все равно вынуждены иметь дело с утомительными деталями языка С (вроде необходимости осуществлять управление памятью "вручную", безобразной арифметики указателей и ужасных синтаксических конструкций). Невзирая на сложность, сегодня существует множество платформ для программирования на C++. Например, MFC (Microsoft Foundation Classes — библиотека базовых классов Microsoft) предоставляет в распоряжение разработчику набор классов C++, которые упрощают процесс создания приложений Windows. Основное предназначение MFC заключается в представлении "разумного подмножества" исходного API-интерфейса Windows в виде набора классов, "магических" макросов и многочисленных средств для автоматической генерации программного кода (обычно называемых мастерами). Несмотря на очевидную пользу данной платформы приложений (и многих других основанных на C++ наборов средств), процесс программирования на C++ остается трудным и чреватым допущением ошибок занятием из-за его исторической связи с языком С. Подход с применением Visual Basic 6.0 Благодаря искреннему желанию иметь возможность наслаждаться более простой жизнью, многие программисты перешли из "мира платформ" на базе С (C++) в мир менее сложных и более дружественных языков наподобие Visual Basic 6.0 (VB6). Язык VB6 стал популярным благодаря предоставляемой им возможности создавать сложные пользовательские интерфейсы, библиотеки программного кода (вроде СОМ-серверов) и логику доступа к базам данных с приложением минимального количества усилий. Во многом как и в MFC, в VB6 сложности API-интерфейса Windows скрываются из вида за счет предоставления ряда интегрированных мастеров, внутренних типов данных, классов и специфических функций VB.
46 Часть I. Общие сведения о языке С# и платформе .NET Главный недостаток языка VB6 (который с появлением платформы .NET был устранен) состоит в том, что он является не полностью объектно-ориентированным, а скорее — просто "объектным". Например, VB6 не позволяет программисту устанавливать между классами отношения "подчиненности" (т.е. прибегать к классическому наследованию) и не обладает никакой внутренней поддержкой для создания параметризованных классов. Более того, VB6 не предоставляет возможности для построения многопоточных приложений, если только программист не готов опускаться до уровня вызовов API-интерфейса Windows (что в лучшем случае является сложным, а в худшем — опасным подходом). На заметку! Язык Visual Basic, используемый внутри платформы .NET (и часто называемый языком VB.NET), имеет мало чего общего с языком VB6. Например, в современном языке VB поддерживается перегрузка операций, классическое наследование, конструкторы типов и обобщения. Подход с применением Java Теперь пришел черед языка Java. Язык Java представляет собой объектно-ориентированный язык программирования, который своими синтаксическими корнями уходит в C++. Как многим известно, достоинства Java не ограничиваются одной лишь только поддержкой независимости от платформ. Java как язык не имеет многих из тех неприятных синтаксических аспектов, которые присутствуют в C++, а как платформа — предоставляет в распоряжение программистам большее количество готовых пакетов с различными определениями типов внутри. За счет применения этих типов программисты на Java могут создавать "на 100% чистые Java-приложения" с возможностью подключения к базе данных, поддержкой обмена сообщениями, веб-интерфейсами и богатым настольными интерфейсами для пользователей (а также многими другими службами). Хотя Java и представляет собой очень элегантный язык, одной из потенциальных проблем является то, что применение Java обычно означает необходимость использования Java в цикле разработки и для взаимодействия клиента с сервером. Надежды на появление возможности интегрировать Java с другими языками мало, поскольку это противоречит главной цели Java — быть единственным языком программирования для удовлетворения любой потребности. В действительности, однако, в мире существуют миллионы строк программного кода, которым бы идеально подошло смешивание с более новым программным кодом на Java. К сожалению, Java делает выполнение этой задачи проблематичной. Пока в Java предлагаются лишь ограниченные возможности для получения доступа к отличным от Java API-интерфейсам, поддержка для истинной межплатформенной интеграции остается незначительной. Подход с применением СОМ Модель COM (Component Object Model — модель компонентных объектов) была предшествующей платформой для разработки приложений, которая предлагалась Microsoft, и впервые появилась в мире программирования приблизительно в 1991 г. (или в 1993 г., если считать моментом ее появления рождение версии OLE 1.0). Она представляет собой архитектуру, которая, по сути, гласит следующее: в случае построения типов в соответствии с правилами СОМ, будет получаться блок многократно используемого двоичного кода. Такие двоичные блоки кода СОМ часто называют "серверами СОМ". Одним из главным преимуществ двоичного СОМ-сервера является то, что к нему можно получать доступ независимым от языка образом. Это означает, что программисты на C++ могут создавать СОМ-классы, пригодные для использования в VB6, программисты на Delphi — применять СОМ-классы, созданные с помощью С, и т.д. Однако,
Глава 1. Философия .NET 47 как не трудно догадаться, подобная независимость СОМ от языка является несколько ограниченной. Например, никакого способа для порождения нового СОМ-класса с использованием уже существующего не имеется (поскольку СОМ не обладает поддержкой классического наследования). Вместо этого для использования типов СОМ-класса требуется задавать несколько неуклюжее отношение принадлежности (has-a). Еще одним преимуществом СОМ является прозрачность расположения. За счет применения конструкций вроде системного реестра, идентификаторов приложений (AppID), заглушек, прокси-объектов и исполняющей среды СОМ программисты могут избегать необходимости иметь дело с самими сокетам, RPC-вызовам и другими низкоуровневыми деталями при создании распределенного приложения. Например, рассмотрим следующий программный код СОМ-клиента наУВб: ' Данный тип MyCOMClass мог бы быть написан на любом ' поддерживающем СОМ языке и размещаться в любом месте ' в сети (в том числе и на локальной машине) . Dim obj as MyCOMClass Set obj = New MyCOMClass ' Местонахождение определяется с помощью AppID. obj.DoSomeWork Хотя COM и можно считать очень успешной объектной моделью, ее внутреннее устройство является чрезвычайно сложным (и требует затрачивания программистами многих месяцев на его изучение, особенно теми, которые программируют на C++). Для облегчения процесса разработки двоичных СОМ-объектов программисты могут использовать многочисленные платформы, поддерживающие СОМ. Например, в ATL (Active Template Library — библиотека активных шаблонов) для упрощения процесса создания СОМ- серверов предоставляется набор специальных классов, шаблонов и макросов на C++. Во многих других языках приличная часть инфраструктуры СОМ тоже скрывается из вида. Поддержки одного только языка, однако, для сокрытия всей сложности СОМ не хватает. Даже при выборе относительно простого поддерживающего СОМ языка вроде VB6, все равно требуется бороться с "хрупкими" записями о регистрации и многочисленными деталями развертывания (в совокупности несколько саркастично называемыми адом DLL). Сложность представления типов данных СОМ Хотя СОМ, несомненно, упрощает процесс создания программных приложений с помощью различных языков программирования, независимая от языка природа СОМ не является настолько простой, насколько возможно хотелось бы. Некоторая доля этой сложности является следствием того факта, что приложения, которые сплетаются вместе с помощью разнообразных языков, получаются совершенно не связанными с синтаксической точки зрения. Например, синтаксис JScript во многом похож на синтаксис С, а синтаксис VBScript представляет собой подмножество синтаксиса VB6. СОМ-серверы, которые создаются для выполнения в исполняющей среде СОМ+ (представляющей собой компонент операционной системы Windows, который предлагает общие службы для библиотек специального кода, такие как транзакции, жизненный цикл объектов, безопасность и т.д.), имеют совершенно не такой вид и поведение, как ориентированные на использование в веб-сети ASP-страницы, в которых они вызываются. В результате получается очень запутанная смесь технологий. Более того, что, пожалуй, даже еще важнее, каждый язык и/или технология обладает собственной системой типов (которая может быть совершенно не похожа на систему типов другого языка или технологии). Помимо того факта, что каждый API-интерфейс поставляется с собственной коллекцией готового кода, даже базовые типы данных могут не всегда интерпретироваться идентичным образом. Например, тип CComBSTR в ATL
48 Часть I. Общие сведения о языке С# и платформе .NET представляет собой не совсем то же самое, что тип String в VB6, и оба они не имеют совершенно ничего общего с типом char* в С. Из-за того, что каждый язык обладает собственной уникальной системой типов, СОМ-программистам обычно требуется соблюдать предельную осторожность при создании общедоступных методов в общедоступных классах СОМ. Например, при возникновении у разработчика на C++ необходимости в создании метода, способного возвращать массив целых чисел в приложении VB6, ему пришлось бы полностью погружаться в сложные вызовы API-интерфейса СОМ для построения структуры SAFE ARRAY, которое вполне могло бы потребовать написания десятков строк кода. В мире СОМ тип данных SAFEARRAY является единственным способом для создания массива, который могли бы распознавать все платформы СОМ. Если разработчик на C++ вернет просто собственный массив C++, у приложения VB6 не будет никакого представления о том, что с ним делать. Подобные сложности могут возникать и при построении методов, предусматривающих выполнение манипуляций над простыми строковыми данными, ссылками на другие объекты СОМ и даже обычными булевскими значениями. Мягко говоря, программирование с использованием СОМ является очень несимметричной дисциплиной. Решение .NET Немало информации для короткого урока истории. Главное понять, что жизнь программиста Windows-приложений раньше была трудной. Платформа .NET Framework являет собой достаточно радикальную "силовую" попытку сделать жизнь программистов легче. Как можно будет увидеть в остальной части настоящей книги, .NET Framework представляет собой программную платформу для создания приложений на базе семейства операционных систем Windows, а также многочисленных операционных систем производства не Microsoft, таких как Mac OS X и различные дистрибутивы Unix и Linux. Для начала не помешает привести краткий перечень некоторых базовых функциональных возможностей, которыми обладает .NET • Возможность обеспечения взаимодействия с существующим программным кодом. Эта возможность, несомненно, является очень хорошей вещью, поскольку позволяет комбинировать существующие двоичные единицы СОМ (т.е. обеспечивать их взаимодействие) с более новыми двоичными единицами .NET и наоборот. С выходом версии .NET 4.0 эта возможность стала выглядеть даже еще проще, благодаря добавлению ключевого слова dynamic (о котором будет более подробно рассказываться в главе 18). • Поддержка для многочисленных языков программирования. Приложения .NET можно создавать с помощью любого множества языков программирования (С#, Visual Basic, F#, S# и т.д.). • Общий исполняющий механизм, используемый всеми поддерживающими .NET языками. Одним из аспектов этого механизма является наличие хорошо определенного набора типов, которые способен понимать каждый поддерживающий .NET язык. • Полная и тотальная интеграция языков. В .NET поддерживается межъязыковое наследование, межъязыковая обработка исключений и межъязыковая отладка кода. • Обширная библиотека базовых классов. Эта библиотека позволяет избегать сложностей, связанных с выполнением прямых вызовов к API-интерфейсу, и предлагает согласованную объектную модель, которую могут использовать все поддерживающие .NET языки.
Глава 1. Философия .NET 49 • Отсутствие необходимости в предоставлении низкоуровневых деталей СОМ. В двоичной единице .NET нет места ни для интерфейсов IClassFactory, IUnknown и IDispatch, ни для кода IDL, ни для вариантных типов данных (подобных BSTR, SAFEARRAY и т.д.). • Упрощенная модель развертывания. В .NET нет никакой необходимости заботиться о регистрации двоичной единицы в системном реестре. Более того, в .NET позволяется делать так, чтобы многочисленные версии одной и той же сборки * , dll могли без проблем сосуществовать на одной и той же машине. Как не трудно догадаться по приведенному выше перечню, платформа .NET не имеет ничего общего с СОМ (за исключением разве что того факта, что обе этих платформы являются детищем Microsoft). На самом деле единственным способом, которым может обеспечиваться взаимодействие между типами .NET и СОМ, будет использование уровня функциональной совместимости. Главные компоненты платформы .NET (CLR, CTS и CLS) Теперь, когда о некоторых из предоставляемых .NET преимуществах уже известно, давайте вкратце ознакомимся с тремя ключевыми (и связанными между собой) сущностями, которые делают предоставление этих преимуществ возможным: CLR, CTS и CLS. С точки зрения программиста .NET представляет собой исполняющую среду и обширную библиотеку базовых классов. Уровень исполняющей среды называется общеязыковой исполняющей средой (Common Language Runtime) или, сокращенно, средой CLR. Главной задачей CLR является автоматическое обнаружение, загрузка и управление типами .NET (вместо программиста). Кроме того, среда CLR заботится о ряде низкоуровневых деталей, таких как управление памятью, обслуживание приложения, обработка потоков и выполнение различных проверок, связанных с безопасностью. Другим составляющим компонентом платформы .NET является общая система типов (Common Type System) или, сокращенно, система CTS. В спецификации CTS представлено полное описание всех возможных типов данных и программных конструкций, поддерживаемых исполняющей средой, того, как эти сущности могут взаимодействовать друг с другом, и того, как они могут представляться в формате метаданных .NET (которые более подробно рассматриваются далее в этой главе и полностью — в главе 15). Важно понимать, что любая из определенных в CTS функциональных возможностей может не поддерживаться в отдельно взятом языке, совместимом с .NET. Поэтому существует еще общеязыковая спецификация (Common Language Specification) или, сокращенно, спецификация CLS, в которой описано лишь то подмножество общих типов и программных конструкций, каковое способны воспринимать абсолютно все поддерживающие .NET языки программирования. Следовательно, в случае построения типов .NET только с функциональными возможностями, которые предусмотрены в CLS, можно оставаться полностью уверенным в том, что все совместимые с .NET языки смогут их использовать. И, наоборот, в случае применения такого типа данных или конструкции программирования, которой нет в CLS, рассчитывать на то, что каждый язык программирования .NET сможет взаимодействовать с подобной библиотекой кода .NET, нельзя. К счастью, как будет показано позже в этой главе, существует очень простой способ указывать компилятору С#, чтобы он проверял весь код на предмет совместимости с CLS.
50 Часть I. Общие сведения о языке С# и платформе .NET Роль библиотек базовых классов Помимо среды CLR и спецификаций CTS и CLS, в составе платформы .NET поставляется библиотека базовых классов, которая является доступной для всех языков программирования .NET. В этой библиотеке не только содержатся определения различных примитивов, таких как потоки, файловый ввод-вывод, системы графической визуализации и механизмы для взаимодействия с различными внешними устройствами, но также предоставляется поддержка для целого ряда служб, требуемых в большинстве реальных приложений. Например, в библиотеке базовых классов содержатся определения типов, которые способны упрощать процесс получения доступа к базам данных, манипулирования XML-документами, обеспечения программной безопасности и создания веб-, а также обычных настольных и консольных интерфейсов. На высоком уровне взаимосвязь между CLR, CTS, CLS и библиотекой базовых классов выглядит так, как показано на рис. 1.1. Доступ к базе данных Организация потоковой обработки Библиотека базовых классов Настольные графические API-интерфейсы Файловый ввод-вывод Безопасность API-интерфейсы для работы с веб-содержимым API-интерфейсы для удаленной работы и другие Общеязыковая исполняющая среда (CLR) Общая система типов (CTS) Общеязыковая спецификация (CLS) Рис. 1.1. Отношения между CLR, CTS, CLS и библиотеками базовых классов Что привносит язык С# Из-за того, что платформа .NET столь радикально отличается от предыдущих технологий, в Microsoft разработали специально под нее новый язык программирования С#. Синтаксис этого языка программирования очень похож на синтаксис языка Java. Однако сказать, что С# просто переписан с Java, будет неточно. И язык С#, и язык Java просто оба являются членами семейства языков программирования С (в которое также входят языки С, Objective С, C++ и т.д.) и потому имеют схожий синтаксис. Правда состоит в том, что многие синтаксические конструкции в С# моделируются согласно различным особенностям Visual Basic 6.0 и C++. Например, как и в VB6, в С# поддерживается понятие формальных свойств типов (в противоположность традиционным методам get и set) и возможность объявлять методы, принимающие переменное количество аргументов (через массивы параметров). Как и в C++, в С# допускается перегружать операции, а также создавать структуры, перечисления и функции обратного вызова (посредством делегатов).
Глава 1. Философия .NET 51 Более того, по мере изучения излагаемого в этой книге материала, можно будет быстро заметить, что в С# поддерживается целый ряд функциональных возможностей, которые традиционно встречаются в различных функциональных языках программирования (например, LISP или Haskell) и к числу которых относятся лямбда-выражения и анонимные типы. Кроме того, с появлением технологии LINQ в С# стали поддерживаться еще и конструкции, которые делают его довольно уникальным в мире программирования. Несмотря на все это, наибольшее влияние на него все-таки оказали именно языки на базе С. Благодаря тому факту, что С# представляет собой собранный из нескольких языков гибрид, он является таким же "чистым" с синтаксической точки зрения, как и язык Java (а то и "чище" его), почти столь же простым, как язык VB6, и практически таким же мощным и гибким как C++ (только без ассоциируемых с ним громоздких элементов). Ниже приведен неполный список ключевых функциональных возможностей языка С#, которые присутствуют во всех его версиях. • Никаких указателей использовать не требуется! В программах на С# обычно не возникает необходимости в манипулировании указателями напрямую (хотя опуститься к этому уровню все-таки можно, как будет показано в главе 12). • Управление памятью осуществляется автоматически посредством сборки мусора. По этой причине ключевое слово delete в С# не поддерживается. • Предлагаются формальные синтаксические конструкции для классов, интерфейсов, структур, перечислений и делегатов. • Предоставляется аналогичная C++ возможность перегружать операции для пользовательских типов, но без лишних сложностей (например, заботиться о "возврате *this для обеспечения связывания" не требуется). • Предлагается поддержка для программирования с использованием атрибутов. Такой подход в сфере разработки позволяет снабжать типы и их членов аннотациями и тем самым еще больше уточнять их поведение. С выходом версии .NET 2.0 (примерно в 2005 г.), язык программирования С# был обновлен и стал поддерживать многочисленные новые функциональные возможности, наиболее заслуживающие внимания из которых перечислены ниже. • Возможность создавать обобщенные типы и обобщенные элементы-члены. За счет применения обобщений можно создавать очень эффективный и безопасный для типов код с многочисленными метками-заполнителями, подстановка значений в которые будет происходить в момент непосредственного взаимодействия с данным обобщенным элементом. • Поддержка для анонимных методов, каковые позволяют предоставлять встраиваемую функцию везде, где требуется использовать тип делегата. • Многочисленные упрощения в модели "делегат-событие", в том числе возможность применения ковариантности, контравариантности и преобразования групп методов. (Если какие-то из этих терминов пока не знакомы, не стоит пугаться; все они подробно объясняются далее в книге.) • Возможность определять один тип в нескольких файлах кода (или, если необходимо, в виде представления в памяти) с помощью ключевого слова partial. В версии .NET 3.5 (которая вышла примерно в 2008 г.) в язык программирования С# снова были добавлены новые функциональные возможности, наиболее важные из которых описаны ниже. • Поддержка для строго типизированных запросов (также называемых запросами LINQ), которые применяются для взаимодействия с различными видами данных.
52 Часть I. Общие сведения о языке С# и платформе .NET • Поддержка для анонимных типов, которые позволяют моделировать форму типа, а не его поведение. • Возможность расширять функциональные возможности существующего типа с помощью методов расширения. • Возможность использовать лямбда-операцию (=>), которая даже еще больше упрощает работу с типами делегатов в .NET. • Новый синтаксис для инициализации объектов, который позволяет устанавливать значения свойств во время создания объектов. В текущем выпуске платформы .NET версии 4.0 язык С# был опять обновлен и дополнен рядом новых функциональных возможностей. Хотя приведенный ниже перечень новых конструкций может показаться довольно ограниченным, по ходу прочтения настоящей книги можно будет увидеть, насколько полезными они могут оказаться. • Поддержка необязательных параметров в методах, а также именованных аргументов. • Поддержка динамического поиска членов во время выполнения посредством ключевого слова dynamic. Как будет показано в главе 18, эта поддержка предоставляет в распоряжение универсальный подход для осуществления вызова членов "на лету", с помощью какой бы платформы они не были реализованы (COM, IronRuby, IronPython, HTML DOM или службы рефлексии .NET). , • Вместе с предыдущей возможностью в .NET 4.0 значительно упрощается обеспечение взаимодействия приложений на С# с унаследованными серверами СОМ, благодаря устранению зависимости от сборок взаимодействия (interop assemblies) и предоставлению поддержки необязательных аргументов ref. • Работа с обобщенными типами стала гораздо понятнее, благодаря появлению возможности легко отображать обобщенные данные на и из общих коллекций System.Object с помощью ковариантности и контравариантности. Возможно, наиболее важным моментом, о котором следует знать, программируя на С#, является то, что с помощью этого языка можно создавать только такой код, который будет выполняться в исполняющей среде .NET (использовать С# для построения "классического" СОМ-сервера или неуправляемого приложения с вызовами API-интерфейса и кодом на С и C++ нельзя). Официально код, ориентируемый на выполнение в исполняющей среде .NET, называется управляемым кодом (managed code), двоичная единица, в которой содержится такой управляемый код — сборкой (assembly; о сборках будет более подробно рассказываться позже в настоящей главе), а код, который не может обслуживаться непосредственно в исполняющей среде .NET — неуправляемым кодом (unma- naged code). Другие языки программирования с поддержкой .NET Следует понимать, что С# является не единственным языком, который может применяться для построения .NET-приложений. При установке доступного для бесплатной загрузки комплекта разработки программного обеспечения Microsoft .NET 4.0 Framework Software Development Kit (SDK), равно как и при установке Visual Studio 2010, для выбора становятся доступными пять управляемых языков: С#, Visual Basic, C++/CLI, JScript .NET и F#.
Глава 1. Философия .NET 53 На заметку! F# — это новый язык .NET, основанный на семействе функциональных языков ML и главным образом — на OCaml. Хотя он может применяться в качестве чисто функционального языка, в нем также предлагается поддержка для конструкций ООП и библиотек базовых классов .NET. Тем,.кому интересно узнать больше о нем, могут посетить его официальную веб-страницу по следующему адресу: http: //msdn. microsoft. com/f sharp. Помимо управляемых языков, предлагаемых Microsoft, существуют .NET-компиля- торы, которые предназначены для таких языков, как Smalltalk, COBOL и Pascal (и это далеко не полный перечень). Хотя в настоящей книге все внимание практически полностью уделяется лишь С#, следующий веб-сайт тоже вызвать интерес: http://www.dotnetlanguages.net Щелкнув на ссылке Resources (Ресурсы) в самом верху домашней страницы этого сайта, можно получить доступ к списку всех языков программирования .NET и соответствующих ссылок, по которым для них можно загружать различные компиляторы (рис. 1.2). C # - Page- Safety- al Р Д Mercury ItonPyUmn p* DEDICATED .NET Languages ™ Forth SML.NET S# Ron Nermji k INVESTIG News | FAQ J Resources | Contact | RSS Feed | Recent Comments Feed Resources Following is a listing of resources that you may find useful either to own or bookmark during your navigation through the .NET language space. If you feel that there's a resource that other .NET developers should know about, please contact nw. .NET Language Sites • At» • APL ч • ASP.NET: ASM to И. • AsmL i • ASP (Gotham) • Bask о VB .NET (Microsoft) о V8.NET (Mono) • BETA • Boo • BtueOragon • С о tec Ф Internet! Protected Mode On fu -r ^100% Рис. 1.2. Один из многочисленных сайтов с документацией по известным языкам программирования .NET Хотя настоящая книга ориентирована главным образом на тех, кого интересует разработка программ .NET с использованием С#, все равно рекомендуется посетить указанный сайт, поскольку там наверняка можно будет найти много языков для .NET, заслуживающих отдельного изучения в свободное время (вроде LISP .NET).
54 Часть I. Общие сведения о языке С# и платформе .NET Жизнь в многоязычном окружении В начале процесса осмысления разработчиком нейтральной к языкам природы платформы .NET, у него возникает множество вопросов и, прежде всего, следующий: если все языки .NET при компиляции преобразуются в управляемый код, то почему существует не один, а множество компиляторов? Ответить на этот вопрос можно по-разному. Мы, программисты, бываем очень привередливы, когда дело касается выбора языка программирования. Некоторые предпочитают языки с многочисленными точками с запятой и фигурными скобками, но с минимальным набором ключевых слов. Другим нравятся языки, предлагающие более "человеческие" синтаксические лексемы (вроде языка Visual Basic). Кто-то не желает отказываться от своего опыта работы на мэйнфреймах и предпочитает переносить его и на платформу .NET (использовать COBOL .NET). А теперь ответьте честно: если бы в Microsoft предложили единственный "официальный" язык .NET, например, на базе семейства BASIC, то все ли программисты были бы рады такому выбору? Или если бы "официальный" язык .NET основывался на синтаксисе Fortran, то сколько людей в мире просто бы проигнорировало платформу .NET? Поскольку среда выполнения .NET демонстрирует меньшую зависимость от языка, используемого для построения управляемого программного кода, программисты .NET могут, не меняя своих синтаксических предпочтений, обмениваться скомпилированными сборками со своими коллегами, другими отделами и внешними организациями (не обращая внимания на то, какой язык .NET в них применяется). Еще одно полезное преимущество интеграции различных языков .NET в одном унифицированном программном решении вытекает из того простого факта, что каждый язык программирования имеет свои сильные (а также слабые) стороны. Например, некоторые языки программирования обладают превосходной встроенной поддержкой сложных математических вычислений. В других лучше реализованы финансовые или логические вычисления, взаимодействие с мэйнфреймами и т.п. А когда преимущества конкретного языка программирования объединяются с преимуществами платформы .NET, выигрывают все. Конечно, в реальности велика вероятность того, что будет возможность тратить большую часть времени на построение программного обеспечения с помощью предпочитаемого языка .NET. Однако, после освоения синтаксиса одного из языков .NET, изучение синтаксиса какого-то другого языка существенно упрощается. Вдобавок это довольно выгодно, особенно тем, кто занимается консультированием по разработке ПО. Например, тому, у кого предпочитаемым языком является С#, в случае попадания в клиентскую среду, где все построено на Visual Basic, это все равно позволит эксплуатировать функциональные возможности .NET Framework и разбираться в общей структуре кодовой базы с минимальным объемом усилий и беспокойства. Сказанного вполне достаточно. Что собой представляют сборки в .NET Какой бы язык .NET не выбирался для программирования, важно понимать, что хотя двоичные ^ЕТ-единицы имеют такое же файловое расширение, как и двоичные единицы СОМ-серверов и неуправляемых программ Win32 (* . dll или * . ехе), внутренне они устроены абсолютно по-другому. Например, двоичные ^ЕТ-единицы * .dll не экспортируют методы для упрощения взаимодействия с исполняющей средой СОМ (поскольку .NET — это не СОМ). Более того, они не описываются с помощью библиотек СОМ-типов и не регистрируются в системном реестре. Пожалуй, самым важным является то, что они содержат не специфические, а наоборот, не зависящие от платформы инструкции на промежуточном языке (Intermediate Language — IL), а также метаданные типов. На рис. 1.3 показано, как все это выглядит схематически.
Исходный код наС# Исходный код HaPerl.NET Исходный код HaCOBOL.NET Исходный код на C++/CLI Глава 1. Философия .NET 55 Компилятор С# Компилятор Perl .NET Компилятор COBOL .NET Компилятор C++/CLI Инструкции IL и метаданные (* .dll или *.ехе) Рис. 1.3. Все .NET-компиляторы генерируют IL-инструкции и метаданные На заметку! Относительно сокращения "IL" уместно сказать несколько дополнительных слов. В ходе разработки .NET официальным названием для IL было Microsoft Intermediate Language (MSIL). Однако в вышедшей последней версии .NET это название было изменено на CIL (Common Intermediate Language — общий промежуточный язык). Поэтому при прочтении литературы по .NET следует помнить о том, что IL, MSIL и CIL обозначают одно и то же. Для отражения современной терминологии в настоящей книге будет применяться аббревиатура CIL. При создании файла * .dll или * . ехе с помощью .NET-компилятор а получаемый большой двоичный объект называется сборкой (assembly). Все многочисленные детали .NET-сборок будет подробно рассматриваться в главе 14. Для облегчения повествования об исполняющей среде здесь, однако, все-таки необходимо рассказать хотя бы об основных свойствах этого нового формата файлов. Как уже упоминалось, в сборке содержится CIL-код, который концептуально похож на байт-код Java тем, что не компилируется в ориентированные на конкретную платформу инструкции до тех пор, пока это не становится абсолютно необходимым. Обычно этот момент "абсолютной необходимости" наступает тогда, когда к какому-то блоку CIL- инструкций (например, к реализации метода) выполняется обращение для его использования в исполняющей среде .NET. Помимо CIL-инструкций, в сборках также содержатся метаданные, которые детально описывают особенности каждого имеющегося внутри данной двоичной .NET- единицы "типа". Например, при наличии класса по имени SportsCar они будут описывать детали наподобие того, как выглядит базовый класс этого класса SportsCar, какие интерфейсы реализует SportsCar (если вообще реализует), а также, подробно, какие члены он поддерживает Метаданные .NET всегда предоставляются внутри сборки и автоматически генерируются компилятором соответствующего распознающего .NET языка. И, наконец, помимо CIL и метаданных типов, сами сборки тоже описываются с помощью метаданных, которые официально называются манифестом (manifest). В каждом таком манифесте содержится информация о текущей версии сборки, сведения о культуре (применяемые для локализации строковых и графических ресурсов) и перечень ссылок на все внешние сборки, которые требуются для правильного функционирования. Разнообразные инструменты, которые можно использовать для изучения типов, метаданных и манифестов сборок, рассматриваются в нескольких последующих главах.
56 Часть I. Общие сведения о языке С# и платформе .NET Однофайловые и многофайловые сборки В большом количестве случаев между сборками .NET и файлами двоичного кода (* . dll или * . ехе) соблюдается простое соответствие "один к одному". Следовательно, получается, что при построении * . dll-библиотеки .NET, можно спокойно полагать, что файл двоичного кода и сборка представляют собой одно и то же, и что, аналогичным образом, при построении исполняемого приложения для настольной системы на файл * . ехе можно ссылаться как на саму сборку. Однако, как будет показано в главе 14, это не совсем так. С технической точки зрения, сборка, состоящая из одного единственного модуля * . dll или * . ехе, называется однофайловой сборкой. В однофайловых сборках все необходимые CIL-инструкции, метаданные и манифесты содержатся в одном автономном четко определенном пакете. Многомофайловые сборки, в свою очередь, состоят из множества файлов двоичного кода .NET, каждый из которых называется модулем (module). При построении многофайловой сборки в одном из ее модулей (называемом первичным или главным (primary) модулем) содержится манифест всей самой сборки (и, возможно, CIL-инструкции и метаданные по различным типам), а во всех остальных — манифест, CIL-инструкции и метаданные типов, охватывающие уровень только соответствующего7 модуля. Как нетрудно догадаться, в главном модуле содержится описание набора требуемых дополнительных модулей внутри манифеста сборки. На заметку! В главе 14 будет более подробно разъясняться, в чем состоит различие между одно- файловыми и многофайловыми сборками. Однако следует иметь в виду, что Visual Studio 2010 может применяться только для создания однофайловых сборок. В тех редких случаях возникновения необходимости в создании именно многофайловой сборки требуется использовать соответствующие утилиты командной строки. Роль CIL Теперь давайте немного более подробно посмотрим, что же собой представляет CIL- код, метаданные типов и манифест сборки. CIL является таким языком, который стоит выше любого конкретного набора ориентированных на определенную платформу инструкций. Например, ниже приведен пример кода на С#, в котором создается модель самого обычного калькулятора. Углубляться в конкретные детали синтаксиса пока не нужно, главное обратить внимание на формат такого метода в этом классе Calc, как Add(). //Класс Calc.cs using System; namespace CalculatorExample { //В этом классе содержится точка для входа в приложение. class Program { static void Main() { Calc с = new Calc () ; int ans = с Add A0, 84); Console.WriteLine(0 + 84 is {0 } . ", ans); // Обеспечение ожидания нажатия пользователем // клавиши <Enter> перед выходом. Console.ReadLine(); / / Калькулятор на С#. class Calc
Глава 1. Философия .NET 57 { public int Add(int x, int y) { return x + y; } } } После выполнения компиляции файла с этим кодом с помощью компилятора С# (csc.exe) получится однофайловая сборка * .ехе, в которой будет содержаться манифест, CIL-инструкции и метаданные, описывающие каждый из аспектов класса Calc и Program. На заметку! О том, как выполнять компиляцию кода с помощью компилятора С#, а также использовать графические IDE-среды, подобные Microsoft Visual Studio 2010, Microsoft Visual C# 2010 Express и SharpDevelop, будет более подробно рассказываться в главе 2. Например, открыв данную сборку в утилите ildasm.exe (которая более подробно рассматривается далее в настоящей главе), можно увидеть, что метод Add () был преобразован в CIL так, как показано ниже: .method public hidebysig instance int32 Add(int32 x, int32 y) cil managed { // Code size 9 @x9) // Размер кода 9 @x9) .maxstack 2 .locals mit (int32 V_0) IL_0000: nop IL_0001: ldarg.l IL_0002: ldarg.2 IL_0003: add IL_0004: stloc.O IL_0005: br.s IL_0007 IL_0007: ldloc.O IL_0008: ret } // end of method Calc::Add // конец метода Calc::Add He стоит беспокоиться, если пока совершенно не понятно, что собой представляет результирующий CIL-код этого метода, потому что в главе 17 будут рассматриваться все необходимые базовые аспекты языка программирования CIL. Главное заметить, что компилятор С# выдает CIL-код, а не ориентированные на определенную платформу инструкции. Теперь напоминаем, что так себя ведут все .NET-компиляторы. Чтобы убедиться в этом, давайте попробуем создать то же самое приложение с использованием языка Visual Basic, а не С#. 'Класс Calc.vb Imports System Namespace CalculatorExample 1 В VB "модулем" называется класс, в котором 1 содержатся только статические члены. Module Program Sub Main () Dim с As New Calc Dim ans As Integer = c.AddA0, 84) Console.WriteLine(0 + 84 is {0}.", ans) Console.ReadLine() End Sub End Module Class Calc
58 Часть I. Общие сведения о языке С# и платформе .NET Public Function Add(ByVal x As Integer, ByVal у As Integer) As Integer Return x + у End Function End Class End Namespace В случае изучения CIL-кода этого метода Add () можно будет обнаружить похожие инструкции (лишь слегка подправленные компилятором Visual Basic, vbc . exe): .method public instance int32 Add(int32 x, int32 y) cil managed { // Code size 8 @x8) // Размер кода 8 @x8) .maxstack 2 .locals mit (int32 V_0) IL_0000: ldarg.l IL_0001: ldarg.2 IL_0002: add.ovf IL_0003: stloc.O IL_0004: br.s IL_0006 IL_0006: ldloc.O IL_0007: ret } // end of method Calc: :Add // конец метода Calc::Add Исходный код. Файлы с кодом Calc . cs и Calc . vb доступны в подкаталоге Chapter l. Преимущества CIL На этом этапе может возникнуть вопрос о том, какую выгоду приносит компиляция исходного кода в CIL, а не напрямую в набор ориентированных на конкретную платформу инструкций. Одним из самых важных преимуществ такого подхода является интеграция языков. Как уже можно было увидеть, все компиляторы .NET генерируют примерно одинаковые CIL-инструкции. Благодаря этому все языки могут взаимодействовать в рамках четко обозначенной двоичной "арены". Более того, поскольку CIL не зависит от платформы, .NET Framework тоже получается не зависящей от платформы, предоставляя те же самые преимущества, к которым привыкли Java-разработчики (например, единую кодовую базу, способную работать во многих операционных системах). На самом деле уже существует международный стандарт языка С#, а также подмножество платформы .NET и реализации для многих операционных систем, отличных от Windows (более подробно об этом речь пойдет в конце настоящей главы). В отличие от Java, однако, .NET позволяет создавать приложения на предпочитаемом языке. Компиляция CIL-кода в инструкции, ориентированные на конкретную платформу Из-за того, что в сборках содержатся CIL-инструкции, а не инструкции, ориентированные на конкретную платформу, CIL-код перед использованием должен обязательно компилироваться на лету. Объект, который отвечает за компиляцию CIL-кода в понятные ЦП инструкции, называется оперативным dust-in-time — JIT) компилятором. Иногда его "по-дружески" называют Jitter. Исполняющая среда .NET использует JIT-компилятор в соответствии с целевым ЦП и оптимизирует его согласно лежащей в основе платформе.
Глава 1. Философия .NET 59 Например, в случае создания .NET-приложения, предназначенного для развертывания на карманном устройстве (например, на мобильном устройстве, функционирующем под управлением Windows), соответствующий JIT-компилятор будет оптимизирован под функционирование в среде с ограниченным объемом памяти, а в случае развертывания сборки на серверной системе (где объем памяти редко представляет проблему), наоборот — под функционирование в среде с большим объемом памяти. Это дает разработчикам возможность писать единственный блок кода, который будет автоматически эффективно компилироваться JIT-компилятором и выполняться на машинах с разной архитектурой. Более того, при компиляции CIL-инструкций в соответствующий машинный код JIT- компилятор будет помещать результаты в кэш в соответствии с тем, как того требует целевая операционная система. То есть при вызове, например, метода PrintDocument () в первый раз соответствующие С11>инструкции будут компилироваться в ориентированные на конкретную платформу инструкции и сохраняться в памяти для последующего использования, благодаря чему при вызове PrintDocument () в следующий раз компилировать их снова не понадобится. На заметку! Можно также выполнять "предварительную ЛТ-компиляцию" при инсталляции приложения с помощью утилиты командной строки ngen.exe, которая поставляется в составе набора .NET Framework 4.0 SDK. Применение такого подхода позволяет улучшить показатели по времени запуска для приложений, насыщенных графикой. Роль метаданных типов в .NET Помимо CIL-инструкций, в сборке .NET содержатся исчерпывающие и точные метаданные, которые описывают каждый определенный в двоичном файле тип (например, класс, структуру или перечисление), а также всех его членов (например, свойства, методы или события). К счастью, за генерацию новейших и наилучших метаданных по типам всегда отвечает компилятор, а не программист. Из-за того, что метаданные .NET являются настолько детальными, сборки представляют собой полностью самоописываемые (self-describing) сущности. Чтобы увидеть, как выглядит формат метаданных типов в .NET, давайте рассмотрим метаданные, которые были сгенерированы для приведенного выше метода Add () из класса Calc на языке С# (метаданные для версии метода Add () на языке Visual Basic будут выглядеть похоже): TypeDef #2 @2000003) TypDefName: CalculatorExample.Calc @2000003) Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldlnit] @0100001) Extends : 01000001 [TypeRef] System.Object Method #1 @6000003) Methodllame Flags PVA ImplFlags CallCnvntn hasThis ReturnType: 14 2 Arguments Argument #1: 14 Argument #2: 14 Add @6000003) [Public] [HideBySig] [ReuseSlot] @0000086) 0x00002090 [IL] [Managed] @0000000) [DEFAULT]
60 Часть I. Общие сведения о языке С# и платформе .NET 2 Parameters A) ParamToken : @8000001) Name : x flags: [none] @0000000) B) ParamToken : @8000002) Name : у flags: [none] @0000000) Метаданные используются во многих операциях самой исполняющей среды .NET, a также в различных средствах разработки. Например, функция IntelliSense, предлагаемая в таких средствах, как Visual Studio 2010, работает за счет считывания метаданных сборки во время проектирования. Кроме того, метаданные используются в различных утилитах для просмотра объектов, инструментах отладки и в самом компиляторе языка С#. Можно с полной уверенностью утверждать, что метаданные играют ключевую роль во многих .NET-технологиях, в том числе в Windows Communication Foundation (WCF), рефлексии, динамическом связывании и сериализации объектов. Более подробно о роли метаданных .NET будет рассказываться в главе 17. Роль манифеста сборки И, наконец, последним, но не менее важным моментом, о котором осталось вспомнить, является наличие в сборке .NET и таких метаданных, которые описывают саму сборку (они формально называются манифестом). Помимо прочих деталей, в манифесте документируются все внешние сборки, которые требуются текущей сборке для корректного функционирования, версия сборки, информация об авторских правах и т.д. Как и за генерацию метаданных типов, за генерацию манифеста сборки тоже всегда отвечает компилятор. Ниже приведены некоторые наиболее существенные детали манифеста, сгенерированного в результате компиляции приведенного ранее в этой главе файла двоичного кода Calc.cs (здесь предполагается, что компилятору было указано назначить сборке имя Calc . ехе): .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 4:0:0:0 } .assembly Calc { .hash algorithm 0x00008004 .ver 1:0:0:0 } .module Calc.exe .imagebase 0x00400000 .subsystem 0x00000003 .file alignment 512 .corflags 0x00000001 В двух словах, в этом манифесте представлен список требуемых для Calc. ехе внешних сборок (в директиве . assembly extern), а также различные характеристики самой сборки (наподобие номера версии, имени модуля и т.д.). О пользе данных манифеста будет гораздо более подробно рассказываться в главе 14. Что собой представляет общая система типов (CTS) В каждой конкретной сборке может содержаться любое количество различающихся типов. В мире .NET "тип" представляет собой просто общий термин, который применяется для обозначения любого элемента из множества (класс, интерфейс, структура, перечисление, делегат}. При построении решений с помощью любого языка .NET, скорее всего, придется взаимодействовать со многими из этих типов. Например, в сборке
Глава 1. Философия .NET 61 может содержаться один класс, реализующий определенное количество интерфейсов, метод одного из которых может принимать в качестве входного параметра перечисление, а возвращать структуру. Вспомните, что CTS (общая система типов) представляет собой формальную спецификацию, в которой описано то, как должны быть определены типы для того, чтобы они могли обслуживаться в CLR-среде. Внутренние детали CTS обычно интересуют только тех, кто занимается разработкой инструментов и/или компиляторов для платформы .NET. Абсолютно всем .NET-программистам, однако, важно уметь работать на предпочитаемом ими языке с пятью типами из CTS. Краткий обзор этих типов приведен ниже. Типы классов В каждом совместимом с .NET языке поддерживается, как минимум, понятие типа класса (class type), которое играет центральную роль в объектно-ориентированном программировании (ООП). Каждый класс может включать в себя любое количество членов (таких как конструкторы, свойства, методы и события) и точек данных (полей). В С# классы объявляются с помощью ключевого слова class. // Тип класса С# с одним методом. class Calc { public int Add(int x, int y) { return x + y; } } В главе 5 будет более подробно показываться, как можно создавать типы классов CTS в С#, а пока в табл. 1.1 приведен краткий перечень характеристик, которые свойственны типам классов. Таблица 1.1. Характеристики классов CTS Характеристика 0пи классов Запечатанные Запечатанные (sealed), или герметизированные, классы не могут выступать в роли базовых для других классов, т.е. не допускают наследования Реализующие Интерфейсом (interface) называется коллекция абстрактных членов, кото- интерфейсы рые обеспечивают возможность взаимодействия между объектом и пользователем этого объекта. CTS позволяет реализовать в классе любое количество интерфейсов Абстрактные Экземпляры абстрактных (abstract) классов не могут создаваться напря- или мую, и предназначены для определения общих аспектов поведения для конкретные производных типов. Экземпляры же конкретных (concrete) классы могут создаваться напрямую Степень видимости Каждый класс должен конфигурироваться с атрибутом видимости (visibility). По сути, этот атрибут указывает, должен ли класс быть доступным для использования внешним сборкам или только изнутри определяющей сборки Типы интерфейсов Интерфейсы представляют собой не более чем просто именованную коллекцию определений абстрактных членов, которые могут поддерживаться (т.е. реализоваться) в данном классе или структуре. В С# типы интерфейсов определяются с помощью ключевого слова interface, как, например, показано ниже:
62 Часть I. Общие сведения о языке С# и платформе .NET // Тип интерфейса в С# обычно объявляется // общедоступным, чтобы позволить типам в других // сборках реализовать его поведение. public interface IDraw { void Draw(); } Сами по себе интерфейсы мало чем полезны. Однако когда они реализуются в классах или структурах уникальным образом, они позволяют получать доступ к дополнительным функциональным возможностям за счет добавления просто ссылки на них в полиморфной форме. Тема программирования с использованием интерфейсов подробно рассматривается в главе 9. Типы структур Понятие структуры тоже сформулировано в CTS. Тем, кому приходилось работать с языком С, будет приятно узнать, что таким пользовательским типам удалось "выжить" в мире .NET (хотя на внутреннем уровне они и ведут себя несколько иначе). Попросту говоря, структура может считаться "облегченным" типом класса с основанной на использовании значений семантикой. Более подробно об особенностях структур будет рассказываться в главе 4. Обычно структуры лучше всего подходят для моделирования геометрических и математических данных, и в С# они создаются с помощью ключевого слова struct. / / Тип структуры в С#. struct Point i //В структурах могут содержаться поля. public int xPos, yPos; //В структурах могут содержаться параметризованные конструкторы. public Point (int x, int у) { xPos = x; yPos = y; } // В структурах могут определяться методы. public void PrintPosition () { Console.WriteLine (" ({ 0}, {1})", xPos, yPos); } } Типы перечислений Перечисления (enumeration) представляют собой удобную программную конструкцию, которая позволяет группировать данные в пары "имя-значение". Например, предположим, что требуется создать приложение видеоигры, в котором игроку бы позволялось выбирать персонажа одной из трех следующих категорий: Wizard (маг), Fighter (воин) или Thief (вор). Вместо того чтобы использовать и отслеживать числовые значения для каждого варианта, в этом случае гораздо удобнее создать соответствующее перечисление с помощью ключевого слова enum: // Тип перечисления С#. public enum CharacterType { Wizard = 100, Fighter = 200, Thief = 300 }
Глава 1. Философия .NET 63 По умолчанию для хранения каждого элемента выделяется блок памяти, соответствующий 32-битному целому, однако при необходимости (например, при программировании с расчетом на устройства, обладающие малыми объемами памяти, вроде мобильных устройств Windows) это значение можно изменить. Кроме того, в CTS необходимо, чтобы перечислймые типы наследовались от общего базового класса System.Enum. Как будет показано в главе 4, в этом базовом классе присутствует ряд весьма интересных членов, которые позволяют извлекать, манипулировать и преобразовывать базовые пары "имя-значение" программным образом. Типы делегатов Делегаты (delegate) являются .NET-эквивалентом безопасных в отношении типов указателей функций в стиле С. Главное отличие заключается в том, что делегат в .NET представляет собой класс, который наследуется от System.MulticastDelegate, a не просто указатель на какой-то конкретный адрес в памяти. В С# делегаты объявляются с помощью ключевого слова delegate. // Этот тип делегата в С# может 'указывать' на любой метод, // возвращающий целое число и принимающий два целых // числа в качестве входных данных. public delegate int BinaryOp(int x, int y); Делегаты очень удобны, когда требуется обеспечить одну сущность возможностью перенаправлять вызов другой сущности и образовывать основу для архитектуры обработки событий .NET. Как будет показано в главах 11 и 19, делегаты обладают внутренней поддержкой для групповой адресации (т.е. пересылки запроса сразу множеству получателей) и асинхронного вызова методов (т.е. вызова методов во вторичном потоке). Члены типов Теперь, когда было приведено краткое описание каждого из сформулированных в CTS типов, пришла пора рассказать о том, что большинство из этих типов способно принимать любое количество членов (member). Формально в роли члена типа может выступать любой элемент из множества {конструктор, финализатор, статический конструктор, вложенный тип, операция, метод, свойство, индексатор, поле, поле только для чтения, константа, событие). В спецификации CTS описываются различные "характеристики", которые могут быть ассоциированы с любым членом. Например, каждый член может обладать характеристикой, отражающей его доступность (т.е. общедоступный, приватный или защищенный). Некоторые члены могут объявляться как абстрактные (для навязывания полиморфного поведения производным типам) или как виртуальные (для определения фиксированной, но допускающей переопределение реализации). Кроме того, почти все члены также могут делаться статическими членами (привязываться на уровне класса) или членами экземпляра (привязываться на уровне объекта). Более подробно о том, как можно создавать членов, будет рассказываться в ходе нескольких следующих глав. На заметку! Как будет описано в главе 10, в языке С# также поддерживается создание обобщенных типов и членов. Встроенные типы данных И, наконец, последним, что следует знать о спецификации CTS, является то, что в ней содержится четко определенный набор фундаментальных типов данных. Хотя в каждом отдельно взятом языке для объявления того или иного встроенного типа данных
64 Часть I. Общие сведения о языке С# и платформе .NET из CTS обычно предусмотрено свое уникальное ключевое слово, все эти ключевые слова в конечном итоге соответствуют одному и тому же типу в сборке mscorlib. dll. В табл. 1.2 показано, как ключевые типы данных из CTS представляются в различных .NET-языках. Таблица 1.2. Встроенные типы данных, описанные в CTS Тип данных в CTS Ключевое слово в Visual Basic Ключевое слово в С# Ключевое слово в С++и CLI System.ByteByte System.SByteSByte System.Intl6 System.Int32 System.Int64 System.UIntl6 System.UInt32 System.UInt64 System.SingleSingle System.DoubleDouble System.Ob]ectObject System.CharChar System.StringString System.DecimalDecimal System.BooleanBoolean Byte SByte Short Integer Long UShort Ulnteger ULong Single Double Object Char String Decimal Boolean byte sbyte short int long ushort uint ulong float double object char String decimal bool unsigned char signed char short int или long int64 unsigned unsigned unsigned unsigned float double objectA wchar t StringA Decimal bool short int или long int64 Из-за того факта, что уникальные ключевые слова в любом управляемом языке являются просто сокращенными обозначениями реального типа из пространства имен System, больше не нужно беспокоиться ни об условиях переполнения и потери значимости (overflow/underflow) в случае числовых данных, ни о внутреннем представлении строк и булевских значений в различных языках. Рассмотрим следующие фрагменты кода, в которых 32-битные числовые переменные определяются в С# и Visual Basic с использованием соответствующих ключевых слов из самих языков, а также формального типа из CTS: // Определение числовых переменных в С#. int 1=0; System.Int32 j = 0; 1 Определение числовых переменных в VB. Dim 1 As Integer = 0 Dim j As System.Int32 = 0 Что собой представляет общеязыковая спецификация (CLS) Как известно, в разных языках программирования одни и те же программные конструкции выражаются своим уникальным, специфическим для конкретного языка образом. Например, в С# конкатенация строк обозначается с помощью знака "плюс" (+),
Глава 1. Философия .NET 65 а в VB для этого обычно используется амперсанд (&). Даже в случае выражения в двух отличных языках одной и той же программной идиомы (например, функции, не возвращающей значения), очень высока вероятность того, что с виду синтаксис будет выглядеть очень по-разному: //Не возвращающий ничего метод в С#. public void MyMethod () { // Какой-нибудь интересный код... } ' Не возвращающий ничего метод в VB. Public Sub MyMethod () ' Какой-нибудь интересный код... End Sub Как уже показывалось, подобные небольшие вариации в синтаксисе для исполняющей среды .NET являются несущественными благодаря тому, что соответствующие компиляторы (в данном случае — csc.exe и vbc.exe) генерируют схожий набор CIL- инструкций. Однако языки могут еще отличаться и по общему уровню функциональных возможностей. Например, в каком-то из языков .NET может быть или не быть ключевого слова для представления данных без знака, а также поддерживаться или не поддерживаться типы указателей. Из-за всех таких вот возможных вариаций было бы просто замечательно иметь в распоряжении какие-то опорные требования, которым должны были бы отвечать все поддерживающие .NET языки. CLS (Common Language Specification — общая спецификация для языков программирования) как раз и представляет собой набор правил, которые во всех подробностях описывают минимальный и полный комплект функциональных возможностей, которые должен обязательно поддерживать каждый отдельно взятый .NET-компилятор для того, чтобы генерировать такой программный код, который мог бы обслуживаться CLR и к которому в то же время могли бы единообразным образом получать доступ все языки, ориентированные на платформу .NET. Во многих отношениях CLS может считаться просто подмножеством всех функциональных возможностей, определенных в CTS. В конечном итоге CLS является своего рода набором правил, которых должны придерживаться создатели компиляторов при желании, чтобы их продукты могли без проблем функционировать в мире .NET Каждое из этих правил имеет простое название (например, "Правило CLS номер 6") и описывает, каким образом его действие касается тех, кто создает компиляторы, и тех, кто (каким-либо образом) будет взаимодействовать с ними. Самым главным в CLS является правило J, гласящее, что правила CLS касаются только тех частей типа, которые делаются доступными за пределами сборки, в которой они определены. Из этого правила можно (и нужно) сделать вывод о том, что все остальные правила в CLS не распространяются на логику, применяемую для построения внутренних рабочих деталей типа .NET. Единственными аспектами типа, которые должны соответствовать CLS, являются сами определения членов (т.е. соглашения об именовании, параметры и возвращаемые типы). В рамках логики реализации члена может применяться любое количество и не согласованных с CLS приемов, поскольку для внешнего мира это не будет играть никакой роли. Для иллюстрации ниже приведен метод Add () на языке С#, который не отвечает правилам CLS, поскольку в его параметрах и возвращаемых значениях используются данные без знака (что не является требованием CLS): class Calc I // Использование данных без знака внешним образом // не соответствует правилам CLS!
66 Часть I. Общие сведения о языке С# и платформе .NET public ulong Add(ulong x, ulong y) { return x + y; } } Однако если бы мы просто использовали данные без знака внутренним образом, как показано ниже: class Calc { public int Adddnt x, mt y) { // Поскольку переменная ulong используется здесь // только внутренне, правила CLS не нарушаются. ulong temp = 0; return x + у; } } тогда правила CLS были бы соблюдены и все языки .NET могли бы обращаться к данному методу Add (). Разумеется, помимо правила 1 в CLS содержится и много других правил. Например, в CLS также описано, каким образом в каждом конкретном языке должны представляться строки текста, оформляться перечисления (подразумевающие использование базового типа для хранения), определяться статические члены и т.д. К счастью, для того, чтобы быть умелым разработчиком .NET, запоминать все эти правила вовсе не обязательно. Опять-таки, очень хорошо разбираться в спецификациях CTS и CLS необходимо только создателям инструментов и компиляторов. Забота о соответствии правилам CLS Как можно будет увидеть в ходе прочтения настоящей книги, в С# на самом деле имеется ряд программных конструкций, которые не соответствуют правилам CLS. Хорошая новость, однако, состоит в том, что компилятор С# можно заставить выполнять проверку программного кода на предмет соответствия правилам CLS с помощью всего лишь единственного атрибута. NET: / / Указание компилятору С# выполнять проверку //на предмет соответствия CLS. [assembly: System.CLSCompliant(true)] Детали программирования с использованием атрибутов более подробно рассматриваются в главе 15. А пока главное понять просто то, что атрибут [CLSCompliant] заставляет компилятор С# проверять каждую строку кода на предмет соответствия правилам CLS. В случае обнаружения нарушения каких-нибудь правил CLS компилятор будет выдавать ошибку и описание вызвавшего ее кода. Что собой представляет общеязыковая исполняющая среда (CLR) Помимо спецификаций CTS и CLS, для получения общей картины на данный момент осталось рассмотреть еще одну аббревиатуру — CLR, которая расшифровывается как Common Language Runtime (общеязыковая исполняющая среда). С точки зрения программирования под термином исполняющая среда может пониматься коллекция внешних служб, которые требуются для выполнения скомпилированной единицы программного кода. Например, при использовании платформы MFC для создания нового
Глава 1. Философия .NET 67 приложения разработчики осознают, что их программе требуется библиотека времени выполнения MFC (т.е. mfc42 . dll). Другие популярные языки тоже имеют свою исполняющую среду: программисты, использующие язык VB6, к примеру, вынуждены привязываться к одному или двум модулям исполняющей среды (вроде msvbvm60 .dll), a разработчики на Java — к виртуальной машине Java (JVM). В составе .NET предлагается еще одна исполняющая среда. Главное отличие между исполняющей средой .NET и упомянутыми выше средами, состоит в том, что исполняющая среда .NET обеспечивает единый четко определенный уровень выполнения, который способны использовать все совместимые с .NET языки и платформы. Основной механизм CLR физически имеет вид библиотеки под названием mscoree .dll (и также называется общим механизмом выполнения исполняемого кода объектов — Common Object Runtime Execution Engine). При добавлении ссылки на сборку для ее использования загрузка библиотеки mscoree . dll осуществляется автоматически и затем, в свою очередь, приводит к загрузке требуемой сборки в память. Механизм исполняющей среды отвечает за выполнение целого ряда задач. Сначала, что наиболее важно, он отвечает за определение места расположения сборки и обнаружение запрашиваемого типа в двоичном файле за счет считывания содержащихся там метаданных. Затем он размещает тип в памяти, преобразует CIL-код в соответствующие платформе инструкции, производит любые необходимые проверки на предмет безопасности и после этого, наконец, непосредственно выполняет сам запрашиваемый программный код. Помимо загрузки пользовательских сборок и создания пользовательских типов, механизм CLR при необходимости будет взаимодействовать и с типами, содержащимися в библиотеках базовых классов .NET. Хотя вся библиотека базовых классов поделена на ряд отдельных сборок, главной среди них является сборка ms cor lib .dll. В этой сборке содержится большое количество базовых типов, охватывающих широкий спектр типичных задач программирования, а также базовых типов данных, применяемых во всех языках .NET. При построении .NET-решений доступ к этой конкретной сборке будет предоставляться автоматически. На рис. 1.4 схематично показано, как выглядят взаимоотношения между исходным кодом (предусматривающим использование типов из библиотеки базовых классов), компилятором .NET и механизмом выполнения .NET. Различия между сборками, пространствами имен и типами Каждый из нас понимает важность библиотек программного кода. Главная цель таких библиотек, как MFC, Java Enterprise Edition или ATL, заключается в предоставлении разработчикам набора готовых, правильно оформленных блоков программного кода, чтобы они могли использовать их своих приложениях. Язык С#, однако, не поставляется с какой-либо специфической библиотекой кода. Вместо этого от разработчиков, использующих С#, требуется применять нейтральные к языкам библиотеки, которые поставляются в .NET. Для поддержания всех типов в библиотеках базовых классов в хорошо организованном виде в .NET широко применяется понятие пространства имен (namespace). Под пространством имен понимается группа связанных между собой с семантической точки зрения типов, которые содержатся в сборке. Например, в пространстве имен System. 10 содержатся типы, имеющие отношение к операциям ввода-вывода, в пространстве имен System. Data — основные типы для работы с базами данных, и т.д.
68 Часть I. Общие сведения о языке С# и платформе .NET Исходный код .NET несовместимом с .NET языке Механизм выполнения .NET (mscoree.dll) Загрузчик классов ЛТ-компилятор г Соответствующие платформе инструкции ■ ■ Выполнение члена Рис. 1.4. Механизм mscoree.dll в действии Очень важно понимать, что в одной сборке (например, ms cor lib. dll) может содержаться любое количество пространств имен, каждое из которых, в свою очередь, может иметь любое число типов. Чтобы стало понятнее, на рис. 1.5 показан снимок окна предлагаемой в Visual Studio 2010 утилиты Object Browser. Эта утилита позволяет просматривать сборки, на которые имеются ссылки в текущем проекте, пространства имен, содержащиеся в каждой из этих сборок, типы, определенные в каждом из этих пространств имени, и члены каждого из этих типов. Важно обратить внимание на то, что в mscorlib.dll содержится очень много самых разных пространств имен (вроде System. 10), и что в каждом из них содержатся свои семантически связанные типы (такие как BinaryReader) . Компилятор .NET Сборка * .dll или *.ехе (с CIL-инструкциями, метаданными и манифестом) Библиотеки базовых классов (mscorlib.dll и др.)
Глава 1. Философия .NET 69 Object Browse/ X Q Browse: My Solution • ... |<Search> - J | ! j3 CSharpAdder i> >J Microsoft.CSharp {} Microsoft.Win32 {} Microsoft.Win32.SafeHandies i {) System.Collections • {} System.Collections.Concurrent {} System.Collections.Generic {} System.Collectiora.ObjectModel {} System.Configuratton.Assemblie5 ; {} System.Deployment.Internal {} System.Diagnostics t> {} System.Diagnostics.CodeAnarysis > {} System.Diagnostics.Contracts 0 System.Diagnostics.Eventing > {} System.Diagnostics.SymbolStore О System.Globalization л {} System.IO ■*jU! > -*J BinaryWrrter «tj BufferedStream 4 BinaryReader(SystemJO.Stream, System.Text.Encoding) -» ♦ BinaryReader(SystemJO.Stream) ♦ CloseO Ф DisposeO y* Dispose(bool) v FillBuffer(mt) ♦ PeekCharQ * Read(byte(L int int) # Read(char[L int int) * ReadO ft Read7BitEncodedIntO ♦ ReedBooleanO * ReadByteO Ф ReadBytes(rnt) V ReadCharO * ReadChars(int) ♦ BadBwrwM V ReadDoubleQ Уш1шш&ЬАЯй4ШШй1штшжтйш тиггпшчгггигпгиипгп-пппппт! и -11 [public class Binary Reader 1 Member of System .10 1 Summary: 1 Reads primitive data types as binary values in a specific encoding. Рис. 1.5. В одной сборке может содержаться произвольное количество пространств имен Птавное отличие между таким подходом и зависящими от конкретного языка библиотеками вроде MFC состоит в том, что он обеспечивает использование во всех языках, ориентированных на среду выполнения .NET, одних и тех лее пространств имен и одних и тех лее типов. Например, в трех приведенных ниже программах иллюстрируется создание постоянно применяемого примера "Hello World" на языках С#, VB и C++/CLI. // Hello world на языке С# using System; public class MyApp { static void Mam() { Console.WriteLine ("Hi from C#"); ' Hello world на языке VB Imports System Public Module MyApp Sub Main () Console.WriteLine ("Hi from VB") End Sub End Module // Hello world на языке C++/CLI #include "stdafx.h" using namespace System; int main(array<System::String Л> Aargs) { Console::WriteLine("Hi from C++/CLI"), return 0; Обратите внимание, что в каждом из языков применяется класс Console, определенный в пространстве имен System. Если отбросить незначительные синтаксические отличия, то в целом все три программы выглядят очень похоже, как по форме, так и по логике.
70 Часть I. Общие сведения о языке С# и платформе .NET Очевидно, что главной задачей любого планирующего использовать .NET разработчика является освоение того обилия типов, которые содержатся в (многочисленных) пространствах имен .NET. Самым главным пространством имен, с которого следует начинать, является System. В этом пространстве имен содержится набор ключевых типов, которые любому разработчику .NET нужно будет эксплуатировать снова и снова. Фактически создание функционального приложения на С# невозможно без добавления хотя бы ссылки на пространство имен System, поскольку все главные типы данных (вроде System. Int32, System. String и т.д.) содержатся именно здесь. В табл. 1.3 приведен краткий список некоторых (но, конечно же, не всех) предлагаемых в .NET пространств имен, которые были поделены на группы на основе функциональности. Таблица 1.3. Некоторые пространства имен в .NET Пространство имен в .NET Описание System System.Collections System.Collections.Generic System.Data System.Data.Common System.Data.EntityClient System.Data.SqlClient System.10 System.10.Compression System.10.Ports System.Reflection System.Refleetion.Emit System.Runtime.InteropServices System.Drawing System.Windows.Forms System.Windows System.Windows.Controls System.Windows.Shapes System.Linq System.Xml.Linq System.Data.DataSetExtensions System.Web Внутри пространства имен System содержится множество полезных типов, позволяющих иметь дело с внутренними данными, математическими вычислениями, генерированием случайных чисел, переменными среды и сборкой мусора, а также ряд наиболее часто применяемых исключений и атрибутов В этих пространствах имен содержится ряд контейнерных типов, а также несколько базовых типов и интерфейсов, которые позволяют создавать специальные коллекции Эти пространства имен применяются для взаимодействия с базами данных с помощью AD0.NET В этих пространствах содержится много типов, предназначенных для работы с операциями файлового ввода- вывода, сжатия данных и манипулирования портами В этих пространствах имен содержатся типы, которые поддерживают обнаружение типов во время выполнения, а также динамическое создание типов В этом пространстве имен содержатся средства, с помощью которых можно позволить типам .NET взаимодействовать с "неуправляемым кодом" (например, DLL- библиотеками на базе С и серверами СОМ) и наоборот В этих пространствах имен содержатся типы, применяемые для построения настольных приложений с использованием исходного набора графических инструментов .NET (Windows Forms) Пространство System.Windows является корневым среди этих нескольких пространств имен, которые представляют собой набор графических инструментов Windows Presentation Foundation (WPF) В этих пространствах имен содержатся типы, применяемые при выполнении программирования с использованием API-интерфейса LINQ Это пространство имен является одним из многих, которые позволяют создавать веб-приложения ASP.NET
Глава 1. Философия .NET 71 Окончание табл. 1.3 Пространство имен в .NET Описание System. ServiceModel Это пространство имен является одним из многих, которые позволяется применять для создания распределенных приложений с помощью API-интерфейса Windows Communication Foundation (WCF) System. Workflow. Runtime Эти два пространства имен являются главными предста- System. Workflow.Activities вителями многочисленных пространств имен, в которых содержатся типы, применяемые для построения поддерживающих рабочие потоки приложений с помощью API-интерфейса Windows Workflow Foundation (WWF) System.Threading В этом пространстве имен содержатся многочисленные System.Threading. Tasks типы для построения многопоточных приложений, способных распределять рабочую нагрузку среди нескольких ЦП. System. Security Безопасность является неотъемлемым свойством мира .NET. В относящихся к безопасности пространствах имен содержится множество типов, которые позволяют иметь дело с разрешениями, криптографической защитой и т.д System. Xml В этом ориентированном на XML пространстве имен содержатся многочисленные типы, которые можно применять для взаимодействия с XML-данными Роль корневого пространства Microsoft При изучении перечня, приведенного в табл. 1.3, нетрудно было заметить, что пространство имен System является корневым для приличного количества вложенных пространств имен (таких как System. 10, System. Data и т.д.). Как оказывается, однако, помимо System в библиотеке базовых классов предлагается еще и ряд других корневых пространств имен наивысшего уровня, наиболее полезным из которых является пространство имен Microsoft. В любом пространстве имен, которое находится внутри пространства имен Microsoft (как, например, Microsoft.CSharp, Microsoft.ManagementConsole и Microsoft .Win32), содержатся типы, применяемые для взаимодействия исключительно с теми службами, которые свойственны только лишь операционной системе Windows. Из-за этого не следует предполагать, что данные типы могут с тем же успехом применяться и в других поддерживающих .NET операционных системах вроде Мае OS X. В настоящей книге детали вложенных в Microsoft пространств имен подробно рассматриваться не будут, поэтому заинтересованным придется обратиться к документации .NET Framework 4.0 SDK. На заметку! В главе 2 будет показано, как пользоваться документацией .NET Framework 4.0 SDK, в которой содержатся детальные описания всех пространств имен, типов и членов, встречающихся в библиотеках базовых классов. Получение доступа к пространствам имен программным образом Не помешает снова вспомнить, что пространства имен представляют собой не более чем удобный способ логической организации взаимосвязанных типов для упрощения работы с ними. Давайте еще раз обратимся к пространству имен System. С человеческой точки зрения System. Console представляет класс по имени Console, который
72 Часть I. Общие сведения о языке С# и платформе .NET содержится внутри пространства имен под названием System. Но с точки зрения исполняющей .NET это не так. Механизм исполняющей среды видит только одну лишь сущность по имени System. Console. В С# ключевое слово using упрощает процесс добавления ссылок на типы, содержащиеся в определенном пространстве имен. Вот как оно работает. Предположим, что требуется создать графическое настольное приложение с использованием API-интерфейса Windows Forms. В главном окне этого приложения должна визуализироваться гистограмма с информацией, получаемой из базы данных, и отображаться логотип компании. Поскольку для изучения типов в каждом пространстве имен требуются время и силы, ниже показаны некоторые из возможных кандидатов на использование в такой программе: // Все пространства имен, которые можно использовать // для создания подобного приложения. using System; // Общие типы из библиотеки базовых классов. using System.Drawing; // Типы для визуализации графики. using System.Windows.Forms; // Типы для создания элементов пользовательского // интерфейса с помощью Windows Forms, using System.Data; // Общие типы для работы с данными, using System. Data. SqlClient; // Типы для доступа к данным MS SQL Server. После указания ряда необходимых пространств имен (и добавления ссылки на сборки, в которых они находятся), можно свободно создавать экземпляры типов, которые в них содержатся. Например, при желании создать экземпляр класса Bitmap (определенного в пространстве имен System. Drawing), можно написать следующий код: // Перечисляем используемые в данном файле // пространства имен явным образом. using System; using System.Drawing; class Program { public void DisplayLogo () { // Создаем растровое изображение // размером 20*20 пикселей. Bitmap companyLogo = new BitmapB0, 20); } } Благодаря импортированию в этом коде пространства имен System. Drawing, компилятор сможет определить, что класс Bitmap является членом данного пространства имен. Если пространство имен System. Drawing не указать, компилятор сообщит об ошибке. При желании переменные также можно объявлять с использованием полностью уточненного имени: II Пространство имен System.Drawing здесь не указано! using System; class Program { public void DisplayLogo () { // Используем полностью уточненное имя. System.Drawing.Bitmap companyLogo = new System.Drawing.BitmapB0, 20); } }
Глава 1. Философия .NET 73 Хотя определение типа с использованием полностью уточненного имени позволяет делать код более удобным для восприятия, трудно не согласиться с тем, что применение поддерживаемого в С# ключевого слова using, в свою очередь, позволяет значительно сократить количество печатаемых знаков. Поэтому в настоящей книге мы будем стараться избегать использования полностью уточненных имен (если только не будет возникать необходимости в устранении какой-то очевидной неоднозначности) и стремиться пользоваться более простым подходом, т.е. ключевым словом using. Важно помнить о том, что ключевое слово using является просто сокращенным способом указания полностью уточненного имени. Поэтому любой из этих подходов приводит к получению одного и того же CIL-кода (с учетом того факта, что в CIL-коде всегда применяются полностью уточненные имена) и не сказывается ни на производительности, ни на размере сборки. Добавление ссылок на внешние сборки Помимо указания пространства имен с помощью поддерживаемого в С# ключевого слова using, компилятору С# необходимо сообщить имя сборки, в которой содержится само CIL-onpeделение упоминаемого типа. Как уже отмечалось, многие из ключевых пространств имен .NET находятся внутри сборки mscorlib.dll. Класс System. Drawing. Bitmap, однако, содержится в отдельной сборке по имени System. Drawing, dll. Подавляющее большинство сборок в .NET Framework размещено в специально предназначенном для этого каталоге, который называется глобальным кэшем сборок (Global Assembly Cache — GAC). На машине Windows по умолчанию GAC может располагаться внутри каталога ?Qwindir%\Assembly, как показано на рис. 1.6. VT Favorite ■ Desktop Ц] Recent Places Л Libraries 0 Documents J* Music W Pictures Щ Videos «$ Homegroup '■ Computer f» Mongo Drive (C:) ^ CD Dnve (F:) Assembly Name j JUSystem.Data.SqlXml j sASystem.Deployment jfO System.Design db System.DirectoryServices i£i System. DirectoryServices-AccountManagement 3.5.0.0 #Ь System.DirectoryServtces.Protocois j iASyst em.Drawing.Design ж) System.EnterpriseServices 40 System.EnterpriseServices I lASystem.IdentityModel ЯХ System.ldentityModel.Selectors ifl System JO.Log dll System.Management Ж1 System. Management-Automatior Version Cut.., Public Key Token 2.0.0.0 2.0.0.0 20.0.0 2.0.0.0 3.5.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0.0 2.0.0,0 5.0.0.0 3.0,0.0 3.0.0,0 2.0 0.0 1.0.0.0 Ь77а5с561934е089 b036f7flld50a3a b03f5f7flld50a3a b03f5f7flld50a3a Ь77а5с5б1934е089 b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7flld50a3a Ь77а5с561934е089 Ь77а5с561934е089 b03f5r7flld50a3a b03f5f7flld50a3a 31bf3856ad364e35 Рис. 1.6. Многие библиотеки .NET размещены в GAC В зависимости от того, какое средство применяется для разработки приложений .NET, на выбор может оказываться доступными несколько различных способов для уведомления компилятора о том, какие сборки требуется включить в цикл компиляции. Эти способы подробно рассматриваются в следующей главе, а здесь их детали опущены. На заметку! С выходом версии .NET 4.0 в Microsoft решили выделить под сборки .NET 4.0 специальное место, находящееся отдельно от каталога С : \Windows\Assembly. Более подробно об этом будет рассказываться в главе 14.
74 Часть I. Общие сведения о языке С# и платформе .NET Изучение сборки с помощью утилиты ildasm. exe Тем, кого начинает беспокоить мысль о необходимости освоения всех пространств имен в .NET, следует просто вспомнить о том, что уникальным пространство имен делает то, что в нем содержатся типы, которые как-то связаны между собой с семантической точки зрения. Следовательно, если потребность в создании пользовательского интерфейса, более сложного, чем у простого консольного приложения, отсутствует, можно смело забыть о таких пространствах имен, как System. Windows . Forms, System.Windows и System.Web (и ряда других), а при создании приложений для рисования — о пространствах имен, которые касаются работы с базами данных. Как и в случае любого нового набора уже готового кода, опыт приходит с практикой. Утилита ildasm.exe (Intermediate Language Disassembler — дизассемблер промежуточного языка), которая поставляется в составе пакета .NET Framework 4.0 SDK, позволяет загружать любую сборку .NET и изучать ее содержимое, в том числе ассоциируемый с ней манифест, CIL-код и метаданные типов. По умолчанию эта утилита установлена в каталоге С: \Program Files\Microsoft SDKs\Windows\v7 . 0A\bin (если здесь ее нет, поищите на компьютере файл по имени ildasm.exe). На заметку! Утилиту ildasm.exe легко запустить, открыв в Visual Studio 2010 окно Command Prompt (Командная строка), введя в нем слово ildasm и нажав клавишу <Enter>. Р Calcexe-ILDASM Btc View Help la yy ЕЯ ► MANIFEST й Щ CakulatorExample f JE CakulatorExample.Calc ► .class public auto ansi beforefieUin* !•■ ■ .ctor: votd() ; ■ Add : int32(int32,int32) й £ Calculator Example. Program ► .class public auto ansi beforefieldinit ■ .ctor: voJd() U Main : void() После запуска этой утилиты нужно выбрать в меню File (Файл) команду Open (Открыть) и найти сборку, которую требуется изучить. Для целей иллюстрации здесь предполагается, что нужно изучить сборку Calc. exe, которая была сгенерирована на основе приведенного ранее в этой главе файла Calc. сs (рис. 1.7). Утилита ildasm.exe представляет структуру любой сборки в знакомом древовидном формате. Просмотр CIL-кода Рис. 1.7. Утилита ildasm.exe позволяет просматривать содержащийся внутри сборки .NET CIL-код, манифест и метаданные типов Помимо содержащихся в сборке пространств имен, типов и членов, утилита ildasm. exe также позволяет просматривать и CIL-инструкции, которые лежат в основе каждого конкретного члена. Например, в результате двойного щелчка на методе Main () в классе Program открывается отдельное окно с CIL-кодом, лежащим в основе этого метода, как показано на рис. 1.8. Просмотр метаданных типов Для просмотра метаданных типов, которые содержатся в загруженной в текущий момент сборке, необходимо нажать комбинацию клавиш <Ctrl+M>. На рис. 1.9 показаны метаданные метода Calc. Add (). Просмотр метаданных сборки (манифеста) И, наконец, чтобы просмотреть содержимое манифеста сборки, необходимо дважды щелкнуть на значке MANIFEST (рис. 1.10).
Глава 1. Философия .NET 75 f? Cakulatorbcampte.CalcApp~Main: voidQ J ( Find Find Ned .method priuate hidebysig static uoid Main() cil managed .entrypoint | // Code size Л2 <0x2a) И .maxstack 3 \ .locals init (class CalculatorExample.Calc и 0, int32 U 1) J IL ••••: nop ] IL 0001: newobj instance uoid CalculatorExample.Calc:: Щ IL tOM: stloc.i U IL 0007: ldloc.O J IL 0000: Idc.iij.s 10 J IL 000a: ldc.iU.s 84 1 IL 000c: calluirt instance int32 CalculatorExample.Calc: [ IL_0011: stloc.1 1" 1IS[ ШЗШ ^ pj 1 .ctor() 1 :Add(int32, int32) w\ Рис. 1.8. Просмотр лежащего в основе CIL-кода /7" Metalnfo ьь'щйш Method 01 @6000003) HethodName Flags RUfl ImplFlags CallCnuntn hasThis ReturnType 2 Arguments Argument 01: Argument 02: 2 Parameters A) ParamToken B) ParamToken Method 02 @6000004) Add @6000003) [Public] [HideBySig] [ReuseSlot] 0x00002090 [IL] [Managed] @0000000) [DEFAULT] 14 H @8000001) Mame : x Flags: [none] @0000 @8000002) Name : у Flags: [none] @0000 .ctor @6000004) Рис. 1.9. Просмотр метаданных типов с помощью ildasm.exe /7 MANIFEST Find Find Next // Metadata version: u4.0.3 0128 assembly extern mscorlib < i=| .publickeytoken - (B7 7A 5C 56 19 34 EO 89 ) .ver 4:8:0:0 — } .assembly Calc < .custom instance uoid [mscorlib]System.Runtime.CompilerSeruice< .custom instance uoid [mscorlib]System.Runtime.CompilerSeruice* - Рис. 1.10. Просмотр данных манифеста с помощью ildasm.exe Несомненно, утилита ildasm. ехе обладает большим, чем было показано здесь количеством функциональных возможностей; все остальные функциональные возможности этой утилиты будут демонстрироваться позже в книге при рассмотрении соответствующих аспектов. Изучение сборки с помощью утилиты Reflector Хотя утилита ildasm.exe и применяется очень часто для просмотра деталей двоичного файла .NET, одним из ее недостатков является то, что она позволяет просматривать только лежащий в основе CIL-код, но не реализацию сборки с использованием
76 Часть I. Общие сведения о языке С# и платформе .NET предпочитаемого управляемого языка. К счастью, в Интернете для загрузки доступно множество других утилит для просмотра и декомпиляции объектов .NET, в том числе и популярная утилита Reflector. Эта утилита распространяется бесплатно и доступна по адресу http://www. red-gate.com/products/reflector. После распаковки из ZIP-архива ее можно запускать и подключать к любой представляющей интерес сборке, выбирая в меню File (Файл) команду Open (Открыть). На рис. 1.11 показано ее применение на примере приложения Calc.exe. Рис. 1.11. Утилита Reflector является очень популярной программой для просмотра объектов Важно обратить внимание на то, что в утилите reflector.exe поддерживается окно Dissembler (Дизассемблер), которое можно открыть нажатием клавиши пробела, а также элемент раскрывающегося списка, который позволяет просматривать лежащую в основе кодовую базу на желаемом языке (разумеется, в том числе и на CIL). Остальные интригующие функциональные возможности этой утилиты предлагается изучить самостоятельно. На заметку! Следует иметь в виду, что в остальной части настоящей книги для иллюстрации различных концепций будет применяться как утилита ildasm.exe, так и утилита reflector.exe. Поэтому загрузите утилиту Reflector, если это еще не было сделано. Развертывание исполняющей среды .NET Нетрудно догадаться, что сборки .NET могут выполняться только на той машине, на которой установлена платформа .NET Framework. Для разработчиков программного обеспечения .NET это не должно оказываться проблемой, поскольку их машина надлежащим образом конфигурируется еще во время установки распространяемого бесплатно пакета NET Framework 4.0 SDK (а также таких коммерческих сред для разработки .NET-приложений, как Visual Studio 2010). В случае развертывания сборки на компьютере, на котором платформа .NET не была установлена, сборка запускаться не будет. Для таких ситуаций Microsoft предлагает специальный установочный пакет dotNetFx4 0_Full_x8 6 .exe, который может бесплатно
Глава 1. Философия .NET 77 поставляться и устанавливаться вместе со специальным программным обеспечением. Этот пакет доступен для загрузки на сайте Microsoft в общем разделе загружаемых продуктов (http: //www.microsoft. com/downloads). После установки пакета dotNetFx40_Full_x86.exe на целевой машине появятся необходимые библиотеки базовых классов .NET, исполняющая среда .NET (mscoree.dll) и дополнительная инфраструктура .NET (такая как GAC). На заметку! Операционные системы Windows Vista и Windows 7 изначально сконфигурированы с необходимой инфраструктурой исполняющей среды .NET. В случае развертывания приложения в среде какой-то другой операционной системы производства Microsoft, например, Windows XP, нужно будет позаботиться об установке и настройке на целевой машине среды .NET. Клиентский профиль исполняющей среды .NET Установочная программа dotNetFx4 0_Full_x8 6.exe имеет объем примерно 77 Мбайт. Если для конечного пользователя обеспечивается возможность развертывать приложение с компакт-диска, это не будет представлять проблемы, поскольку установочная программа сможет просто запускать исполняемый файл тогда, когда машина не сконфигурирована надлежащим образом. Если пользователь должен будет загружать dotNetFx4 0_Full_x8 6 .exe по медленному соединению с Интернетом, ситуация несколько усложняется. Для разрешения подобной проблемы в Microsoft разработали альтернативную установочную программу — так называемый клиентский профиль (dotNetFx40_Client_x8 6.exe), который тоже доступен для бесплатной загрузки на сайте Microsoft. Эта установочная программа, как не трудно догадаться по ее названию, предусматривает выполнение установки подмножества библиотек базовых классов .NET в дополнение к необходимой инфраструктуре исполняющей среды. Поскольку она имеет гораздо меньший размер (примерно 34 Мбайт), установку тех же самых библиотек, что появляются при полной установке .NET, на целевой машине она не обеспечивает. При желании не охватываемые ею сборки могут быть добавлены на целевую машину при выполнении пользователем обновления Windows (с помощью службы Windows Update). На заметку! Как у полного, так и у клиентского профиля исполняющей среды имеются 64-разрядные аналоги, которые называются, соответственно, dotNetFx40_Full_x86_x64.exe и dotNetFx40 Client x86 x64.exe. Не зависящая от платформы природа .NET В завершение настоящей главы хотелось бы сказать несколько слов о не зависящей от платформы природе .NET. К удивлению большинства разработчиков, сборки .NET могут разрабатываться и выполняться в средах операционных систем производства не Microsoft, в частности — в Mac OS X, различных дистрибутивах Linux, Solaris, а также на устройствах типа iPhone производства Apple (через API-интерфейс MonoTbuch). Чтобы понять, что делает подобное возможным, необходимо рассмотреть еще одну используемую в мире .NET аббревиатуру — CLI, которая расшифровывается как Common Language Infrastructure (Общеязыковая инфраструктура). Вместе с языком программирования С# и платформой .NET в Microsoft был также разработан набор официальных документов с описанием синтаксиса и семантики языков С# и CIL, формата сборок .NET, ключевых пространств имен и технических деталей работы гипотетического механизма исполняющей среды .NET (названного виртуальной системой выполнения — Virtual Execution System (VES)).
78 Часть I. Общие сведения о языке С# и платформе .NET Все эти документы были поданы в организацию Ecma International (http: //www. ecma-international.org) и утверждены в качестве официальных международных стандартов. Среди них наибольший интерес представляют: • документ ЕСМА-334, в котором содержится спецификация языка С#; • документ ЕСМА-335, в котором содержится спецификация общеязыковой инфраструктуры (CLI). Важность этих документов становится очевидной с пониманием того факта, что они предоставляют третьим сторонам возможность создавать дистрибутивы платформы .NET для любого количества операционных систем и/или процессоров. Среди этих двух спецификаций документ ЕСМА-335 является более "объемным", причем настолько, что был разбит на шесть разделов, которые перечислены в табл. 1.4. Таблица 1.4. Разделы спецификации CLI Разделы документа п ЕСМА-335 Предназначение Раздел I. Концепции В этом разделе описана общая архитектура CLI, в том числе правила и архитектура CTS и CLS и технические детали функционирования механизма среды выполнения .NET Раздел II. Определение В этом разделе описаны детали метаданных и формат сборок в .NET метаданных и семантика Раздел III. Набор В этом разделе описан синтаксис и семантика кода CIL инструкций CIL Раздел IV. Профили В этом разделе дается общий обзор тех минимальных и полных биб- и библиотеки лиотек классов, которые должны поддерживаться в дистрибутиве .NET Раздел V. В этом разделе описан формат обмена деталями отладки Раздел VI. Дополнения В этом разделе представлена коллекция дополнительных и более конкретных деталей, таких как указания по проектированию библиотек классов и детали по реализации компилятора CIL Следует иметь в виду, что в разделе IV (Профили и библиотеки) описан лишь минимальный набор пространств имен, в которых содержатся ожидаемые от дистрибутива CLI службы (наподобие коллекций, консольного ввода-вывода, файлового ввода-вывода, многопоточной обработки, рефлексии, сетевого доступа, ключевых средств защиты и возможностей для манипулирования XML-данными). Пространства имен, которые упрощают разработку веб-приложений (ASP.NET), доступ к базам данных (ADO.NET) и создание настольных приложений с графическим пользовательским интерфейсом (Windows Forms /Windows Presentation Foundation) в CLI не описаны. Хорошая новость состоит в том, что в главных дистрибутивах .NET библиотеки CLI дополняются совместимыми с Microsoft эквивалентами ASP.NET, ADO.NET и Windows Forms, чтобы предоставлять полнофункциональные платформы для разработки приложений производственного уровня. На сегодняшний день популярностью пользуются две основных реализации CLI (помимо самого предлагаемого Microsoft и рассчитанного на Windows решения). Хотя настоящая книга и посвящена главным образом созданию .NET-приложений с помощью поставляемого Microsoft дистрибутива .NET, в табл. 1.5 приведена краткая информация касательно проектов Mono и Portable.NET.
Глава 1. Философия .NET 79 Таблица 1.5. Дистрибутивы .NET, распространяемые с открытым исходным кодом Дистрибутив Описание http: / /www. mono-pro j ect. com Проект Mono представляет собой распространяемый с открытым исходным кодом дистрибутив CLI, который ориентирован на различные версии Linux (например, openSuSE, Fedora и т.п.), а также Windows и устройства Mac OS X и iPhone http: //wwwmdotgnu. org Проект Portable.NET представляет собой еще один распространяемый с открытым исходным кодом дистрибутив CLI, который может работать в целом ряде операционных систем. Он нацелен охватывать как можно больше операционных систем (Windows, AIX, BeOS, Mac OS X, Solaris и все главные дистрибутивы Linux) Как в Mono, так и в Portable.NET предоставляется ЕСМА-совместимый компилятор С#, механизм исполняющей среды .NET, примеры программного кода, документация, а также многочисленные инструменты для разработки приложений, которые по своим функциональным возможностям эквивалентны поставляемым в составе .NET Framework 4.0 SDK. Более того, Mono и Portable.NET поставляются с компиляторами VB.NET, Java и С. На заметку! Описание приемов создания межплатформенных .NET-приложений с помощью Mono можно найти в приложении Б. Резюме Целью этой главы было предоставить базовые теоретические сведения, необходимые для изучения остального материала настоящей книги. Сначала были рассмотрены ограничения и сложности, которые существовали в технологиях, предшествовавших появлению .NET, а потом показано, как .NET и С# упрощают существующее положение вещей. Главную роль в .NET, по сути, играет механизм выполнения (mscoree . dll) и библиотека базовых классов (mscorlib.dll вместе с сопутствующими файлами). Общеязыковая среда выполнения (CLR) способна обслуживать любой двоичный файл .NET (сборку), который отвечает правилам управляемого программного кода. Как было показано в этой главе, в каждой сборке (помимо метаданных типов и манифеста) содержатся CIL- инструкции, которые с помощью JIT-компилятора преобразуются в инструкции, ориентированные на конкретную платформу. Помимо этого, здесь рассматривалась роль общеязыковой спецификации (CLS) и общей системы типов (CTS). После этого было рассказано о таких полезных утилитах для просмотра объектов, как ildasm.exe и reflector.exe, а также о том, как сконфигурировать машину для обслуживания приложений .NET с помощью полного и клиентского профилей. И, наконец, напоследок было вкратце упомянуто о преимуществах не зависящей от платформы природы С# и .NET, о чем более подробно пойдет речь в приложении Б.
ГЛАВА 2 Создание приложений на языке С# Программисту, использующему язык С#, для разработки .NET-приложений на выбор доступно много инструментов. Целью этой главы является совершение краткого обзорного тура по различным доступным средствам для разработки .NET- приложений, в том числе, конечно же, Visual Studio 2010. Глава начинается с рассказа о том, как работать с компилятором командной строки С# (esc. exe), и самым простейшим из всех текстовых редакторов Notepad (Блокнот), который входит в состав операционной системы Microsoft Windows, а также приложением Notepad++, доступным для бесплатной загрузки. Хотя для изучения приведенного в настоящей книге материала вполне хватило бы компилятора csc.exe и простейшего текстового редактора, читателя наверняка заинтересует использование многофункциональных интегрированных сред разработки (Integrated Development Environment — IDE). В этой главе также описана бесплатная IDE-среда с открытым исходным кодом SharpDevelop, предназначенная для разработки приложений .NET. Как будет показано, по своим функциональным возможностям эта IDE-среда не уступает многим коммерческим аналогам. Кроме того, в главе кратко рассматривается IDE-среда Visual C# 2010 Express (распространяемая бесплатно), а также ключевая функциональность Visual Studio 2010. На заметку! В ходе этой главы будут встречаться синтаксические конструкции С#, которые пока еще не рассматривались. Официальное изучение языка С# начнется в главе 3 Роль комплекта .NET Framework 4.0 SDK Одним из мифов в области разработки .NET-приложений является то, что программистам якобы обязательно требуется приобретать копию Visual Studio для того, чтобы разрабатывать программы на С#. На самом деле, создавать .NET-программу любого рода можно с помощью распространяемого бесплатно и доступного для загрузки комплекта инструментов для разработки программного обеспечения .NET Framework 4.0 SDK (Software Development Kit). В этом пакете поставляются многочисленные управляемые компиляторы, утилиты командной строки, примеры кода, библиотеки классов .NET и полная справочная система. На заметку! Программа установки .NET Framework 4.0 SDK (dotnetf x4 Of ullsetup. exe) доступна на странице загрузки .NET по адресу http: //msdn.microsoft.com/netframework.
Глава 2. Создание приложений на языке С# 81 Тем, кто планирует использовать Visual Studio 2010 или Visual C# 2010 Express, следует иметь в виду, что в установке .NET Framework 4.0 SDK нет никакой необходимости. При установке любого из упомянутых продуктов этот пакет SDK устанавливается автоматически и сразу же предоставляет все необходимое. Если использование IDE-среды от Microsoft для проработки материала настоящей книги не планируется, обязательно установите .NET Framework 4.0 SDK, прежде чем двигаться дальше. Окно командной строки в Visual Studio 2010 При установке .NET Framework 4.0 SDK, Visual Studio 2010 или Visual C# 2010 Express на локальном жестком диске создается набор новых каталогов, в каждом из которых содержатся разнообразные инструменты для разработки .NET-приложений. Многие из этих инструментов работают в режиме командной строки, и чтобы использовать их в любом каталоге, нужно сначала соответствующим образом зарегистрировать пути к ним в операционной системе. Для этого можно обновить переменную среды PATH вручную, но лучше пользоваться предлагаемым в Visual Studio окном командной строки (Command Prompt). Чтобы открыть это окно (рис. 2.1), необходимо выбрать в меню Start (Пуск) пункт All Programs^ Microsoft Visual Studio 2010^Visual Studio Tools (Все программы^ Microsoft Visual Studio 20Ю1^Инструменты Visual Studio). [Setting environment for using Microsoft Visual Studio 2010 x86 tools. C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC> Рис. 2.1. Окно командной строки в Visual Studio 2010 Преимущество применения именно этого окна командной строки связано с тем, что оно уже сконфигурировано на предоставление доступа к каждому из инструментов для разработки .NET-приложений. При условии, что на компьютере развернута среда разработки .NET, можно попробовать ввести следующую команду и нажать клавишу <Enter>: esc -? Если все в порядке, появится список аргументов командной строки, которые может принимать работающий в режиме командной строки компилятор С# (esc означает C-sharp compiler]. Создание приложений на С# с использованием esc. ехе В действительности необходимость в создании крупных приложений с использованием одного лишь компилятора командной строки С# может никогда не возникнуть, тем не менее, важно понимать в общем, как вручную компилировать файлы кода. Существует несколько причин, по которым освоение этого процесса может оказаться полезным.
Часть I. Общие сведения о языке С# и платформе .NET Самой очевидной причиной является отсутствие Visual Studio 2010 или какой-то другой графической IDE-среды. Работа может выполняться в университете, где использование инструментов для генерации кода и IDE-сред обычно запрещено. Планируется применение автоматизированных средств разработки, таких как msbuild. ехе, которые требуют знать опции командной строки для используемых инструментов. Возникло желание углубить свои познания в С#. В графических IDE-средах в конечном итоге все заканчивается предоставлением компилятору esc. ехе инструкций относительно того, что следует делать с входными файлами кода С#. В этом отношении изучение происходящего "за кулисами'' позволяет получить необходимые знания. Еще одно преимущество подхода с использованием одного лишь компилятора esc. ехе состоит в том, что он позволяет обрести навыки и чувствовать себя более уверенно при работе с другими инструментами командной строки, входящими в состав .NET Framework 4.0 SDK. Как будет показано далее, целый ряд важных утилит работает исключительно в режиме командной строки. Чтобы посмотреть, как создавать .NET-приложение без IDE-среды, давайте построим с помощью компилятора С# и текстового редактора Notepad простую исполняемую сборку по имени TestApp.exe. Сначала необходимо подготовить исходный код. Откройте программу Notepad (Блокнот), выбрав в меню Start (Пуск) пункт All Programs1^ Accessories1^ Notepad (Все программы ^Стандартные ^Блокнот), и введите следующее типичное определение класса на С#: // Простое приложение на языке С#. using System; class TestApp { static void Main() { Console.WriteLine ("Testing! 1, 2, 3"); } } После окончания ввода сохраните файл (например, в каталоге С: \CscExample) под именем TestApp. cs. Теперь давайте ознакомимся с ключевыми опциями компилятора С#. На заметку! По соглашению всем файлам с кодом на С# назначается расширение * .cs. Имя файла не нуждается в специальном отображении на имя какого-либо типа или типов. Указание целевых входных и выходных параметров Первым делом важно разобраться с тем, как указывать имя и тип создаваемой сборки (т.е., например, консольное приложение по имени MyShell.exe, библиотека кода по имени MathLib.dll или приложение Windows Presentation Foundation по имени Halo8.exe). Каждый из возможных вариантов имеет соответствующий флаг, который нужно передать компилятору esc. ехе в виде параметра командной строки (табл. 2.1). На заметку! Параметры, передаваемые компилятору командной строки (а также большинству других утилит командной строки), могут сопровождаться префиксом в виде символа дефиса (-) или символа косой черты (/).
Глава 2. Создание приложений на языке С# 83 Таблица 2.1. Выходные параметры, которые может принимать компилятор С# Параметр Описание /out /target:exe /target:library /targetrmodule /target:winexe Этот параметр применяется для указания имени создаваемой сборки По умолчанию сборке присваивается то же имя, что у входного файла *. cs Этот параметр позволяет создавать исполняемое консольное приложение. Сборка такого типа генерируется по умолчанию, потому при создании подобного приложения данный параметр можно опускать Этот параметр позволяет создавать однофайловую сборку * . dll Этот параметр позволяет создавать модуль. Модули являются элементами многофайловых сборок (и будут более подробно рассматриваться в главе 14) Хотя приложения с графическим пользовательским интерфейсом можно создавать с применением параметра /target: exe, параметр /target: winexe позволяет предотвратить открытие окна консоли под остальными окнами Чтобы скомпилировать TestApp. cs в консольное приложение TextApp.exe, перейдите в каталог, в котором был сохранен файл исходного кода: cd C:\CscExample Введите следующую команду (обратите внимание, что флаги должны обязательно идти перед именем входных файлов, а не после): esc /target:exe TestApp.cs Здесь флаг /out не был указан явным образом, поэтому исполняемый файл получит имя TestApp. exe из-за того, что именем входного файла является TestApp. Кроме того, для почти всех принимаемых компилятором С# флагов поддерживаются сокращенные версии написания, наподобие /t вместо /target (полный список которых можно увидеть, введя в командной строке команду esc -?). esc /t:exe TestApp.cs Более того, поскольку флаг /t: exe используется компилятором как выходной параметр по умолчанию, скомпилировать TestApp. cs также можно с помощью следующей простой команды: esc TestApp.cs Теперь можно попробовать запустить приложение TestApp . exe из командной строки, введя имя его исполняемого файла, как показано на рис. 2.2. J Administrator Visual Studio Command Prompt '■*»«*' *'*" »■ ■-, f- t: \CscExamp1 e>TestApp.exe [Testing! 1, 2, 3 ]C:\CscExamp1e> i Рис. 2.2. Приложение TestApp в действии
84 Часть I. Общие сведения о языке С# и платформе .NET Добавление ссылок на внешние сборки Давайте посмотрим, как скомпилировать приложение, в котором используются типы, определенные в отдельной сборке .NET. Если осталось неясным, каким образом компилятору С# удалось понять ссылку на тип System. Console, вспомните из главы 1, что во время процесса компиляции происходит автоматическое добавление ссылки на mscorlib.dll (если по какой-то необычной причине нужно отключить эту функцию, следует передать компилятору csc.exe параметр /nostdlib). Модифицируем приложение TestApp так, чтобы в нем открывалось окно сообщения Windows Forms. Для этого откройте файл TestApp. cs и измените его следующим образом: using System; // Добавить эту строку: using System.Windows.Forms; class TestApp { static void Main() { Console.WriteLine("Testing! 1, 2, 3"); // И добавить эту строку: MessageBox.Show("Hello..."); Рис. 2.3. Первое приложение Windows Forms } Обратите внимание на импорт пространства имен System. Windows .Forms с помощью поддерживаемого в С# ключевого слова using (о котором рассказывалось в главе 1). Вспомните, что явное перечисление пространств имен, которые используются внутри файла * .cs, позволяет избегать необходимости указывать полностью уточненные имена типов. Далее в командной строке нужно проинформировать компилятор esc. ехе о том, в какой сборке содержатся используемые пространства имен. Поскольку применялся класс MessageBox из пространства имен System. Windows . Forms, значит, нужно указать компилятору на сборку System.Windows .Forms .dll, что делается с помощью флага /reference (или его сокращенной версии /г): esc /r:System.Windows.Forms.dll TestApp.cs Если теперь снова попробовать запустить приложение, то помимо консольного вывода в нем должно появиться еще и окно с сообщением, как показано на рис. 2.3. Добавление ссылок на несколько внешних сборок Кстати, как поступить, когда необходимо указать esc. ехе несколько внешних сборок? Для этого нужно просто перечислить все сборки через точку с запятой. В рассматриваемом примере ссылаться на несколько сборок не требуется, но ниже приведена команда, которая иллюстрирует перечисление множества сборок: esc /r: System. Windows . Forms ,dll;System. Drawmg.dll *.cs На заметку! Как будет показано позже в настоящей главе, компилятор С# автоматически добавляет ссылки на ряд ключевых сборок .NET (таких как System. Windows . Forms . dll), даже если они не указаны с помощью флага /г.
Глава 2. Создание приложений на языке С# 85 Компиляция нескольких файлов исходного кода В текущем примере приложение TestApp. exe создавалось с использованием единственного файла исходного кода * . cs. Хотя определять все типы .NET в одном файле * . cs вполне допустимо, в большинстве случаев проекты формируются из нескольких файлов *. cs для придания кодовой базе большей гибкости. Чтобы стало понятнее, давайте создадим новый класс и сохраним его в отдельном файле по имени HelloMsg. cs. // Класс HelloMessage using System; using System.Windows.Forms; class HelloMessage { public void Speak() { MessageBox.Show("Hello..."); } } Изменим исходный класс TestApp так, чтобы в нем использовался класс этого нового типа, и закомментируем прежнюю логику Windows Forms: using System; // Эта строка больше не нужна: // using System.Windows.Forms; class TestApp { static void Mam () { Console .WriteLme ("Testing ' 1, 2, 3"); // Эта строка тоже больше не нужна: // MessageBox.Show("Hello..."); // Используем класс HelloMessage: HelloMessage h = new HelloMessage(); h. Speak () ; } } Чтобы скомпилировать файлы исходного кода на С# , необходимо их явно перечислить как входные файлы: esc /r:System.Windows.Forms.dll TestApp.cs HelloMsg.cs В качестве альтернативного варианта компилятор С# позволяет использовать групповой символ (*) для включения в текущую сборку всех файлов * .cs, которые содержатся в каталоге проекта: esc /г:System.Windows.Forms.dll *.cs Вывод, получаемый после запуска этой программы, идентичен предыдущей программе. Единственное отличие между этими двумя приложениями связано с разнесением логики по нескольким файлам. Работа с ответными файлами в С# Как не трудно догадаться, для создания сложного приложения С# из командной строки потребовалось бы вводить утомительное количество входных параметров для уведомления компилятора о том, как он должен обрабатывать исходный код. Для облег-
86 Часть I. Общие сведения о языке С# и платформе .NET чения этой задачи в компиляторе С# поддерживается использование так называемых ответных файлов (response files). В ответных файлах С# размещаются все инструкции, которые должны использоваться в процессе компиляции текущей сборки. По соглашению эти файлы имеют расширение * . rsp (сокращение от response— ответ). Чтобы посмотреть на них в действии, давайте создадим ответный файл по имени TestApp. rsp, содержа ищи следующие аргументы (комментарии в данном случае обозначаются символом #): # Это ответный файл для примера # TestApp.exe из главы 2. # Ссылки на внешние сборки: /г:System.Windows.Forms.dll # Параметры вывода и подлежащие компиляции файлы # (здесь используется групповой символ): /target:exe /out:TestApp.exe *.cs Теперь при условии сохранения данного файла в том же каталоге, где находятся подлежащие компиляции файлы исходного кода на С#, все приложение можно будет создать следующим образом (обратите внимание на применение символа @): esc @TestApp.rsp В случае необходимости допускается также указывать и несколько ответных * . rsp файлов в качестве входных параметров (например, esc @FirstFile.rsp @SecondFile . rsp @ThirdFile . rsp). При таком подходе, однако, следует иметь в виду, что компилятор обрабатывает параметры команд по мере их поступления. Следовательно, аргументы командной строки, содержащиеся в поступающем позже файле * . rsp, могут переопределять параметры из предыдущего ответного файла. Еще важно обратить внимание на то, что все флаги, перечисляемые явным образом перед ответным файлом, будут переопределяться настройками, которые содержатся в этом файле. То есть в случае ввода следующей команды: esc /outiMyCoolApp.exe @TestApp.rsp имя сборки будет по-прежнему выглядеть как TestApp. exe (а не MyCoolApp. exe) из-за того, что в ответном файле TestApp. rsp содержится флаг /out: TestApp. exe. В случае перечисления флагов после ответного файла они будут переопределять настройки, содержащиеся в этом файле. На заметку! Действие флага /reference является кумулятивным. Где бы не указывались внешние сборки (перед, после или внутри ответного файла), в конечном итоге каждая из них все равно будет добавляться к остальным. Используемый по умолчанию ответный файл (esc. rsp) Последним моментом, связанным с ответными файлами, о котором необходимо упомянуть, является то, что с компилятором С# ассоциирован ответный файл esc. rsp, который используется по умолчанию и размещен в том же самом каталоге, что и файл esc .exe (обычно это С: \Windows\Microsof t .NET\Framework\<BepcMH>, где на месте элемента <версия> идет номер конкретной версии платформы). Открыв файл esc. rsp в программе Notepad (Блокнот), можно увидеть, что в нем с помощью флага /г: указано множество сборок .NET, в том числе различные библиотеки для разработки веб-приложений, программирования с использованием технологии LINQ и обеспечения доступа к данным и прочие ключевые библиотеки (помимо, конечно же, самой главной библиотеки mscorlib.dll).
Глава 2. Создание приложений на языке С# 87 При создании программ на С# с применением с~.с . ехе ссылка на этот ответный файл добавляется автоматически, даже когда указан специальный файл * . rsp. Из-за наличия такого ответного файла по умолчанию, рассматриваемое приложение TestApp. ехе можно скомпилировать и помощью следующей команды (поскольку в esc . rsp уже содержится ссылка на System. Windows . Forms . dll): esc /out: TestApp .ехе *.cr. Для отключения функции автоматического чтения файла esc. rsp укажите опцию /noconfig: esc @TestApp.rsp /noconfig На заметку! В случае добавления (с помощью опции /г) ссылок на сборки, которые на самом деле не используются, компилятор их проигнорирует Поэтому беспокоиться по поводу "разбухания кода" не нужно. Понятно, что у компилятора командной строки С# имеется множество других параметров, которые можно применять для управления генерацией результирующей сборки .NET. Другие важные возможности будут демонстрироваться по мере необходимости далее в книге, а полные сведения об этих параметрах можно всегда найти в документации .NET Framework 4.0 SDK Исходный код. Код приложения CscExample доступен в подкаталоге Chapter 2. Создание приложений .NET с использованием Notepad++ Еще одним текстовым редактором, о котором следует кратко упомянуть, является распространяемое с открытым исходным кодом бесплатное приложение Notepad++. Загрузить его можно по адресу http: //notepad-plus . sourceforge .net/. В отличие от простого редактора Notepad (Блокнот), поставляемого в составе Windows, приложение Notepad++ позволяет создавать код на множестве различных языков и поддерживает установку разнообразных дополнительных подключаемых модулей. Помимо этого, Notepad++ обладает рядом других замечательных достоинств, в том числе: • изначальной поддержкой для использования ключевых слов С# (и их кодирования цветом включительно); • поддержкой для свертывания синтаксиса (syntax folding), позволяющей сворачивать и разворачивать группы операторов в коде внутри редактора (и подобной той, что предлагается в Visual Studio 2010/C# 2010 Express); • возможностью увеличивать и уменьшать масштаб отображения текста с помощью колесика мыши (имитирующего действие клавиши <Ctrl>); • настраиваемой функцией автоматического завершения (autocompletion) различных ключевых слов С# и названий пространств имен .NET. Чтобы активизировать поддержку функции автоматического завершения кода на С# (рис. 2.4), необходимо одновременно нажать клавиши <Ctrl> и пробела. На заметку! Список вариантов, предлагаемых для автоматического завершения кода в отображаемом окне, можно изменять и расширять. Для этого необходимо открыть файл С: \Program Files\Notepad++\plugms\APIs\cs . xml для редактирования и добавить в него любые дополнительные записи.
88 Часть I. Общие сведения о языке С# и платформе .NET t:\CscE.4antpleNHellcMjg.« - Ncte^d - ♦ ' File Edit Search View Format Language Settings Macro Run TextFX Plugins Window ? o..|H^oe&l4'tufcl*ci|#lk|4t.|- s 1. 1 .. ♦ Ы HetoMag cs | i!li|X>< • // Tbe HelloMessage class using System; using System.Windows.Forms; using clas; 3< й с System.Web.Configuration System.Web.Hosting System.Web.Mail I System.Web.Services C* 148 chars 172 bytes 12 lines Ln:4 Col: 7 Sel: 0 @ bytes) in 0 ranges Dos\Windows ANSI Рис. 2.4. Использование функции автоматического завершения кода в Notepad++ Более подробно о приложении Notepad++ здесь рассказываться не будет. Чтобы узнать о нем больше, воспользуйтесь предлагаемым в его меню ? пунктом Help (Справка). Создание приложений.NET с помощью SharpDevelop Нельзя не согласиться с тем, что написание кода С# в приложении Notepad++, несомненно, является шагом в правильном направлении по сравнению с использованием редактора Notepad (Блокнот) и командной строки. Тем не менее, в Notepad++ отсутствуют богатые возможности IntelliSense, визуальные конструкторы для построения графических пользовательских интерфейсов, шаблоны проектов, инструменты для работы с базами данных и многое другое. Для удовлетворения перечисленных потребностей больше подходит такой рассматриваемый далее продукт для разработки .NET-приложений, как SharpDevelop (также называемый #Develop). Продукт SharpDevelop представляет собой распространяемую с открытым исходным кодом многофункциональную IDE-среду, которую можно применять для создания .NET- сборок с помощью С# ,VB, CIL, а также Python-образного .NET-языка под названием Boo. Помимо того, что эта IDE-среда предлагается совершенно бесплатно, интересно обратить внимание на тот факт, что она сама реализована полностью на С#. Для установки среды SharpDevelop необходимо загрузить и скомпилировать ее файлы * . cs вручную или запустить готовую программу setup. ехе. Оба дистрибутива доступны по адресу http://www.sharpdevelop.com/. IDE-среда SharpDevelop обладает массой достоинств в плане улучшения продуктивности. Наиболее важными из них являются: • поддержка для множества языков и типов проектов .NET; • функция IntelliSense, завершение кода и возможность использования только определенных фрагментов кода; • диалоговое окно Add Reference (Добавление ссылки), позволяющее легко добавлять ссылки на внешние сборки, в том числе и те, что находятся в глобальном кэше сборок (Global Assembly Cache — GAC); • визуальный конструктор Windows Forms; • встроенные утилиты для просмотра объектов и определения кода; • визуальные утилиты для проектирования баз данных; • утилита для преобразования кода на С# в код на VB (и наоборот).
Глава 2. Создание приложений на языке С# 89 Впечатляюще для бесплатной IDE-среды, не так ли? Ниже кратко рассматриваются некоторые наиболее интересные достоинства из перечисленных выше. На заметку! На момент написания книги в текущей версии SharpDevelop пока не поддерживались средства С# 2010 / .NET 4.0. Периодически заглядывайте на веб-сайт SharpDevelop, чтобы проверить, не появились ли следующие выпуски среды. Создание простого тестового проекта После установки SharpDevelop за счет выбора в меню File (Файл) пункта New^Solution (Создать1^ Решение) можно указывать, какой тип проекта требуется сгенерировать (и на каком языке .NET). Например, предположим, что нужно создать проект по имени MySDWinApp типа Windows Application (Приложение Windows) на языке С# (рис. 2.5). Рис. 2.5. Диалоговое окно создания нового проекта в SharpDevelop Как и в Visual Studio, в SharpDevelop предлагается окно элементов управления для конструктора графических пользовательских интерфейсов Windows Forms (позволяющее перетаскивать элементы управления на поверхность конструктора) и окно Properties (Свойства), позволяющее настраивать внешний вид и поведение каждого из элементов графического пользовательского интерфейса. На рис. 2.6 показан пример настройки элемента управления Button (Кнопка); обратите внимание, что для этого был выполнен щелчок на вкладке Design (Конструктор), отображаемой в нижней части открытого файла кода. После щелчка на вкладке Source (Исходный код) в нижней части окна конструктора форм, как не трудно догадаться, будет предлагаться функция IntelliSense, функция завершения кода и встроенная справка (рис. 2.7). В дизайне SharpDevelop имитируются многие функциональные возможности, которые предоставляются в IDE-средах .NET производства Microsoft (и о которых пойдет речь далее). Поэтому более подробно здесь эта IDE-среда SharpDevelop рассматриваться не будет. Для получения дополнительной информации можно воспользоваться меню Help (Справка).
90 Часть I. Общие сведения о языке С# и платформе .NET ) MySDWinApp - SharpDevelop шшш £ile £drt tfew Rroject Debug Search Fojmat Tools Window Help *? C* : Ш Jtt ■ ► ! I Default layout - J Tools "ft Pointer | |Э Button pCheckBcw FfComboBox Ia Label l0Ra<*©Button _W TextBox pSCheckedUstBox И]$ DateTimePicker . DomainUpDown Q FlowLayoutPenel ^.(GfOupBcor П HScrollBsr Д LinkLabeJ Data Ж Custom Con [53 Projects~|JJjTools I ^йЫНхтл? I f | * MySDWmApp ШШ1 Sour» | Desen (Output v Debug ~"*[ll" ;; button 1 System Windows Forms Button i|ELi|E.,r 1Л |f ImageAfcon MddtoCenter Imagehdex fane) ; knegeKey fione) Imagebst (none) Д X FlightToLeft No j fiSHHHHHI Click Me! [*1 •' i Textron MiddleCenter : The text associated w4h the control. ^ Errors [I] Output pTTaskLirt |ШDefinition View | l^j*Properties f^Classes| Inl coll chl Рис. 2.6. Конструирование приложения типа Windows Forms графическим образом в SharpDevelop •^MySDWinApp MamForm v ♦MainFormi) ; ) // 1000: Add constructor cede after the InitializeCqrrpc // this.I 'АНолТгагврагегсу 'Anchor ^♦ApplyAutoScaling "•AutoScale 'AutoScaieBaseSze 'AutoScaleDimensiors 'AutoScaJeF actor [public vlrtuaTFool AutoScrolf" iets or sets a value indicating whether the form enables autoscrolling leturns: true to enable autoscroinq on the form; otherwise, false The default is false. Source fbeegn Рис. 2.7. В SharpDevelop поддерживается много утилит для генерации кода Создание приложений .NET с использованием Visual С# 2010 Express Летом 2004 г. компания Microsoft представила совершенно новую линейку IDE-сред по общим названием "Express" (http: //mscin.microsoft.com/express). На сегодняшний день на рынке предлагается несколько членов этого семейства (все они распространяются бесплатно и поддерживаются и обслуживаются компанией Microsoft). • Visual Web Developer 2010 Express. "Облегченный" инструмент для разработки динамических веб-сайтов ASP.NET и служб WCF. • Visual Basic 2010 Express. Упрощенный инструмент для программирования, идеально подходящий для программистов-новичков, которые хотят научиться создавать приложения с применением дружественного к пользователям синтаксиса Visual Basic.
Глава 2. Создание приложений на языке С# 91 • Visual C# 2010 IZxpress и Visual C++ 2010 Express. IDE-среды, ориентированные специально на студентов и всех прочих желающих обучиться основам программирования с использованием предпочитаемого синтаксиса. • SQL Server Express. Система для управления базами данных начального уровня, предназначенная для любителей, энтузиастов и учащихся-разработчиков. Некоторые уникальные функциональные возможности Visual C# 2010 Express В целом продукты линейки Express представляют собой усеченные версии своих полнофункциональных аналогов в линейке Visual Studio 2010 и ориентированы главным образом на любителей .NET и занимающихся изучением .NET студентов. Как и в SharpDevelop, в Visual C# 2010 Express предлагаются разнообразные инструменты для просмотра объектов, визуальный конструктор Windows Forms, диалоговое окно Add References (Добавление ссылок), возможности IntelliSense и шаблоны для расширения программного кода. Помимо этого в Visual C# 2010 Express доступно несколько (важных) функциональных возможностей, которые в SharpDevelop в настоящее время отсутствуют. К их числу относятся: • развитая поддержка для создания приложений Window Presentation Foundation (WPF) с помощью XAML; • функция IntelliSense для новых синтаксических конструкций С# 2010, в том числе именованных аргументов и необязательных параметров; • возможность загружать дополнительные шаблоны, позволяющие разрабатывать приложения ХЬох 360, приложения WPF с интеграцией Twitter, и многое другое. На рис. 2.8 показан пример, иллюстрирующий создание с помощью Visual C# Express XAML-разметки для проекта WPF : Common WPF Controls ;АВ WPF Centre* There ire no usable controls in this 91 Drag in item onto this text to «dd it ti ;jjf Background О Binding jf BrndingGroop ■^ Brt/rupEffect |3f BitmapEffectlnput :-ff BofdetBrush ■;3f BorderThidtness :3f CacheMode I d Design ■ fl .BXAMl | '-'<Uindo»i x:Class-*MpfApplicatienl.n1^ Calendar >:»Jni--rittp://5criejMS.iiicr / Click %alnr.:K'"bttp://schemes.*i!tf[ ClickMode Titlt'-'MainWindow" Hcigtit-jjji <£_ ^ <Grid> <8utton (■.:ri,w*«"«ybutt< _ </Wirtdow> f ■■ U-, ji щ 1! JEEEBL ;.3 Solution WpfApplicBtionl' A project) j 7! Wpf Application 1 it Properties i' сЛ References t> M Appjcaml > '»- MainWindowjtaml 100% -j« ^ InS Grid 'A.noWGnd i Рис. 2.8. Visual C# Express обладает встроенной поддержкой API-интерфейсов .NET 4.0
92 Часть I. Общие сведения о языке С# и платформе .NET Поскольку по внешнему виду и поведению IDE-среда Visual C# 2010 Express очень похожа на Visual Studio 2010 (и, в некоторой степени, на SharpDevelop), более подробно она здесь рассматриваться не будет. Ее вполне допустимо использовать для проработки дальнейшего материала книги, но при этом обязательно следует иметь в виду, что в ней не поддерживаются шаблоны проектов для создания веб-сайтов ASP.NET. Чтобы иметь возможность строить веб-приложения, необходимо загрузить продукт Visual Web Developer 2010, который также доступен на сайте http://msdn.microsoft.com/ express. Создание приложений .NET с использованием Visual Studio 2010 Профессиональные разработчики программного обеспечения .NET наверняка располагают самым серьезным в этой сфере продуктом производства Microsoft, который называется Visual Studio 2010 и доступен по адресу http : //msdn .microsoft. com/ vstudio. Этот продукт представляет собой самую функционально насыщенную и наиболее приспособленную под использование на предприятиях IDE-среду из всех, что рассматривались в настоящей главе. Такая мощь, несомненно, имеет свою цену, которая варьируется в зависимости от версии Visual Studio 2010. Как не трудно догадаться, каждая версия поставляется со своим уникальным набором функциональных возможностей. На заметку! Количество версий в семействе продуктов Visual Studio 2010 очень велико. В оставшейся части книги предполагается, что в качестве предпочитаемой IDE-среды используется версия Visual Studio 2010 Professional. Хотя далее предполагается наличие копии Visual Studio 2010 Professional, это вовсе не обязательно для проработки излагаемого в настоящей книге материала. В худшем случае может встретиться описание опции, которая отсутствует в используемой IDE- среде. Однако весь приведенный в книге код будет прекрасно компилироваться, какой бы инструмент не применялся. На заметку! После загрузки кода для настоящей книги можно открывать любой пример в Visual Studio 2010 (или Visual C# 2010 Express), дважды щелкая на соответствующем файле решения * . sin. Если на машине не установлено ни Visual Studio 2010, ни С#20010 Express, файлы * . cs потребуется вставлять вручную в рабочую область проекта внутри используемой IDE-среды. Некоторые уникальные функциональные возможности Visual Studio 2010 Как не трудно догадаться, Visual Studio 2010 также поставляется с графическими конструкторами, поддержкой использования отдельных фрагментов кода, средствами для работы с базами данных, утилитами для просмотра объектов и проектов, а также встроенной справочной системой. Но, в отличие от многих из уже рассмотренных IDE-сред, в Visual Studio 2010 предлагается множество дополнительных возможностей, наиболее важные из которых перечислены ниже: • графические редакторы и конструкторы XML; • поддержка разработки программ Windows, ориентированных на мобильные устройства;
Глава 2. Создание приложений на языке С# 93 • поддержка разработки программ Microsoft Office; • поддержка разработки проектов Windows Workflow Foundation; • встроенная поддержка рефакторинга кода; • инструменты визуального конструирования классов. По правде говоря, в Visual Studio 2010 предлагается настолько много возможностей, что для их полного описания понадобилась бы отдельная книга. В настоящей книге такие цели не преследуются. Тем не менее, в нескольких следующих разделах наиболее важные функциональные возможности рассматриваются чуть подробнее, а другие по мере необходимости будут описаны далее в книге. Ориентирование на .NET Framework в диалоговом окне New Project Те, кто следует указаниям этой главы, сейчас могут попробовать создать новое консольное приложение на С# (по имени Vs2010Example), выбрав в меню File (Файл) пункт New^Project (Создать^Проект). Как можно увидеть на рис. 2.9, в Visual Studio 2010 поддерживается возможность выбора версии .NET Framework B.0, 3.x или 4.0), для которой должно создаваться приложение, с помощью раскрывающегося списка, отображаемого в правом верхнем углу диалогового окна New Project (Новый проект). Для всех описываемых в настоящей книге проектов, в этом списке можно оставлять выбранным предлагаемый по умолчанию вариант .NET Framework 4.0. Рис. 2.9. В Visual Studio 2010 позволяется выбирать определенную целевую версию .NET Framework Использование утилиты Solution Explorer Утилита Solution Explorer (Проводник решений), доступная через меню View (Вид), позволяет просматривать набор всех файлов с содержимым и ссылаемых сборок, которые входят в состав текущего проекта (рис. 2.10). Обратите внимание, что внутри папки References (Ссылки) в окне Solution Explorer отображается список всех сборок, на которые в проекте были добавлены ссылки. В зависимости от типа выбираемого проекта и целевой версии .NET Framework, этот список выглядит по-разному.
94 Часть I. Общие сведения о языке С# и платформе .NET Solution Explorer i^g| Solution 'VsiOlOExample' A project) d 3 Vs2010Examp4e ■ЗА Properties л ^j References *0 Microsoft.CSharp чЭ System чЗ System. С о re •a System.Data <J System.Data.DataSetExtensions -O System Xm\ *£3 System.Xml.Linq «^ Program.cs Рис. 2.10. Окно утилиты Solution Explorer Добавление ссылок на внешние сборки Если необходимо сослаться на дополнительные сборки, щелкните на папке References правой кнопкой мыши и выберите в контекстном меню пункт Add Reference (Добавить ссылку). После этого откроется диалоговое окно, позволяющее выбрать желаемые сборки (в Visual Studio это аналог параметра /reference в компиляторе командной строки). На вкладке .NET этого окна, показанной на рис. 2.11, отображается список наиболее часто используемых сборок .NET; на вкладке Browse (Обзор) предоставляется возможность найти сборки .NET, которые находятся на жестком диске; на вкладке Recent (Недавние) приводится перечень сборок, на которые часто добавлялись ссылки в других проектах. О Add Reference COM ''Jp'rajecbj Browte] Recent I Component Name System.IdentityModel System.IdentityModel.Sele... System.Management System.Management.Instr... System.Messaging System.Net System.Numerics System.Printing System.Runtime System. Runtime. Remoting System.Runttme.Sertalizati... Version 4.0.0.0 4.0.0.0 4ЛД0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 Runtime V4.0.21006 v4.021006 v4.0.21006 v4.0.21006 v4.0.21006 v4.0.21006 v4.021006 v4.0.21006 у4.0.Л006 v4.021006 V4.021006 Path C:\Program G\Program — CAProgram CAProgram C:\Program CAProgram CAProgram CAProgram C:\Program CAProgram CAProgram » ► :«««П I Рис. 2.11. Диалоговое окно Add Reference Просмотр свойств проекта И, наконец, напоследок важно обратить внимание на наличие в окне утилиты Solution Explorer пиктограммы Properties (Свойства). Двойной щелчок на ней приводит к открытию редактора конфигурации проекта, окно которого называется Project Properties (Свойства проекта) и показано на рис. 2.12.
Глава 2. Создание приложений на языке С# 95 Piatfoffrc Ы/А Default Vs2D10Example Output type: •ramework4 Client Profile I Console Application I I Resources Specify how application resources will be managed: Assembly Information... » Icon and manifest « a»w^#<4 m ^пгЧялч rv *m*4**4 finrf »«4a4 if ♦« 111\тш^-ъшш£Шшвшшят I Vs2010Example > Q| Project References a <} Vs2010Example ^ j^J Program a [jj Base Types Рис. 2.12. Окно Project Properties Различные возможности, доступные в окне Project Properties, более подробно рассматриваются по ходу данной книги. В этом окне можно устанавливать различные параметры безопасности, назначать сборке надежное имя, развертывать приложение, вставлять необходимые для приложения ресурсы и конфигурировать события, которые должны происходить перед и после компиляции сборки. Утилита Class View Следующей утилитой, с которой необходимо познакомиться, является Class View (Просмотр классов), доступ к которой тоже можно получать через меню View. Эта утилита позволяет просматривать все типы, которые присутствуют в текущем проекте, с объектно-ориентированной точки зрения (а не с точки зрения файлов, как это позволяет делать утилита Solution Explorer). В верхней панели утилиты Class View отображается список пространств имен и их типов, а в нижней панели — члены выбранного в текущий момент типа, как показано на рис. 2.13. В результате двойного щелчка на типе или члене типа в окне утилиты Class View в Visual Studio будет автоматически открываться соответствующий файл кода С#, с размещением курсора мыши на соответствующем месте. Еще одной замечательной функциональностью утилиты Class View в Visual Studio 2010 является возможность открывать любую ссылаемую сборку и просматривать содержащиеся внутри нее пространства имен, типы и члены (рис. 2.14). Утилита Object Browser В Visual Studio 2010 доступна еще одна утилита для изучения множества сборок, на которые имеются ссылки в текущем проекте. Называется эта утилита Object Browser (Браузер объектов) и получить к ней доступ можно, опять-таки, через меню View. После открытия ее окна останется просто выбрать сборку, которую требуется изучить (рис. 2.15). >♦ -ObjectO * Equa!s(object, object) "-♦ Equals(object) V GetHashCodeQ Ф GetTypeO qj+ MemberwJseOoneQ J Рис. 2.13. Окно утилиты Class View
96 Часть I. Общие сведения о языке С# и платформе .NET ^ Vs2010Example л Q| Project References > .*3 Microsoft.CSharp J J mscorlib > О Microsoft. Win32 » {} Microsoft,Win32.SafeHandles ^ (} System t> ^[^ > J Action ft AccessViolationException(System.Runtime.Serielizstion.SerielizationInfol % Acce$sViolationException(string, System.Exception) v AccessVk)lationException(string) V AccessViolationExceptionO □шва-. Рис. 2.14. Утилита Class View может также применяться для просмотра ссылаемых сборок Object Browser Browse All Components I <Search> 0 a mscorlib [2.0.0.0] -СЭ mscorlib [2.0.5.0] л j mscorlib [4.0.0.0] !> {} Microsoft.Win32 t> {} Microsoft.Win32.SafeHandles {} System d {} System.Collections •% Bit Array Л% CaselnsensitiveComparer > "tj CaselnsensitiveHashCodeProvider ij CollectionBasc -', Comparer l> 4$ DictionaryBase L> Tj^ DictionaryEntry r, JH Ul.rhf.kU I > Adapter(System.Collections.ILi5t) ♦ Add(object) V AddRange(System.CollectionsJCollection) ♦ ArrayList(System.CollectionsJCollection) ♦ ArrayListOnt) ♦ ArrayListO <* BinarySearch(object, System.Collections.IComparer) t BinarySearch(object) ♦ BinarySearch(int, int, object, System.Collections.IComparer) public class Array Li st Member of System.Collections Summary: Implements the System.CollectionsJList interface using an array whose size is dynamically increased as required. Рис. 2.15. Окно утилиты Object Browser Встроенная поддержка рефакторинга программного кода Одной из главных функциональных возможностей Visual Studio 2010 является встроенная поддержка для проведения рефакторинга существующего кода. Если объяснять упрощенно, то под рефакторингом (refactoring) подразумевается формальный механический процесс улучшения существующего кода. В прежние времена рефакторинг требовал приложения массы ручных усилий. К счастью, теперь в Visual Studio 2010 можно достаточно хорошо автоматизировать этот процесс. За счет использования меню Refactor (Рефакторинг), которое становится доступным при открытом файле кода, а также соответствующих клавиатурных комбинаций быстрого вызова, смарт-тегов (smart tags) и/или вызывающих контекстные меню щелчков, можно существенно видоизменять код с минимальным объемом усилий. В табл. 2.2 перечислены некоторые наиболее распространенные приемы рефакторинга, которые распознаются в Visual Studio 2010.
Глава 2. Создание приложений на языке С# 97 Таблица 2.2. Приемы рефакторинга, поддерживаемые в Visual Studio 2010 Прием рефакторинга Описание Extract Method (Извлечение метода) Encapsulate Field (Инкапсуляция поля) Extract Interface (Извлечение интерфейса) Reorder Parameters (Переупорядочивание параметров) Remove Parameters (Удаление параметров) Rename (Переименование) Позволяет определять новый метод на основе выбираемых операторов программного кода Позволяет превращать общедоступное поле в приватное, инкапсулированное в форму свойство С# Позволяет определять новый тип интерфейса на основе набора существующих членов типа Позволяет изменять порядок следования аргументов в члене Позволяет удалять определенный аргумент из текущего списка параметров Позволяет переименовывать используемый в коде метод, поле, локальную переменную и т.д. по всему проекту Чтобы увидеть процесс рефакторинга в действии, давайте модифицируем метод Main (), добавив в него следующий код: static void Main(string [ ] args) { // Настройка консольного интерфейса (CUI). Console.Title = "My Rocking App11; Console.ForegroundColor = ConsoleColor.Yellow; Console.BackgroundColor = ConsoleColor.Blue; Console WriteLine (M***************************************'4 • Console.WriteLine("***** Welcome to My Rocking App! *******"); Console WriteLine ("***************************************") • Console.BackgroundColor = ConsoleColor.Black; // Ожидание нажатия клавиши <Enter>. Console.ReadLine (); } В таком, как он есть виде, в этом коде нет ничего неправильного, но давайте представим, что возникло желание сделать так, чтобы данное приветственное сообщение отображалось в различных местах по всей программе. В идеале вместо того, чтобы заново вводить ту же самую отвечающую за настройку консольного интерфейса логику, было бы неплохо иметь вспомогательную функцию, которую можно было бы вызывать для решения этой задачи. С учетом этого, попробуем применить к существующему коду прием рефакторинга Extract Method (Извлечение метода). Для этого выделите в окне редактора все содержащиеся внутри Main () операторы, кроме последнего вызова Console .ReadLine (), и щелкните на выделенном коде правой кнопкой мыши. Выберите в контекстном меню пункт Refactor^ Extract Method (РефакторингаИзвлечь метод), как показано на рис. 2.16. В открывшемся далее окне назначьте новому методу имя ConfigureCUI () . После этого метод Main() станет вызывать новый только что сгенерированный метод ConfigureCUI (), внутри которого будет содержаться выделенный ранее код: class Program { static void Main(string[] args) { ConfigureCUI (); // Ожидание нажатия клавиши <Enter>.
98 Часть I. Общие сведения о языке С# и платформе .NET Console.ReadLine(); } private static void ConfigureCUl() { // Настройка консольного интерфейса (CUI). Console.Title = "My Rocking App"; Console.Title = "Мое приложение"; Console.ForegroundColor = ConsoleColor.Yellow; Console.BackgroundColor = ConsoleColor.Blue; Сonsole.WriteLine("******************** Console.WriteLine("***** Welcome to My Rocking App! Console.WriteLine("**************************~****^ Console.BackgroundColor = ConsoleColor.Black; ************ k- ****** И ); } Этот лишь один простой пример применения предлагаемых в Visual Studio 2010 встроенных возможностей для рефакторинга, и по ходу настоящей книги будет встречаться еще немало подобных примеров. Ц Program.cs* X Щ ij4Vs2010Example.Program * e*Main(string[] args) 1 '-1 ciass Hrogran ■ ■■■ — static void Main(string[] args) { Console.Title - "My Rockir Console.ForegroundColor - Console.BackgroundColor ■ Console.WriteLineC****** h (Jonsole.WriteLitM!("*****"* Con sole.BackgroundColor m // Wait for Enter key to t Console.ReadLine(); 1 > ' 1} 1~00% ~«|« Refactor Organize Usings £) Create Unit Tests.,, Generate Sequence Diagram... <% Insert Snippet.,, % Surround With,.. J Go To Definition Find AH References _,-, View Call Hierarchy Breakpoint •i Run To Cursor * Cut -J Copy A Paste Outlining 1 CtrklCX Ctrl+K. S F12 Ctrl+K, R CtrkrCT ► Ctri+FlO Ctri+X Ctrt+C Ctri+V «t/ Rename... Чф*-' Extract Method... к V1 Encapsulate Field... _..: Extract Interface... * ч Remove Parameters... а.ь Reorder Parameters.,. J ■ F2 CtrUR, M Ctri+R, E CtrkRJ Ctrl+RV Qri+R, 0 J . t Рис. 2.16. Активизация рефакторинга кода Возможности для расширения и окружения кода В Visual Studio 2010 (а также в Visual C# 2010 Express) можно вставлять готовые блоки кода С# выбором соответствующих пунктов в меню, вызовом контекстных меню по щелчку правой кнопкой мыши и/или использованием соответствующих клавиатурных комбинаций быстрого вызова. Число доступных шаблонов для расширения кода впечатляет. В целом их можно поделить на две основных группы. • Шаблоны для вставки фрагментов кода (code snippet). Эти шаблоны позволяют вставлять общие блоки кода в месте расположения курсора мыши. • Шаблоны для окружения кода (Surround With). Эти шаблоны позволяют помещать блок избранных операторов в рамки соответствующего контекста. Чтобы посмотреть на эту функциональность в действии, давайте предположим, что требуется обеспечить проход по поступающим в метод Main () параметрам в цикле foreach.
Глава 2. Создание приложений на языке С# 99 Вместо того чтобы вводить необходимый код вручную, можно активизировать фрагмент кода f oreach. После выполнения этого действия IDE-среда поместит шаблон кода f oreach в месте, где в текущий момент находится курсор мыши. Поместим курсор мыши после первой открывающей фигурной скобки в методе Main (). Одним из способов активизации фрагмента кода является выполнение щелчка правой кнопкой мыши и выбора в контекстном меню пункта Insert Snippet (Вставить фрагмент кода) (или Surround With (Окружить с помощью)). Это приводит к отображению списка всех относящихся к данной категории фрагментов кода (для закрытия контекстного меню достаточно нажать клавишу <Esc>). В качестве клавиатурной комбинации быстрого вызова можно просто ввести имя интересующего фрагмента кода, которым в данном случае является f oreach. На рис. 2.17 видно, что пиктограмма, представляющая фрагмент кода, внешне немного напоминает клочок бумаги. Щ Program.cs* X ЩШ j$Vs2Q10Example.Program - J ,j<*Main(string[] args) fusing System; using System.Collections.Generic; I using System.Linq; [using System.Text; "namespace Vs2010Example 1С ^ class Program i ( static void Main(string[] args) { Рис. 2.17. Активизация фрагмента кода Отыскав фрагмент кода, который требуется активизировать, нажмите два раза клавишу <ТаЬ>. Это приведет к автоматическому завершению всего фрагмента кода и оставлению ряда меток-заполнителей, в которых останется только ввести необходимые значения, чтобы фрагмент был готов. Нажимая клавишу <ТаЬ>, можно переходить от одной метки-заполнителя к другой и заполнять пробелы (по завершении нажмите клавишу <Esc> для выхода из режима редактирования фрагмента кода). В результате щелчка правой кнопкой мыши и выбора в контекстном меню пункта Surround With (Окружить с помощью) будет тоже появляться список возможных вариантов. При использовании средства Surround With обычно сначала выбирается блок операторов кода для представления того, что должно применяться для их окружения (например, блок try/catch). Обязательно уделите время изучению предопределенных шаблонов расширения кода, поскольку они могут радикально ускорить процесс разработки программ. На заметку! Все шаблоны для расширения кода представляют собой XML-описания кода, подлежащие генерации в IDE-среде. В Visual Studio 2010 (а также в Visual C# 2010 Express) можно создавать и собственные шаблоны кода. Дополнительные сведения доступны в статье "Investigating Code Snippet Technology" ("Исследование технологии применения фрагментов кода") на сайте http: //msdn .microsoft. com. щ
100 Часть I. Общие сведения о языке С# и платформе .NET Solution Explorer |*Э Solution 'Vs2ti view Class Diagram ) 73I u.iflint^HH ■ ■ ' 3 Vs2010ExafflpVT- ill Properties ,j> References СЭ Microsoft.С Sharp ■*<3 System 4J System.Core нЭ System.Data чЭ System.Data.DataSetExtensions *<Э System.Xml -O SystemJCml.Linq ^3 Program.es Рис. 2.18 Вставка файла диаграммы классов Утилита Class Designer В Visual Studio 2010 имеется возможность конструировать классы визуальным образом (в Visual С# 2010 Express такой возможности нет). Для этого в составе Visual Studio 2010 поставляется утилита под названием Class Designer (Конструктор классов), которая позволяет просматривать и изменять отношения между типами (классами, интерфейсами, структурами, перечислениями и делегатами) в проекте. С помощью этой утилиты можно визуально добавлять или удалять члены из типа с отражением этих изменений в соответствующем файле кода на С#, а также в диаграмме классов. Для работы с этой утилитой сначала необходимо вставить новый файл диаграммы классов. Делать это можно несколькими способами, одним из которых является щелчок на кнопке View Class Diagram (Просмотр диаграммы классов) в правой части окна Solution Explorer, как показано на рис. 2.18 (при этом важно, чтобы в окне был выбран проект, а не решение). После выполнения этого действия появляются пиктограммы, представляющие классы, которые входят в текущий проект. Щелкая внутри них на значке с изображением стрелки для того или иного типа, можно отображать или скрывать члены этого типа (рис. 2.19). На заметку! С помощью панели Class Designer (Конструктор классов) можно настраивать параметры отображения поверхности конструктора желаемым образом. ClassDiagraml.cd* X Ц С < rzz *ЙГ_ 11®] Class a Methods & ConfigureCUI ll^ Main ^ i i i-rzr-z 1 Рис. 2.19. Просмотр диаграммы классов Эта утилита работает вместе с двумя другими средствами Visual Studio 2010 — окном Class Details (Детали класса), которое можно открыть путем выбора в меню View (Вид) пункта Other Windows (Другие окна), и панелью Class Designer Toolbox (Элементы управления конструктора классов), которую можно отобразить выбором в меню View (Вид) пункта Toolbox (Элементы управления). В окне Class Details не только отображаются детали выбранного в текущий момент элемента в диаграмме, но также можно изменять его существующие члены и вставлять новые на лету, как показано на рис. 2.20.
Глава 2. Создание приложений на языке С# 101 Class Details - Program * * if %* -3 & *f Name г> Л% ConfigureCUI dV Main -•♦ <;addmethod> * Properties 3* < add property* | *g Class Details Д void void private private »..*,x] Hide и (II и 1 I Toolbox 1 d Class Designer 1^ Pointer В Class E Enum £j Interface * J Abstract Class □ Struct О Delegate 4- Inheritance *т_ Association 1 У& Comment 1 л General ▼ п x] Л 1 d Class Рис. 2.20. Окно Class Details Что касается панели Class Designer Toolbox, которую, как уже было сказано, можно активизировать через меню View (Вид), то она позволяет вставлять в проект новые типы (и создавать между ними желаемые отношения) визуальным образом (рис. 2.21). (Следует иметь в виду, что для просмотра этой панели требуется, чтобы окно диаграммы классов было активным.) По мере выполнения этих действий IDE-среда автоматически создает незаметным образом соответствующие новые определения типов на С#. Для примера давайте перетащим из панели Class Designer Toolbox в окно Class Designer новый элемент Class (Класс), в отрывшемся окне назначим ему имя Саг, а затем с помощью окна Class Details добавим в него общедоступное поле типа string по имени PetName (рис. 2.22). Взглянув на С#-определение класса Саг после этого, можно увидеть, что оно было соответствующим образом обновлено (добавленные комментарии не считаются): public class Car { // Использовать общедоступные данные обычно //не рекомендуется, но здесь это упрощает пример. public string petName; } Теперь давайте активизируем утилиту Class Designer еще раз и перетащим на поверхность конструктора новый элемент типа Class, присвоив ему имя SportsCar. Затем выберем в Class Designer Toolbox пиктограмму Inheritance (Наследование) и щелкнем в верхней части пиктограммы SportsCar. Далее, не отпуская левую кнопку мыши, перетащим курсор мыши на поверхность пиктограммы класса Саг и отпустим ее. Правильное выполнение всех перечисленных выше действий приведет к тому, что класс SportsCar станет наследоваться от класса Саг, как показано на рис. 2.23. Рис. 2.21. Панель Designer Toolbox (Class Det - - Ц- X Name * Properties *£ <add property> I * Fields У [petName ♦ <add field* * Events Type Summary И Рис. 2.22. Добавление поля с помощью окна Class Details
102 Часть I. Общие сведения о языке ClassDtagraml.cd* X Class S Methods a* ConfigureCUI аФ Main L^.'.umju.niiinuM - МММ [^ > ►___ Рис. 2.23. Визуальное наследования одного класса от другого Чтобы завершить данный пример, осталось обновить сгенерированный класс SportsCar, добавив в него общедоступный метод с именем GetPetName (): public class SportsCar : Car { public string GetPetName() { petName = "Fred"; return petName; } } Все эти (и другие) визуальные инструменты Visual 2010 придется еще много раз использовать в книге. Однако уже сейчас должно появиться чуть большее понимание основных возможностей этой IDE-среды. На заметку! Концепция наследования подробно рассматривается в главе 6. Интегрируемая система документации .NET Framework 4.0 И, наконец, последним средством в Visual Studio 2010, которым необходимо обязательно уметь пользоваться с самого начала, является полностью интегрируемая справочная система. Поставляемая с .NET Framework 4.0 SDK документация представляет собой исключительно хороший, очень понятный и насыщенный полезной информацией источник. Из-за огромного количества предопределенных типов .NET (насчитывающих тысячи), необходимо погрузиться в исследование предлагаемой документации. Не желающие делать это обрекают себя как разработчика .NET на длительное, мучительное и болезненное существование. При наличии соединения с Интернетом просматривать документацию .NET Framework 4.0 SDK можно в онлайновом режиме по следующему адресу: http://msdn.microsoft.com/library Разумеется, при отсутствии постоянного соединения с Интернетом такой подход оказывается не очень удобным. К счастью, ту же самую справочную систему можно установить локально на своем компьютере. Имея уже установленную копию Visual Studio 2010, необходимо выбрать в меню Start (Пуск) пункт All Programs^ Microsoft С# и платформе .NET 1 Сж Class 1 S Fields Ф petName ^ „ Y ,f ■■■■"•■:■■■ ' j SportsCar | Class J +Car ®) > "Щ
Глава 2. Создание приложений на языке С# 103 Visual Studio 2010^Visual Studio Tools^ Manage Help (Все программы1^Microsoft Visual Studio 2010 ^Утилиты Visual Studio ^Управление настройками справочной системы). Затем можно приступать к добавлению интересующей справочной документации, как показано на рис. 2.24 (если на диске хватает места, имеет смысл добавить всю возможную документацию). Ь Hdp Library Manage: Find Content on Disk Actions ! v .NET Development .NET Framework 4 M& v visual Studio 2010 Application Lifecycle Management ЙЙЙ Extending Application Lifecycle Management Md JScript AfW Office Development ^ Settings | Help Status Click Add to select content for offline help. Update Рис. 2.24. В окне Help Library Manager (Управление библиотекой справочной документации) можно загрузить локальную копию документации .NET Framework 4.0 SDK На заметку! Начиная с версии .NET Framework 4.0, просмотр справочной системы осуществляется через текущий веб-браузер, даже в случае локальной установки документации .NET Framework 4.0 SDK. После локальной установки справочной системы простейшим способом для взаимодействия с ней является выделение интересующего ключевого слова С#, имени типа или имени члена в окне представления кода внутри Visual Studio 2010 и нажатие клавиши <F1>. Это приводит к открытию окна с документацией, касающейся конкретного выбранного элемента. Например, если выделено ключевое слово string в определении класса Саг, после нажатия клавиши <F1> появится страница со справочной информацией об этом ключевом слове. Еще одним полезным компонентом справочной системы является доступное для редактирования поле Search (Искать), которое отображается в левой верхней части экрана. В этом поле можно вводить имя любого пространства имен, типа или члена и тем самым сразу же переходить в соответствующее место в документации. При попытке найти подобным образом пространство имен System.Reflection, например, можно будет узнать о деталях этого пространства имен, изучить содержащиеся внутри него типы, просмотреть примеры кода с ним и т.д. (рис. 2.25). В каждом узле внутри дерева описаны типы, которые содержатся в данном пространстве имен, их члены и параметры этих членов. Более того, при просмотре страницы справки по тому или иному типу всегда сообщается имя сборки и пространства имен, в котором содержится запрашиваемый тип (соответствующая информация отображается в верхней части данной страницы). В остальной части настоящей книги ожидается, что читатель будет заглядывать в эту очень важную справочную систему и изучать дополнительные детали рассматриваемых сущностей.
104 Часть I. Общие сведения о языке С# и платформе .NET Q SyJtwn.Reflection Names... «- С ft tt hUp://127.0.0.1-.47873/help/l/ms.hefp?me№^ ► О- A^ Library Horn* Visual Studio 2010 .NET Framework 4 NET Framework Cl*« library System. Retire t ion Namespace ArnbiquouiMetchEvception Clan Assembry CU« Assembly Algorithm] dAttnbutr Class AssembtyCompanyAttribute Class AssemblyConf iguratiortAttribtrte Class AssembryCopynghtAttnbute Class AssembtyCuKureAttribtrte Class AssemblyDefaultAliasAttrtbute Class AssembryOelaySignAttribute Class AssemblyDescriptionAttribute Class AsiembryFiteVersionAttobute Class AssembJyFlagsAttnbute CUss AssembrylnformationalVersionAttnbute Class AssembtyKeyFiteAttrrbute Class AssemblyKeyNameAttribute Class- Assembh/Name Class AsiemblyNameFlags Enumeration i i-irrmmtiitiihliwniaiiif fitiri System.Reflection Namespace OO^Eual Studio Ш Send Feedback The System.Reflection namespace contains types that retrieve information about assemblies, modules, members, parameters, and other entities in managed code by examining their metadata. These types also can be used to manipulate instances of loaded types, for example to hook up events or to invoke methods. To dynamically create types, use the System.Reftection.Emit namespace. Classes Am big u ousM a tch Exception Description The exception that is thrown when binding to a member results in more than one member matching the binding criteria. This dass cannot be inherited. Represents an assembry, which is a reusable, versionable, and self-describing building block of a common language runtime application. Рис. 2.25. Поле Search позволяет быстро находить представляющие интерес элементы На заметку! Не лишним будет напомнить еще раз о важности использования документации .NET Framework 4.0 SDK. Ни одна книга, какой бы объемной она ни была, не способна охватить все аспекты платформы .NET. Поэтому необходимо научиться пользоваться справочной системой; впоследствии это окупится с лихвой. Резюме Итак, нетрудно заметить, что у разработчиков появилась масса новых "игрушек". Целью настоящей главы было проведение краткого экскурса в основные средства, которые программист на С# может использовать во время разработки. В начале было рассказано о том, как генерировать сборки .NET с применением бесплатного компилятора С# и редактора "Блокнот" (Notepad). Затем было приведено краткое описание приложения Notepad++ и показано, как его использовать для редактирования и компиляции файлов кода * . cs. И, наконец, здесь были рассмотрены три таких многофункциональных IDE-среды, как SharpDevelQp (распространяется с открытым исходным кодом), Microsoft Visual С# 2010 Express и Microsoft Visual Studio 2010 Professional. Функциональные возможности каждого из этих продуктов в настоящей главе были описаны лишь кратко, поэтому каждый волен заняться более детальным изучением избранной IDE-среды в свободное время (многие дополнительные функциональные возможности Visual Studio 2010 будут рассматриваться далее в настоящей книге).
ЧАСТЬ II DiaBHbie конструкции программирования наС# В этой части... Глава 3. Главные конструкции программирования на С#: часть I Глава 4. Главные конструкции программирования на С#: часть II Глава 5. Определение инкапсулированных типов классов Глава 6. Понятия наследования и полиморфизма Глава 7. Структурированная обработка исключений Глава 8. Время жизни объектов
ГЛАВА О Вгавные конструкции программирования на С#: часть I В настоящей главе начинается формальное исследование языка программирования С#. Здесь предлагается краткий обзор отдельных тем, в которых необходимо разобраться, чтобы успешно изучить платформу .NET Framework. В первую очередь поясняется то, как создавать объект приложения и как должна выглядеть структура метода Main (), который является входной точкой в любой исполняемой программе. Далее рассматриваются основные типы данных в С# (и их аналоги в пространстве имен System), в том числе типы классов System. String и System. Text. StringBuilder. После представления деталей основных типов данных в .NET рассказывается о ряде методик, которые можно применять для их преобразования, в том числе об операциях сужения (narrowing) и расширения (widening), а также использовании ключевых слов checked H.unchecked. Кроме того, в этой главе описана роль ключевого слова var в языке С#, которое позволяет неявно определять локальную переменную. И, наконец, в главе приводится краткий обзор ключевых операций, итерационных конструкций и конструкций принятия решений, применяемых для создания рабочего кода на С#. Разбор простой программы на С# В языке С# вся логика программы должна содержаться внутри определения какого- то типа (в главе 1 уже говорилось, что тип представляет собой общий термин, которым обозначается любой элемент из множества {класс, интерфейс, структура, перечисление, делегат}). В отличие от многих других языков, в С# не допускается создавать ни глобальных функций, ни глобальных элементов данных. Вместо этого требуется, чтобы все члены данных и методы содержались внутри определения типа. Для начала давайте создадим новый проект типа Console Application (Консольное приложение) по имени SimpleCSharpAppB. Как показано ниже, в исходном коде Program, cs ничего особо примечательного нет: using System; using System. Collections . Generic- using System.Linq; using System.Text namespace SimpleCSharpApp
Глава 3. Главные конструкции программирования на С#: часть I 107 { class Program { static void Main(string [ ] args) { } } Имея такой код, далее модифицируем метод Main () в классе Program, добавив в него следующие операторы: class Program { static void Main(string[] args) { // Вывод простого сообщения пользователю. Console.WriteLine("***** My First C# App *****"); Console.WriteLine("Hello World!"); .Console.WriteLine(); // Ожидание нажатия клавиши <Enter> // перед завершением работы. Console.ReadLine(); } } В результате получилось определение типа класса, поддерживающее единственный метод по имени Main (). По умолчанию классу, в котором определяется метод Main (), в Visual Studio 2010 назначается имя Program; при желании это имя легко изменить. Класс, определяющий метод Main (), должен обязательно присутствовать в каждом исполняемом приложении на С# (будь то консольная программа, настольная программа для Windows или служба Windows), поскольку он применяется для обозначения точки входа в приложение. Формально класс, в котором определяется метод Main (), называется объектом приложения. Хотя в одном исполняемом приложении допускается иметь более одного такого объекта (это может быть удобно при проведении модульного тестирования), при этом обязательно необходимо информировать компилятор о том, какой из методов Main () должен использоваться в качестве входной точки. Для этого нужно либо указать опцию main в командной строке, либо выбрать соответствующий вариант в раскрывающемся списке на вкладке Application (Приложение) окна редактора свойств проекта в Visual Studio 2010 (см. главу 2). Обратите внимание, что в сигнатуре метода Main () присутствует ключевое слово static, которое более подробно рассматривается в главе 5. Пока достаточно знать, что область действия статических (static) членов охватывает уровень всего класса (а не уровень отдельного объекта) и потому они могут вызываться без предварительного создания нового экземпляра класса. На заметку! С# является чувствительным к регистру языком программирования. Следовательно, Main и main или Readline и ReadLine будут представлять собой далеко не одно и то же. Поэтому необходимо запомнить, что все ключевые слова в С# вводятся в нижнем регистре (например, public, lock, class, dynamic), а названия пространств имен, типов и членов всегда начинаются (по соглашению) с заглавной буквы, равно как и любые вложенные в них слова (как, например, Console .WriteLine, System.Windows . Forms .MessageBox и System. Data.SqlClient). Как правило, при каждом получении от компилятора ошибки, связанной с "неопределенными символами", требуется проверить регистр символов.
108 Часть II. Главные конструкции программирования на С# Помимо ключевого слова static, данный метод Main () имеет еще один параметр, который представляет собой массив строк (string [ ] args). Хотя в текущий момент этот массив никак не обрабатывается, в данном параметре в принципе может содержаться любое количество аргументов командной строки (процесс получения доступа к которым будет описан чуть ниже). И, наконец, данный метод Main () был сконфигурирован с возвращаемым значением void, которое свидетельствует о том, что решено не определять возвращаемое значение явным образом с помощью ключевого слова return перед выходом из области действия данного метода. Логика Program содержится внутри самого метода Main (). Здесь используется класс Console из пространства имен System. В число его членов входит статический метод WriteLine (), который позволяет отправлять строку текста и символ возврата каретки на стандартное устройство вывода. Кроме того, здесь вызывается метод Console. ReadLine (), чтобы окно командной строки, запускаемое в IDE-среде Visual Studio 2010, оставалось видимым во время сеанса отладки до тех пор, пока не будет нажата клавиша <Enter>. Варианты метода Main () По умолчанию в Visual Studio 2010 будет генерироваться метод Main () с возвращаемым значением void и массивом типов string в качестве единственного входного параметра. Однако такой метод Main () является далеко не единственно возможным вариантом. Вполне допускается создавать собственные варианты входной точки в приложение с помощью любой из приведенных ниже сигнатур (главное, чтобы они содержались внутри определения какого-то класса или структуры на С#): // Возвращаемый тип int и массив строк в качестве параметра. static int Main(string[] args) { // Должен обязательно возвращать значение перед выходом! raturn 0; } //Ни возвращаемого типа, ни параметров. static void Main() { } // Возвращаемый тип int, но никаких параметров. static int Main() { // Должен обязательно возвращать значение перед выходом! return 0; } На заметку! Метод Main () может также определяться как общедоступный (public), а не приватный (private), каковым он считается, если не указан конкретный модификатор доступа. В Visual Studio 2010 метод Main () автоматически определяется как неявно приватный. Это гарантирует отсутствие у приложений возможности напрямую обращаться к точке входа друг друга. Очевидно, что выбор способа создания метода Main () зависит от двух моментов. Во-первых, он зависит от того, нужно ли, чтобы системе после окончания выполнения метода Main () и завершения работы программы возвращалось какое-то значение; в этом случае необходимо возвращать тип данных int, а не void. Во-вторых, он зависит от необходимости обработки предоставляемых пользователем параметров командной строки; в этом случае они должны сохраняться в массиве strings. Рассмотрим все возможные варианты более подробно.
Глава 3. Главные конструкции программирования на С#: часть I 109 Спецификация кода ошибки в приложении Хотя в большинстве случаев методы Main () возвращают void в качестве возвращаемого значения, способность возвращать int из Main () позволяет согласовать С# с другими языками на базе С. По соглашению, возврат значения 0 свидетельствует о том, что выполнение программы прошло успешно, а любого другого значения (например, -1) — что в ходе выполнения программы произошла ошибка (следует иметь в виду, что значение 0 возвращается автоматически даже в случае, если метод Main () возвращает void). В операционной системе Windows возвращаемое приложением значение сохраняется в переменной среды по имени %ERRORLEVEL%. В случае создания приложения, в коде которого предусмотрен запуск какого-то исполняемого модуля (см. главу 16), получить значение %ERRORLEVEL% можно с помощью статического свойства System. Diagnostics.Process.ExitCode. Из-за того, что возвращаемое приложением значение в момент завершения его работы передается системе, приложение не может получать и отображать свой конечный код ошибки во время выполнения. Однако просмотреть код ошибки по завершении выполнения программы все-таки можно. Чтобы увидеть, как это делается, модифицируем метод Main () следующим образом: // Обратите внимание, что теперь возвращается int, а не void. static int Main(string [ ] args) { // Вывод сообщения и ожидание нажатия клавиши <Enter>. Console.WriteLine(»***** My First C# App *****"); Console.WriteLine("Hello World!"); Console.WriteLine(); Console.ReadLine(); // Возврат произвольного кода ошибки. return -1; } Теперь давайте обеспечим перехват возвращаемого Main () значения с помощью командного файла. Для этого перейдите в окне проводника Windows в каталог, где хранится скомпилированное приложение (например, в C:\SimpleCSharpApp\bin\Debug), создайте в нем новый текстовый файл (по имени SimpleCSharpApp.bat) и добавьте в него следующие инструкции: @echo off rem Командный файл для приложения SimpleCSharpApp.exe, rem перехватывающий возвращаемое им значение. SimpleCSharpApp @if "%ERRORLEVEL%" == " goto success :fail echo This application has failed1 rem Выполнение этого приложения не удалось 1 echo return value = %ERRORLEVEL% goto end :success echo This application has succeeded! rem Выполнение этого приложения прошло успешно! echo return value = %ERRORLEVEL% goto end :end echo All Done . , rem Все сделано.
110 Часть II. Главные конструкции программирования на С# Теперь откройте окно командной строки в Visual Studio 2010 и перейдите в каталог, где находится исполняемый файл приложения и созданный только что файл * .bat. Запустите командный файл, набрав его имя и нажав <Enter>. После этого на экране должен появиться вывод, подобный показанному на рис. 3.1, поскольку метод Main () сейчас возвращает значение -1. Если бы он возвращал значение 0, в окне консоли появилось бы сообщение This application has succeeded!. I Visual Studio 2010 Command Prompt С:\SimpleCSharpApp\bi n\Debug>SimpleCSharpApp.bat :***** My First C# App ***** IgHello World! prhis application has failed! return value = -1 |a11 Done. ::\Si mpleCSharpApp\bi n\Debug> Рис. З.1. Перехват значения, возвращаемого приложением, с помощью командного файла В большинстве приложений на С# (если не во всех) в качестве возвращаемого Main () значения будет использоваться void, которое, как уже известно, неявно подразумевает возврат кода ошибки 0. Из-за этого все демонстрируемые далее в настоящей книге методы Main () будут возвращать именно void (никакие командные файлы для перехвата кода возврата в последующих проектах не используются). Обработка аргументов командной строки Теперь, когда стало более понятно, что собой представляет возвращаемое значение метода Main (), давайте посмотрим на входной массив данных string. Предположим, что теперь требуется обновить приложение так, чтобы оно могло обрабатывать любые возможные параметры командной строки. Одним из возможных способов для обеспечения такого поведения является использование поддерживаемого в С# цикла for (все итерационные конструкции С# более подробно рассматриваются далее в главе): static int Main(string [ ] args) { // Обработка любых входящих аргументов. for(int i = 0; i < args.Length; i++) Console .WnteLine ("Arg: {0}", args[i]); Console.ReadLine (); return -1; } Здесь с помощью свойства Length типа System.Array производится проверка, содержатся ли в массиве string какие-то элементы. Как будет показано в главе 4, все массивы в С# на самом деле относятся к классу System.Array и потому имеют общий набор членов. При проходе по массиву значение каждого элемента выводится в окне консоли. Процесс предоставления аргументов в командной строке сравнительно прост и показан на рис. 3.2. В качестве альтернативы, вместо стандартного цикла for для прохода по входному массиву строк можно также использовать ключевое слово f о reach.
Глава 3. Главные конструкции программирования на С#: часть I 111 | Visual Studio 2010 Command Prompt *■ SimpleCSHaipApp /argl ~ar$2 >:ШШ fC:\SimpleCSharpApp\bin\Debug>Simp1eCSharpApp /argl -arg2 ***** My First c# App ***** IHello World! rg: /argl rg: -arg2 Рис. З.2. Предоставление аргументов в командной строке Ниже приведен соответствующий пример: // Обратите внимание, что в случае использования // цикла foreach проверять размер массива //не требуется. static int Main(string [ ] args) // Обработка любых входящих аргументов с помощью foreach. foreach (string arg in args) Console.WriteLine("Arg: {0}", arg); Console.ReadLine(); return -1; И, наконец, получать доступ к аргументам командной строки можно с помощью статического метода GetCommandLineArgs (), принадлежащего типу System.Environment. Возвращаемым значением этого метода является массив строк (string). В первом элементе этого массива содержится имя самого приложения, а во всех остальных — отдельные аргументы командной строки. Важно обратить внимание, что при этом определять метод Main () так, чтобы он принимал в качестве входного параметра массив string, больше не требуется, хотя никакого вреда от этого не будет. public static int Main(string[] args) { // Получение аргументов с использованием System.Environment. string[] theArgs = Environment.GetCommandLineArgs(); foreach (string arg in theArgs) Console.WriteLine("Arg: {0}", arg); Console.ReadLine (); return -1; } Разумеется, то, на какие аргументы командной строки должна реагировать программа (если вообще должна), и в каком формате они должны предоставляться (например, с префиксом - или /), можно выбирать самостоятельно. В приведенном выше коде просто передавался набор опций, которые выводились прямо в командной строке. Но что если бы создавалась новая видеоигра, и приложение программировалось на обработку опции, скажем, -godmode? Например, при запуске пользователем приложения с этим флагом можно было бы предпринимать в его отношении соответствующие меры. Указание аргументов командной строки в Visual Studio 2010 В реальном мире конечный пользователь имеет возможность предоставлять аргументы командной строки при запуске программы. Для целей тестирования приложения в процессе разработки также может потребоваться указывать возможные флаги команд-
112 Часть II. Главные конструкции программирования на С# ной строки. В Visual Studio 2010 для этого необходимо дважды щелкнуть на пиктограмме Properties (Свойства) в окне Solution Explorer, выбрать в левой части окна вкладку Debug (Отладка) и указать в текстовом поле Command line arguments (Аргументы командной строки) желаемые значения (рис. 3.3). Configuration: l^ft«^jDebug) *J Platform; [Active (*86) Debug* m Start project Start external program: 3S €.i Start browser with URL , Start Options Reference Paths Signing Security Command line arguments: ! -godmode -argl -arg2 Working directory: Q И Use remote machine Рис. 3.3. Указание аргументов командной строки в Visual Studio 2010 Указанные аргументы командной строки будут автоматически передаваться методу Main () при проведении отладки или запуске приложения в IDE-среде Visual Studio. Интересное отклонение от темы: некоторые дополнительные члены класса System.Environment В классе Environment помимо GetCommandLineArgs () предоставляется ряд других чрезвычайно полезных методов. В частности, этот класс позволяет с помощью различных статических членов получать детальные сведения, касающиеся операционной системы, под управлением которой в текущий момент выполняется .NET-приложение. Чтобы оценить пользу от класса System.Environment, модифицируем метод Main () так, чтобы в нем вызывался вспомогательный метод по имени ShowEnvironmentDetails (): static int Main(string [ ] args) • { // Вспомогательный метод для класса Program. ShowEnvironmentDetails(); Console.ReadLine(); return -1; Теперь реализуем этот метод в классе Program, чтобы в нем вызывались различные члены класса Environment: static void ShowEnvironmentDetails () { // Отображение информации о дисковых устройствах //на данной машине и прочих интересных деталей.
Глава 3. Главные конструкции программирования на С#: часть I 113 foreach (string drive in Environment.GetLogicalDrives ()) Console.WriteLine("Drive: {0}", drive); // диски Console.WriteLine ("OS: {0}", Environment.OSVersion) ; // ОС Console.WriteLine("Number of processors: {0}", Environment.ProcessorCount); // количество процессоров Console.WriteLine(".NET Version: {0}", Environment.Version); // версия .NET } На рис. 3.4 показано возможное тестовое выполнение данного метода. Если на вкладке Debug в Visual Studio 2010 аргументы командной строки не указаны, в окне консоли они, соответственно, тоже появляться не будут. И С \Windo*s\j>sterTt37vcmd,e*e 1, ЯкМгУМмЯ {***** My First сё Арр ***** Hello World I Urg: -godmode |Arg: -argl flArg: -arg2 IDnve: C:\ JDrive: D:\ IDrive: E:\ ■Drive: F:\ ■Drive: H:\ JOS: Microsoft Windows NT 6.1.7600.0 ^Number of processors: 4 ■.NET Version: 4.0.20506.1 *': ' > *! , . J Рис. З.4. Отображение переменных среды Помимо показанных в предыдущем примере, у класса Environment имеются и другие члены. В табл. 3.1 перечислены некоторые наиболее интересные из них; полный список со всеми деталями можно найти в документации .NET Framework 4.0 SDK. Таблица 3.1. Некоторые свойства System.Environment Свойство Описание ExitCode MachineName NewLine StackTrace SystemDirectory UserName Позволяет получить или установить код возврата приложения Позволяет получить имя текущей машины Позволяет получить символ новой строки, поддерживаемый в текущей среде Позволяет получить текущие данные трассировки стека для приложения Возвращает полный путь к системному каталогу Возвращает имя пользователя, который запустил данное приложение Исходный код. Проект SimpleCSharpApp доступен в подкаталоге Chapter 3. Класс System. Console Почти во всех примерах приложений, создаваемых в начальных главах книги, будет интенсивно использоваться класс System. Console. И хотя в действительности консольный пользовательский интерфейс (Console User Interface — CUI) не является настолько
114 Часть II. Главные конструкции программирования на С# привлекательным, как графический (Graphical User Interface — GUI) или веб-интерфейс, ограничение первых примеров консольными программами, позволяет уделить больше внимания синтаксису С# и ключевым характеристикам платформы .NET, а не сложным деталям построения графических пользовательских интерфейсов или веб-сайтов. Класс Console инкапсулирует в себе возможности, позволяющие манипулировать вводом, выводом и потоками ошибок в консольных приложениях. В табл. 3.2 перечислены некоторые наиболее интересные его члены. Таблица 3.2. Некоторые члены класса System. Console Член Описание Веер () Этот метод вынуждает консоль подавать звуковой сигнал определенной частоты и длительности BackgroundColor Эти свойства позволяют задавать цвет изображения и фона для теку- ForegroundColor щего вывода. В качестве значения им может присваиваться любой из членов перечисления ConsoleColor Buf f erHeight Эти свойства отвечают за высоту и ширину буферной области консоли BufferWidth Title Это свойство позволяет устанавливать заголовок для текущей консоли WindowHeight Эти свойства позволяют управлять размерами консоли по отношению WindowWidth к установленному буферу WindowTop WindowLeft Clear () Этот метод позволяет очищать установленный буфер и область изображения консоли Базовый ввод-вывод с помощью класса Console Помимо членов, перечисленных в табл. 3.2, в классе Console имеются методы, которые позволяют захватывать ввод и вывод; все они являются статическими и потому вызываются за счет добавления к имени метода в качестве префикса имени самого класса (Console). К их числу относится уже показанный ранее метод WriteLine (), который позволяет вставлять в поток вывода строку текста (вместе с символом возврата каретки); метод Write (), который позволяет вставлять в поток вывода текст без символа возврата каретки; метод ReadLine (), который позволяет получать информацию из потока ввода вплоть до нажатия клавиши <Enter>; и метод Read (), который позволяет захватывать из потока ввода одиночный символ. Рассмотрим пример выполнения базовых операций ввода-вывода с использованием класса Console, для чего создадим новый проект типа Console Application по имени BasicConsolelO и модифицируем метод Main () внутри него так, чтобы в нем вызывался вспомогательный метод GetUserData(): class Program { static void Main(string [ ] args) } Console.WriteLine ("***** Basic Console I/O *****"); GetUserDataO ; Console.ReadLine();
Глава 3. Главные конструкции программирования на С#: часть I 115 Теперь реализуем этот метод в классе Program вместе с логикой, приглашающей пользователя вводить некоторые сведения и отображающей их на стандартном устройстве вывода. Для примера у пользователя будет запрашиваться имя и возраст (который для простоты будет трактоваться как текстовое значение, а не привычное числовое}. static void GetUserData () { // Получение информации об имени и возрасте. Console.Write("Please enter your name: ") ; // Запрос на ввод имени string userName = Console.ReadLine(); Console.Write("Please enter your age: ") ; // Запрос на ввод возраста string userAge = Console.ReadLine(); // Изменение цвета изображения, просто ради интереса. ConsoleColor prevColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.Yellow; // Отображение полученных сведений в окне консоли. Console.WriteLine ("Hello {0}! You are {1} years old.", userName, userAge); // Восстановление предыдущего цвета. Console.ForegroundColor = prevColor; } После запуска этого приложения входные данные будут выводиться в окне консоли (с использованием указанного специального цвета}. Форматирование вывода, отображаемого в окне консоли В ходе первых нескольких глав можно было заметить, что внутри различных строковых литералов часто встречались обозначения вроде { 0 } и {1}. Дело в том, что в .NET для форматирования строк поддерживается стиль, немного напоминающий стиль оператора printf () в С. Попросту говоря, при определении строкового литерала с сегментами данных, значения которых остаются неизвестными до этапа времени выполнения, внутри него допускается указывать метку-заполнитель с использованием синтаксиса в виде фигурных скобок. Во время выполнения на месте каждой такой метки-заполнителя подставляется передаваемое в Console .WriteLine () значение (или значения}. В качестве первого параметра методу WriteLine () всегда передается строковый литерал, в котором могут содержаться метки-заполнители вида {0}, {1}, {2} и т.д. Следует запомнить, что отсчет в окружаемых фигурными скобками метках-заполнителях всегда начинается с нуля. Остальными передаваемыми WriteLine () параметрами являются просто значения, которые должны подставляться на месте соответствующих меток-заполнителей. На заметку! Если количество пронумерованных уникальным образом заполнителей превышает число необходимых для их заполнения аргументов, во время выполнения будет генерироваться исключение, связанное с форматом. Метка-заполнитель может повторяться в пределах одной и той же строки. Например, для создания строки " 9, Number 9, Number 9 " можно было бы написать такой код: // Вывод строки "9, Number 9, Number 9" Console.WriteLine("{0}, Number {0}, Number {0}", 9); Также следует знать о том, что каждый заполнитель допускается размещать в любом месте внутри строкового литерала, и вовсе не обязательно, чтобы следующий после него заполнитель имел более высокий номер. Например: // Отображает: 20, 10, 3 0 Console.WriteLineCIl}, {0}, {2}", 10, 20, 30);
116 Часть II. Главные конструкции программирования на С# Форматирование числовых данных Если требуется использовать более сложное форматирование для числовых данных, в каждый заполнитель можно включить различные символы форматирования, наиболее полезные из которых перечислены в табл. 3.3. Таблица 3.3. Символы для форматирования числовых данных в .NET Символ форматирования Описание С или с Применяется для форматирования денежных значений. По умолчанию этот флаг идет перед символом локальной культуры (например, знаком доллара [ $ ], если речь идет о культуре US English) D или d Применяется для форматирования десятичных чисел. Этот флаг может также задавать минимальное количество цифр для представления значения Е или е Применяется для экспоненциального представления. Регистр этого флага указывает, в каком регистре должна представляться экспоненциальная константа — в верхнем (Е) или в нижнем (е) F или f Применяется для представления числовых данных в формате с фиксированной точкой. Этот флаг может также задавать минимальное количество цифр для представления значения G или g Расшифровывается как general (общий (формат)). Этот символ может применяться для представления числа в фиксированном или экспоненциальном формате N или п Применяется для базового числового форматирования (с запятыми) X или х Применяется для представления числовых данных в шестнадцатеричном формате. В случае использования символа X в верхнем регистре, в шестнадцатеричном представлении будут содержаться символы верхнего регистра Все эти символы форматирования присоединяются к определенной метке-заполнителю в виде суффикса после двоеточия (например, {0:С}, {1:d}, {2:Х}.). Для примера изменим метод Main () так, чтобы в нем вызывалась новая вспомогательная функция по имени FormatNumericalData (), и затем реализуем его в классе Program для обеспечения форматирования значения с фиксированной точкой различными способами. // Использование нескольких дескрипторов формата. static void FormatNumericalData() { Console.WriteLine("The value 99999 in various formats:"); Console.WriteLineC'c format: {0:c}", 99999); Console.WriteLine ("d9 format: {0:d9}", 99999); Console.WriteLine ("f3 format: {0:f3}", 99999); Console.WriteLine("n format: {0:n}", 99999); } // Обратите внимание, что использование X и х // определяет, будут символы отображаться //в верхнем или нижнем регистре. Console.WriteLine ("E format: {0:E}", 99999) Console.WriteLine ("e format: {0:e}", 99999) Console.WriteLine ("X format: {0:X}", 99999) Console.WriteLine ("x format: {0:x}", 99999) На рис. 3.5 показан вывод этого приложения.
Глава 3. Главные конструкции программирования на С#: часть I 117 Помимо символов, позволяющих управлять форматированием числовых данных, в .NET поддерживается несколько лексем, которые можно использовать в строковых литералах и которые позволяют управлять позиционированием содержимого и добавлением в него пробелов. Более того, лексемы, применяемые для числовых данных, допускается применять и для форматирования других типов данных (например, для перечислений или типа DateTime). Вдобавок можно создавать специальный класс (или структуру) и определять в нем специальную схему форматирования за счет реализации интерфейса ICustomFormatter. По ходу настоящей книги будут встречаться и другие примеры форматирования; тем, кто всерьез заинтересовался темой форматирования строк в .NET, следует обязательно изучить посвященный форматированию строк раздел в документации .NET Framework 4.0. Исходный код. Проект BasicConsolelO доступен в подкаталоге Chapter 3. Форматирование числовых данных в приложениях, отличных от консольных Напоследок хотелось бы отметить, что символы форматирования строк* .NET могут использоваться не только в консольных приложениях. Тот же синтаксис форматирования можно применять и в вызове статического метода string.Format (). Это может быть удобно при генерации во время выполнения текстовых данных, которые должны использоваться в приложении любого типа (например, в настольном приложении с графическим пользовательским интерфейсом, в веб-приложении ASP.NET или веб-службах XML). Для примера предположим, что требуется создать графическое настольное приложение и применить форматирование к строке, отображаемой в окне сообщения внутри него: static void DisplayMessage () { // Использование string.Format() для форматирования строкового литерала. string userMessage = string.Format(00000 in hex is {0:x}", 100000); // Для компиляции этой строки кода требуется // ссылка на Sy stem. Windows. Forms .dll1 System.Windows.Forms.MessageBox.Show(userMessage); } Обратите внимание, что string.Format () возвращает новый объект string, который форматируется в соответствии с предоставляемыми флагами. После этого текстовые данные могут использоваться любым желаемым образом. Системные типы данных и их сокращенное обозначение в С# Как и в любом языке программирования, в С# поставляется собственный набор основных типов данных, которые должны применяться для представления локальных переменных, переменных экземпляра, возвращаемых значений и входных параметров. ***** Basic Console I/O ***** ~1 ' I Please enter your name: Saku Please enter your age: 1 Hello Saku! You are 1 years old. The value 99999 in various formats: с format: $99,999.00 d9 format: 000099999 f3 format: 99999.000 n format: 99,999.00 E format: 9.999900E+004 e format: 9.999900e+004 X format: 1869F x format: 1869f iPress any key to continue . . . ш Рис. З.5. Базовый консольный ввод-вывод (с форматированием строк .NET)
118 Часть II. Главные конструкции программирования на С# Однако в отличие от других языков программирования, в С# эти ключевые слова представляют собой нечто большее, чем просто распознаваемые компилятором лексемы. Они, по сути, представляют собой сокращенные варианты обозначения полноценных типов из пространства имен System. В табл. 3.4 перечислены эти системные типы данных вместе с охватываемыми ими диапазонами значений, соответствующими ключевыми словами на С# и сведениями о том, отвечают ли они требованиям общеязыковой спецификации CLS (Common Language Specification). На заметку! Как рассказывалось в главе 1, с кодом.NET, отвечающим требованиям CLS, может работать любой управляемый язык программирования. В случае включения в программы данных, которые не соответствуют требованиям CSL, другие языки могут не иметь возможности использовать их. Таблица 3.4. Внутренние типы данных С# Сокращенный вариант обозначения в С# bool sbyte byte short ushort int uint long ulong char float double decimal Отвечает ли требованиям CLS Да Нет Да Да Нет Да Нет Да Нет Да Да Да Да Системный тип System.Boolean System.SByte System.Byte System.Int16 System.UIntl6 System.Int32 System.UInt32 System.Int64 System.UInt64 System.Char System.Single System.Double System.Decimal Диапазон значений true или false от-128 ДО 127 отО до 255 от -32 768 до 32 767 отО до 65 535 от-2 147 483 648 до 2 147 483 647 отО до 4 294 967 295 от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 отО до 18 446 744073 709 551615 от U+0000 дои+ffff от+1,5x105 до 3,4x1038 от 5,0x1024 до 1,7x10308 от ±1,0x1 Ое-28 до ±7,9x1028 Описание Представляет признак истинности или ложности 8-битное число со знаком 8-битное число без знака 16-битное число со знаком 16-битное число без знака 32-битное число со знаком 32-битное число без знака 64-битное число со знаком 64-битное число без знака Одиночный 16-битный символ Unicode 32-битное число с плавающей точкой 64-битное число с плавающей точкой 96-битное число со знаком
Глава 3. Главные конструкции программирования на С#: часть I 119 Окончание табл. 3.4 Сокращенный Отвечает вариант ли требова- Системный тип Диапазон значений Описание обозначения в С# . ниям CLS string Да System.String Ограничивается объе- Представляет ряд мом системной памяти символов в формате Unicode object Да System.Object Позволяет сохранять Служит базовым любой тип в объектной классом для всех переменной типов в мире .NET На заметку! По умолчанию число с плавающей точкой трактуется как относящееся к типу double. Из-за этого для объявления переменной типа float сразу после числового значения должен указываться суффикс f или F (например, 5. 3F). Неформатированные целые числа по умолчанию трактуются как относящиеся к типу данных int. Поэтому для типа данных long необходимо использовать суффикс 1 или L (например, 4L). Каждый из числовых типов, такой как short или int. отображается на соответствующую структуру в пространстве имен System. Структуры, попросту говоря, представляют собой типы значений, которые размещаются в стеке. Типы string и object, с другой стороны, являются ссылочными типами, а это значит, что данные, сохраняемые в переменных такого типа, размещаются в управляемой куче. Более подробно о типах значений и ссылочных типах будет рассказываться в главе 4. Пока что важно просто понять то, что типы-значения могут размещаться в памяти очень быстро и обладают фиксированным и предсказуемым временем жизни. , Объявление и инициализация переменных При объявлении локальной переменой (например, переменной, действующей в пределах какого-то члена) должен быть указан тип данных, за которым следует имя самой переменной. Чтобы посмотреть, как это выглядит, давайте создадим новый проект типа Console Application по имени BasicDataTypes и модифицируем класс Program так, чтобы в нем использовался следующий вспомогательный метод, вызываемый в Main (): static void LocalVarDeclarations() { Console.WriteLine("=> Data Declarations:"); // Объявления данных // Локальные переменные объявляются следующим образом: // типДанных имяПеременной; int mylnt; string myString; Console.WriteLine(); } Следует иметь в виду, что в случае использования локальной переменной до присваивания ей начального значения компилятор сообщит об ошибке. Поэтому рекомендуется всегда присваивать начальные значения локальным элементам данных во время их объявления. Делать это можно как в одной строке, так и в двух, разнося объявление и присваивание на два отдельных оператора кода. static void LocalVarDeclarations() { Console.WriteLine("=> Data Declarations:"); // Объявления данных
120 Часть II. Главные конструкции программирования на С# // Локальные переменные объявляются и инициализируются следующим образом: // типДанных имяПеременной = начальноеЗначение; int mylnt = 0; // Объявлять локальные переменные и присваивать им начальные // значения можно также в двух отдельных строках. string myString; myString = "This is my character data"; Console.WriteLine(); } Допускается объявление сразу нескольких переменных одинакового базового типа в одной строке кода, как показано ниже на примере трех переменных типа bool: static void LocalVarDeclarations () { Console.WriteLine ("=> Data Declarations:"); int mylnt = 0; string myString; myString = "This is my character data"; // Объявление трех переменных типа bool в одной строке. bool Ы = true, Ь2 = false, ЬЗ = Ы; Console.WriteLine(); } Поскольку ключевое слово bool в С# является сокращенным вариантом обозначения такой структуры из пространства имен System, как Boolean, размещать любой тип данных можно также с использованием его полного имени (естественно, это же касается всех остальных ключевых слов, представляющих типы данных в С#). Ниже приведена окончательная версия реализации LocalVarDeclarations (). static void LocalVarDeclarations () { Console.WriteLine ("=> Data Declarations:"); Console.WriteLine ( "=> Объявления данных:"); // Локальные переменные объявляются и инициализируются следующим образом: // типДанных имяПеременной = начальноеЗначение; int mylnt = 0; string myString; myString = "This is my character data"; // Объявление трех переменных типа bool в одной строке. bool Ы = true, Ь2 = false, ЬЗ = Ы; // Использование типа данных System для объявления переменной bool. System .'Boolean Ь4 = false; Console.WriteLine ("Your data: {0}, {1}, {2}, {3}, {4}, {5}", mylnt, myString, Ы, Ь2, ЬЗ, Ь4); Console.WriteLine(); } Внутренние типы данных и операция new Все внутренние (intrinsic) типы данных поддерживают так называемый конструктор по умолчанию (см. главу 5). Это позволяет создавать переменные за счет использования ключевого слова new и тем самым автоматически устанавливать для них значения, которые являются принятыми для них по умолчанию:
Глава 3. Главные конструкции программирования на С#: часть I 121 • значение false для переменных типа bool; • значение 0 для переменных числовых типов (или 0. О для типов с плавающей точкой); • одиночный пустой символ для переменных типа string; • значение 0 для переменных типа Biglnteger; • значение 1/1/0001 12 : 00 : 00 AM для переменных типа DateTime; • значение null для переменных типа объектных ссылок (включая string). На заметку! Упомянутый в предыдущем списке тип данных Biglnteger является нововведением в .NET 4.0 и будет более подробно рассматриваться далее в главе. Хотя код на С# в случае использования ключевого слова new при создании переменных базовых типов получается более громоздким, синтаксически он вполне корректен, как, например, приведенный ниже код: static void NewingDataTypes () { Console.WriteLine ("=> Using new to create variables:"); // Использование ключевого слова // new для создания переменных bool b = new bool (); // Установка в false. int i = new int() ; // Установка в 0. double d = new double () ; // Установка в 0. DateTime dt = new DateTime(); // Установка в 1/1/0001 12:00:00 AM Console.WriteLine ("{0}, {1}, {2}, {3}", b, l, d, dt) ; Console.WriteLine(); } Иерархия классов типов данных Очень интересно отметить то, что даже элементарные типы данных в .NET имеют вид иерархии классов. Если вы не знакомы с концепциями наследования, ищите всю необходимую информацию в главе 6. Пока важно усвоить лишь то, что типы, которые находятся в самом верху иерархии, обеспечивают некоторое поведение по умолчанию, которое передается унаследованным от них типам. На рис. 3.6 схематично показаны отношения между ключевыми системными типами. Обратите внимание, что каждый из этих типов в конечном итоге наследуется от класса System.Object, в котором содержится набор методов (таких как ToString (), Equals () и GetHashCode ()), являющихся общими для всех поставляемых в библиотеках базовых классов .NET типов (все эти методы подробно рассматриваются в главе 6). Также важно отметить, что многие из числовых типов данных унаследованы от класса System.ValueType. Потомки ValueType автоматически размещаются в стеке и потому обладают очень предсказуемым временем жизни и являются довольно эффективными. Типы, у которых в цепочке наследования не присутствует класс System. ValueType (вроде System.Type, System.String, System.Array, System.Exception и System. Delegate), в стеке не размещаются, а попадают в кучу и подвергаются автоматической сборке мусора. Не погружаясь глубоко в детали классов System. Object и System. ValueType, сейчас главное уяснить то, что поскольку любое ключевое слово в С# (например, int) представляет собой сокращенный вариант обозначения соответствующего системного типа (в данном случае System. Int32), приведенный ниже синтаксис является вполне допустимым.
122 Часть II. Главные конструкции программирования на С# Object Туре String Array Exception Delegate MulticastDelegate ValueType Любой тип, унаследованный от ValueType, является структурой или перечислением, а не классом. Boolean Byte Char Decimal Double Intl6 Int32 Int64 SByte Перечисления и структуры Uintl6 UInt32 UInt64 Void DateTime Guid TimeSpan Single Рис. З.6. Иерархия классов системных типов Причина в том, что тип System. Int32 (представляемый как int в С#) в конечном итоге все равно унаследован от класса System.Object и, следовательно, в нем может вызываться любой из его общедоступных членов с помощью такой вспомогательной функции. static void ObjectFunctionality () { Console.WriteLine("=> System.Object Functionality:"); // Функциональные возможности System.Object // Ключевое слово int в С# в действительности представляет // собой сокращенный вариант обозначения типа System.Int32, // который наследует от System.Object следующие члены: Console.WriteLine (2.GetHashCodeO = {0}", 12.GetHashCode() ) ; Console.WriteLine(2.EqualsB3) = {0}" , 12.EqualsB3) ) ; Console.WriteLine(2.ToStringO = {0}", 12.ToString()); Console.WriteLine(2.GetType() = {0}", 12.GetType()); Console.WriteLine();
Глава 3. Главные конструкции программирования на С#: часть I 123 На рис. 3.7 показано, как будет выглядеть вывод в случае вызова данного метода в Main (). ***** Fun with Basic Data Types *** :=> System.Object Functionality: 12.GetHashCodeO = 12 12.EqualsB3) = False 12.ToStringО = 12 12.GetType() = System.Int32 Press any key to continue . . . Рис. З.7. Все типы (даже числовые) расширяют класс System.Object Члены числовых типов данных Продолжая обсуждение встроенных типов данных в С#, нельзя не упомянуть о том, что числовые типы в .NET поддерживают свойства MaxValue и MinValue, которые позволяют получать информацию о диапазоне значений, хранящихся в данном типе. Помимо свойств MinValue/MaxValue каждый числовой тип может иметь и другие полезные члены. Например, тип System. Double позволяет получать значения эпсилон (бесконечно малое) и бесконечность (представляющие интерес для тех, кто занимается решением математических задач). Ниже для иллюстрации приведена соответствующая вспомогательная функция. static void DataTypeFunctionality () { Console.WriteLine("=> Data type Functionality:"); // Функциональные возможности типов данных // Максимальное значение типа int Console.WriteLine("Max of int: {0}", int.MaxValue); // Минимальное значение типа int Console.WriteLine ("Min of int: {0}", int.MinValue); // Максимальное значение типа double Console.WriteLine ("Max of double: {0} // Минимальное значение типа double Console.WriteLine ("Min of double: {0}1 double.MaxValue); double.MinValue); // Значение эпсилон типа double Console.WriteLine("double.Epsilon: {0}", double.Epsilon); // Значение плюс бесконечность типа double Consolе.WriteLine("double.Positivelnfinity: {0}", double.Positivelnfinity); // Значение минус бесконечность типа double Console.WriteLine("double.Negativelnfinity: { 0} ", double.Negativelnfinity); Console.WriteLine() ; Члены System.Boolean Теперь рассмотрим тип данных System.Boolean. Единственными значениями, которые могут присваиваться типу bool в С#, являются true и false. Нетрудно догадаться, что свойства MinValue и MaxValue в нем не поддерживаются, но зато поддерживаются такие свойства, как TrueString и FalseString (которые выдают, соответственно,
124 Часть II. Главные конструкции программирования на С# строку "True" и "False"). Чтобы стало понятнее, давайте добавим во вспомогательный метод DataTypeFunctionality () следующие операторы кода: Console.WriteLine("bool.Falsestring: {0 } ", bool.Falsestring); Console.WriteLine("bool.TrueString: {0}", bool.TrueString); На рис. 3.8 показано, как будет выглядеть вывод после вызова DataType Functionality () BMain(). | C:\Windo*j\system32\cmd.e4e ***** Fun with Basic Data Types ***** U> Data type Functionality: Max of int: 2147483647 Min of int: -2147483648 Max of double: 1.79769313486232E+308 Min of double: -1.79769313486232E+308 double.Epsilon: 4.94065645841247E-324 double.Posi tivelnfi ni ty: Infi ni ty double.Negativelnfinity: -Infinity bool.FalseString: False bool.TrueString: True Press any key to continue . . . Рис. З.8. Демонстрация избранных функциональных возможностей различных типов данных Члены System.Char Текстовые данные в С# представляются с помощью ключевых слов string и char, которые являются сокращенными вариантами обозначения типов System. String и System.Char (оба они основаны на кодировке Unicode). Как известно, string позволяет представлять непрерывный набор символов (например, "Hello"), a char — только конкретный символ в типе string (например, ' Н '). Помимо возможности хранить один элемент символьных данных, тип System. Char обладает массой других функциональных возможностей. В частности, с помощью его статических методов можно определять, является данный символ цифрой, буквой, знаком пунктуации или чем-то еще. Например, рассмотрим приведенный ниже метод. static void CharFunctionality () { Console.WriteLine ("=> char type Functionality:"); // Функциональные возможности типа char char myChar = ' a' ; Console.WriteLine("char.IsDigit ('a1) : {0}", char.IsDigit(myChar)); Console.WriteLine("char.IsLetter('a1): {0}", char.IsLetter(myChar)); Console.WriteLine("char.IsWhiteSpace ( 'Hello There1, 5): {0}", char.IsWhiteSpace("Hello There", 5) ) ; Console.WriteLine("char.IsWhiteSpace ( 'Hello There', 6): {0}", char.IsWhiteSpace ("Hello There", 6) ) ; Console.WriteLine("char.IsPunctuation('?'): {0}", char.IsPunctuation('?')); Console.WriteLine(); Как видно в этом фрагменте кода, многие из членов System.Char могут вызываться двумя способами: с указанием конкретного символа и с указанием целой строки с числовым индексом, ссылающимся на позицию, в которой находится проверяемый символ.
Глава 3. Главные конструкции программирования на С#: часть I 125 Синтаксический разбор значений из строковых данных Типы данных .NET предоставляют возможность генерировать переменную лежащего в их основе типа на основе текстового эквивалента (т.е. выполнять синтаксический разбор). Эта возможность чрезвычайно полезна при преобразовании некоторых из предоставляемых пользователем данных (например, значений, выбираемых в раскрывающемся списке внутри графического пользовательского интерфейса). Ниже приведен пример метода ParseFromStrings (), демонстрирующий выполнение синтаксического разбора. static void ParseFromStrings () { Console .WriteLine ( "=> Data type parsing:11); // Синтаксический разбор типов данных bool b = bool.Parse("True"); Console.WriteLine("Value of b: {0}", b); double d = double.Parse("99.884"); Console.WriteLine("Value of d: {0}", d) , int i = int.Parse("8"); Console.WriteLine("Value of i: {0}", l) , char с = Char.Parse("w") ; Console.WriteLine("Value of c: {0}", c) , Console.WriteLine(); } Типы System.DateTime и System. TimeSpan В пространстве имен System имеется несколько полезных типов данных, для которых в С# ключевых слов не предусмотрено. К этим типам относятся структуры DateTime и TimeSpan (а также показанные на рис. 3.6 типы System. Guid и System. Void, изучением которых можете заняться самостоятельно). В типе DateTime содержатся данные, представляющие конкретное значение даты (месяц, день, год) и времени, которые могут форматироваться различными способами с применением членов, доступных в этом типе. static void UseDatesAndTimes () { Console.WriteLine ("=> Dates and Times:"); // Отображение значений даты и времени // Этот конструктор принимает в качестве // аргументов сведения о годе, месяце и дне. DateTime dt = new DateTime B010, 10, 17); // Какой это день месяца? Console.WriteLine("The day of {0} is {1}", dt.Date, dt.DayOfWeek); // Сейчас месяц декабрь. dt = dt.AddMonthsB); Console.WriteLine("Daylight savings: {0}", dt.IsDaylightSavingTime()); // Этот конструктор принимает в качестве аргументов // сведения о часах, минутах и секундах. TimeSpan ts = new TimeSpan D, 30, 0) ; Console.WriteLine(ts); // Вычитаем 15 минут из текущего значения TimeSpan //и отображаем результат. Console.WriteLine(ts.Subtract(new TimeSpan @, 15, 0) ) ) ; }
126 Часть II. Главные конструкции программирования на С# Пространство имен System.Numerics в .NET 4.0 В версии .NET 4.0 предлагается новое пространство имен под названием System. Numerics, в котором определена структура по имени Biglnteger. Тип данных Biglnteger служит для представления огромных числовых значений (вроде национального долга США), не ограниченных ни верхней, ни нижней фиксированной границей. На заметку! В пространстве System. Numerics доступна и вторая структура по имени Complex, которая позволяет моделировать математически сложные числовые данные (такие как мнимые и реальные числа, или гиперболические тангенсы). Дополнительные сведения об этой структуре можно найти в документации .NET Framework 4.0 SDK. Хотя в большинстве приложений .NET необходимость в использовании структуры Biglnteger может никогда не возникать, если все-таки это случится, в первую очередь в свой проект нужно добавить ссылку на сборку System. Numerics . dll. Выполните следующие шаги. 1. В Visual Studio выберите в меню Project (Проект) пункт Add Reference (Добавить ссылку). 2. В открывшемся после этого окне перейдите на вкладку .NET. 3. Найдите и выделите сборку System. Numerics в списке представленных библиотек. 4. Щелкните на кнопке ОК. После этого добавьте следующую директиву в файл, в котором будет использоваться тип данных Biglnteger: // Здесь определен тип Biglnteger: using System.Numerics; Теперь можно создать переменную Biglnteger с использованием операции new. Внутри конструктора можно указать числовое значение, в том числе с плавающей точкой. Вспомните, что при определении целочисленный литерал (вроде 500) по умолчанию трактуется исполняющей средой как относящийся к типу int. а литерал с плавающей точкой (такой как 55.333) — как относящийся к типу double. Как же тогда установить для Biglnteger большое значение, не переполняя типы данных, которые используются по умолчанию для неформатированных числовых значений? Простейший подход состоит в определении большого числового значения в виде текстового литерала, который затем может быть преобразован в переменную типа Biglnteger с помощью статического метода Parse (). При необходимости можно также передать байтовый массив непосредственно конструктору класса Biglnteger. На заметку! После присваивания значения переменной Biglnteger изменять ее больше не разрешается, поскольку содержащиеся в ней данные являются неизменяемыми. Тем не менее, в классе Biglnteger предусмотрено несколько членов, которые возвращают новые объекты Biglnteger на основе модификаций надданными (вроде статического метода Multiply (), который будет использоваться в следующем примере кода). В любом случае после определения переменной Biglnteger обнаруживается, что в этом классе доступны члены, очень похожие на членов других внутренних типов данных в С# (например, float и int). Помимо этого в классе Biglnteger еще есть несколько статических членов, которые позволяют применять базовые математические выражения (такие как сложение и умножение) к переменным Biglnteger.
Глава 3. Главные конструкции программирования на С#: часть I 127 Ниже приведен пример работы с классом Biglnteger. static void UseBiglnteger() { Console.WriteLine("=> Use Biglnteger:"); // Использование Biglnteger Biglnteger biggy = Biglnteger.Parse("9999999999999999999999999999999 999 999999999999"); Console.WriteLine("Value of biggy is {0}", biggy); // Значение переменной biggy Console.WriteLine("Is biggy an even value?: {0}", biggy.IsEven); // Является ли значение biggy четным? Console.WriteLine("Is biggy a power of two?: {0}", biggy.IsPowerOfTwo); // Является ли biggy степенью двойки? Biglnteger reallyBig = Biglnteger.Multiply(biggy, Biglnteger.Parse ("88888888888888888888888 88888888888 6888888 88")); Console.WriteLine("Value of reallyBig is {0}", reallyBig); // Значение переменной reallyBig } Важно обратить внимание, что к типу данных Biglnteger применимы внутренние математические операции в С#, такие как +, -, и *. Следовательно, вместо того чтобы вызывать метод Biglnteger.MultiplyO для перемножения двух больших чисел, можно использовать такой код: Biglnteger reallyBig2 = biggy * reallyBig; К этому моменту уже должно быть ясно, что ключевые слова, представляющие базовые типы данных в С#, обладают соответствующим типами в библиотеках базовых классов .NET, и что каждый из этих типов предоставляет определенные функциональные возможности. Подробные описания всех членов этих типов данных можно найти в документации .NET Framework 4.0 SDK. Исходный код. Проект BasicDataTypes доступен в подкаталоге Chapter 3. Работа со строковыми данными В System. String предоставляется набор методов для определения длины символьных данных, поиска подстроки в текущей строке, преобразования символов из верхнего регистра в нижний и наоборот, и т.д. В табл. 3.5 перечислены некоторые наиболее интересные члены этого класса. Таблица 3.5. Избранные члены класса System. String Член Описание Length Свойство, которое возвращает длину текущей строки Compare () Статический метод, который позволяет сравнить две строки Contains () Метод, который позволяет определить, содержится ли в строке определенная подстрока Equals () Метод, который позволяет проверить, содержатся ли в двух строковых объектах идентичные символьные данные Format () Статический метод, позволяющий сформатировать строку с использованием других элементарных типов данных (например, числовых данных или других строк) и обозначений типа {0}, о которых рассказывалось ранее в этой главе
128 Часть II. Главные конструкции программирования на С# Окончание табл. 3.5 Член Описание Insert () Метод, который позволяет вставить строку внутрь другой определенной строки PadLef t () Методы, которые позволяют дополнить строку какими-то символами, PadRight () соответственно, справа или слева Remove () Методы, которые позволяют получить копию строки с соответствующими Replace () изменениями (удалением или заменой символов) Split () Метод, возвращающий массив string с присутствующими в данном экземпляре подстроками внутри, которые отделяются друг от друга элементами из указанного массива char или string Trim () Метод, который позволяет удалять все вхождения определенного набора символов с начала и конца текущей строки ToUpper () Методы, которые позволяют создавать копию текущей строки в формате, ToLower () соответственно, верхнего или нижнего регистра Базовые операции манипулирования строками Работа с членами System. String выглядит так, как и следовало ожидать. Все, что требуется — это просто объявить переменную типа string и воспользоваться предоставляемой этим типом функциональностью через операцию точки. Однако при этом следует иметь в виду, что некоторые из членов System. String представляют собой статические методы и потому должны вызываться на уровне класса (а не объекта). Для примера давайте создадим новый проект типа Console Application по имени FunWithStrings, добавим в него следующий метод и вызовем его в Main () : static void BasicStringFunctionality () { Console.WriteLme ("=> Basic String functionality:11); // Базовые функциональные возможности типа String string firstName = "Freddy"; Console. WriteLme ("Value of firstName: { 0 } " , firstName); // Значение переменной firstName Console .WriteLme ("firstName has {0} characters.", firstName .Length) ; // Длина значения переменной firstname Console. WriteLme ("firstName in uppercase: {0}", {0}", firstName .ToUpper ()) ; // Значение переменной firstName в верхнем регистре Console .WriteLme ( "firstName in lowercase: {0}", {0}", firstName .ToLower ()) ; // Значение переменной firstName в нижнем регистре Console .WriteLme ("firstName contains the letter y? : {0}", {0}", // Содержитеч ли в значении firstName буква у firstName.Contains("у")); Console.WriteLme ("firstName after replace: {0}", firstName.Peplace ("dy", "") ) ; // Значение firstName после замены Console . WriteLme () ; } Здесь объяснять особо нечего: в приведенном методе на локальной переменной типа string производится вызов различных членов вроде ToUpper () и Contains () для получения различных форматов и выполнения различных преобразований. На рис. 3.9 показано, как будет выглядеть вывод.
Глава 3. Главные конструкции программирования на С#: часть I 129 | CWindowj\iystem32\cmd.exe Т ' _i | П^^Д'га^Г I***** Fun with Strings ***** U> Basic String functionality: |lvalue of firstName: Freddy TfirstName has 6 characters. firstName in uppercase: FREDDY rfirstName in lowercase: freddy rfirstName contains the letter y?: firstName after replace: Fred Рис. З.9. Базовые операции манипулирования строками Вывод, получаемый в результате вызова метода Replace (), может привести в некоторое замешательство. Переменная f irstName на самом деле не изменилась, а вместо этого метод возвращает обратно новую строку в измененном формате. Мы еще вернемся к неизменяемой природе строк немного позже, после исследования ряда других моментов. Конкатенация строк Переменные string могут сцепляться вместе для создания строк большего размера с помощью такой поддерживаемой в С# операции, как +. Как известно, подобный прием называется конкатенацией строк. Для примера рассмотрим следующую вспомогательную функцию: static void StringConcatenation () { Console. WriteLine ("=> String concatenation:11); // Конкатенация строк string si = "Programming the " ; string s2 = "PsychoDrill (PTP)"; string s3 = si + s2; Console.WriteLine (s3) ; Console.WriteLine (); } Возможно, будет интересно узнать, что символ + в С# при обработке компилятором приводит к добавлению вызова статического метода String. Concat (). После компиляции приведенного выше кода и открытия результирующей сборки в утилите ildasm. ехе (см. главу 1) можно будет увидеть такой CIL-код, как показан на рис. 3.10. ffl FunWithStrings.Prognim:^StringConcetenetion: voidQ Find Find Next IL 000b: nop IL_Mtc: ldstr IL 0011: stlOC.t IL_W2: ldstr IL_M17: stloc.1 IL 0018: ldlOC.e IL 0019: ldloc.1 =1 "Programing the " "PsychoDrill (PTP)" IL_M1f: IL 0020: stloc.2 ldloc.2 nil 11mn run Рис. 3.10. Операция + в С# приводит к добавлению вызова метода String. Concat () По этой причине конкатенацию строк также можно выполнять вызовом метода String. Concat () напрямую (что на самом деле особых преимуществ не дает, а фактически лишь увеличивает количество подлежащих вводу строк кода):
130 Часть II. Главные конструкции программирования на С# static void StringConcatenation () { Console.WriteLine("=> String concatenation:"); string si = "Programming the " ; string s2 = "PsychoDrill (РТР)"; ^ string s3 = String.Concat(si, s2) ; Console.WriteLine(s3); Console.WriteLine() ; } Управляющие последовательности символов Как и в других языках на базе С, в С# строковые литералы могут содержать различные управляющие последовательности символов (escape characters), которые позволяют уточнять то, как символьные данные должны выводиться в выходном потоке. Начинается каждая такая управляющая последовательность с символа обратной косой черты, за которым следует интерпретируемый знак. В табл. 3.6 перечислены некоторые часто применяемые управляющие последовательности. Таблица 3.6. Управляющие последовательности, которые могут применяться в строковых литералах Управляющая 0писа последовательность \ ' Вставляет в строковый литерал символ одинарной кавычки \ " Вставляет в строковый литерал символ двойной кавычки \\ Вставляет в строковый литерал символ обратной косой черты. Может быть полезной при определении путей к файлам и сетевым ресурсам \а Заставляет систему выдавать звуковой сигнал, который в консольных приложениях может служить своего рода звуковой подсказкой пользователю \п Вставляет символ новой строки (на платформах Windows) \г Вставляет символ возврата каретки \t Вставляет в строковый литерал символ горизонтальной табуляции Например, если необходимо, чтобы в выводимой строке после каждого слова шел символ табуляции, можно воспользоваться управляющей последовательностью \t. Если нужно создать один строковый литерал с символами кавычек внутри, другой — с определением пути к каталогу и третий со вставкой трех пустых строк после вывода символьных данных, можно применить такие управляющие последовательности, как \ ", \ \ и \п. Кроме того, ниже приведен еще один пример, в котором для привлечения внимания каждый строковый литерал снабжен звуковым сигналом. static void EscapeChars () { Console.WriteLine ("=> Escape characters:\a"); string strWithTabs = "Model\tColor\tSpeed\tPet Name\a "; Console.WriteLine(strWithTabs); Console.WriteLine("Everyone loves V'Hello World\"\a "); Console.WriteLine("C:\\MyApp\\bin\\Debug\a ") ; // Добавить 4 пустых строки и снова выдать звуковой сигнал. Console.WriteLine("All finished.\n\n\n\a "); Console.WriteLine ();
Глава 3. Главные конструкции программирования на С#: часть I 131 Определение дословных строк За счет добавления к строковому литералу префикса @ можно создавать так называемые дословные строки (verabtim string). Дословные строки позволяют отключать обработку управляющих последовательностей в литералах и выводить объекты string в том виде, в каком они есть. Эта возможность наиболее полезна при работе со строками, представляющими пути к каталогам и сетевым ресурсам. Таким образом, вместо использования управляющей последовательности \ \ можно написать следующий код: // Следующая строка будет воспроизводиться дословно, //т.е. с отображением всех управляющих // последовательностей символов. Console.WriteLine(@"C:\MyApp\bin\Debug"); Также важно отметить, что дословные строки могут применяться для сбережения пробелов в строках, разнесенных на несколько строк: // При использовании дословных строк пробелы сохраняются. string myLongString = @"This is a very very very long string"; Console.WriteLine(myLongString); С использованием дословных строк можно также напрямую вставлять в литералы символы двойной кавычки, просто дублируя лексему ": Console. WriteLine (@"Cerebus said ""Darrr! Pret-ty sun-sets1111"); Строки и равенство Как будет подробно объясняться в главе 4, ссылочный тип (reference type) представляет собой объект, размещаемый в управляемой куче, которая подвергается автоматическому процессу сборки мусора. По умолчанию при выполнении проверки на предмет равенства ссылочных типов (с помощью таких поддерживаемых в С# операций, как == и ! =) значение true будет возвращаться в том случае, если обе ссылки указывают на один и тот же объект в памяти. Хотя string представляет собой ссылочный тип, операции равенства для него были переопределены так, чтобы давать возможность сравнивать значения объектов string, а не сами объекты в памяти, на которые они ссылаются. static void StringEquality () { Console.WriteLine("=> String equality:"); // Равенство строк string si = "Hello!"; string s2 = "Yo'"; Console.WriteLine ("si = {0}", si); Console.WriteLine ("s2 = {0}", s2) ; Console.WriteLine (); // Выполнение проверки на предмет равенства данных строк. Console.WriteLine ("si == s2: {0}", si == s2) ; Console.WriteLine ("si == Hello!: {0}", si == "Hello!"); Console.WriteLine ("si == HELLO!: {0}", si == "HELLO!"); Console.WriteLine ("si == hello!: {0}", si == "hello!"); Console.WriteLine("si.Equals (s2) : {0}", si.Equals (s2)); Console.WriteLine("Yo.Equals (s2) : {0}", "Yo!".Equals(s2)); Console.WriteLine (); }
132 Часть II. Главные конструкции программирования на С# В С# операции равенства предусматривают выполнение в отношении объектов string посимвольной проверки с учетом регистра. Следовательно, строки "Hello! ", "HELLO ! " и "hello !" не равны между собой. Кроме того, из-за наличия у string связи с System. String, проверку на предмет равенства можно выполнять также с помощью поддерживаемого классом String метода Equals () и других поставляемых в нем операций. И, наконец, поскольку каждый строковый литерал (например, "Yo") является самым настоящим экземпляром System. String, доступ к функциональным возможностям для работы со строками можно получать также для фиксированной последовательности символов. Неизменная природа строк Один из интересных аспектов System. String состоит в том, что после присваивания объекту string первоначального значения символьные данные больше изменяться не могут. На первый взгляд это может показаться заблуждением, ведь строкам постоянно присваиваются новые значения, а в типе System. String доступен набор методов, которые, похоже, только то и делают, что позволяют изменять символьные данные тем или иным образом (например, преобразовывать их в верхний или нижний регистр). Если, однако, присмотреться внимательнее к тому, что происходит "за кулисами", то можно будет увидеть, что методы типа string на самом деле возвращают совершенно новый объект string в измененном виде: static void StringsArelmmutable () { // Установка первоначального значения для строки. string si = "This is my string."; Console.WriteLine ("si = {0}", si); // Преобразование si в верхний регистр? string upperString = si.ToUpper(); Console.WriteLine("upperString = {0}", upperString); // Нет! si по-прежнему остается в том же формате! Console.WriteLine("si = {0}", si); } На рис. 3.11 показано, как будет выглядеть вывод приведенного выше кода, по которому легко убедиться в том, что исходный объект string (si) не преобразуется в верхний регистр при вызове ToUpper (), а вместо этого возвращается его копия в измененном соответствующим образом формате. Рис. 3.11. Строки остаются неизменными Тот же самый закон неизменности строк действует и при использовании в С# операции присваивания. Чтобы удостовериться в этом, давайте закомментируем (или удалим) весь существующий код в StringsArelmmutable () (чтобы уменьшить объем генерируемого CIL-кода) и добавим следующие операторы:
Глава 3. Главные конструкции программирования на С#: часть I 133 static void StringArelmmutable () { string s2 = "My other string"; s2 = "New string value"; } Скомпилируем приложение и загрузим результирующую сборку в утилиту ildasm.exe (см. главу 1). На рис. 3.12 показан CIL-код, который будет сгенерирован для метода StringsArelmmutable (). Г /7 FunWithStrings.Program:5trmgsAreImmut»bte: votdO LEili^LJeSel 1 PRndi Findltort ~ .method priuate hidebysig static uoid StringsArelmmutableO cil managed I !< I // Code size 21 @x15) I.maxstack 1 .locals init ([0] string s2) II 0000: nop IL 0001: ldstr "Ну other string" IL_O0M: stloc.O IL 0007: ldstr "New string ualue" IL 000c: stloc.O IL OOOd: ldloc.8 IL_tlte: call uoid [mscorlib]System.Console::WriteLine(string) IL_0013: nop IL_M14: ret I i> // end of method Program::StringsflreImmutable Рис. 3.12. Присваивание значения объекту string приводит к созданию нового объекта string Хотя низкоуровневые детали CIL пока подробно не рассматривались, важно обратить внимание на наличие многочисленных вызов кода операции ldstr (загрузка строки). Этот код операции ldstr в CIL предусматривает выполнение загрузки нового объекта string в управляемую кучу. В результате предыдущий объект, в котором содержалось значение "My other string", будет в конечном итоге удален сборщиком мусора. Так что же конкретно необходимо вынести из всего этого? Если кратко: класс string может оказываться неэффективным и приводить к "разбуханию" кода в случае неправильного использования, особенно при выполнении конкатенации строк. Когда же необходимо представлять базовые символьные данные, такие как номер карточки социального страхования, имя и фамилия или простые фрагменты текста, используемые внутри приложения, он является идеальным вариантом. В случае создания приложения, предусматривающего интенсивную работу с текстовыми данными, представление обрабатываемых данных с помощью объектов string будет очень плохой идеей, поскольку практически наверняка (и часто не напрямую) будет приводить к созданию ненужных копий данных string. Как тогда должен поступать программист? Ответ на этот вопрос ищите ниже. Тип System.Text.StringBuilder Из-за того, что тип string может оказаться неэффективным при необдуманном использовании, в библиотеках базовых классов .NET поставляется еще пространство имен System.Text. Внутри этого (достаточно небольшого) пространства имен предлагается класс по имени StringBuilder. Как и в классе System. String, в StringBuilder содержатся методы, которые позволяют, например, заменять и форматировать сегменты. Чтобы использовать этот класс в файлах кода на С#, первым делом необходимо позаботиться об импорте следующего пространства имен: // Здесь определен класс StringBuilder: using System.Text;
134 Часть II. Главные конструкции программирования на С# Уникальным в StringBuilder является то, что при вызове его членов производится непосредственное изменение внутренних символьных данных объекта (что, конечно же, более эффективно), а не получение копии этих данных в измененном формате. При создании экземпляра StringBuilder начальные значения для объекта можно задавать с помощью не одного, а нескольких конструкторов. Тем, кто не знаком с понятием конструктора, сейчас важно уяснить лишь то, что конструкторы позволяют создавать объект с определенным начальным состоянием за счет применения ключевого слова new. Ниже приведен пример применения StringBuilder. static void FunWithStringBuilder () { Console.WriteLine ("=> Using the StringBuilder:"); StringBuilder sb = new StringBuilder("**** Fantastic Games ****"); sb.Append("\n"); sb.AppendLine("Half Life"); sb.AppendLine("Beyond Good and Evil"); sb.AppendLine("Deus Ex 2"); sb.AppendLine("System Shock") ; Console.WriteLine(sb.ToString()); sb.Replace(", "Invisible War"); Console.WriteLine(sb.ToString()); Console.WriteLine("sb has {0} chars.", sb.Length); . Console.WriteLine(); } Здесь сначала создается объект StringBuilder с первоначальным значением н**** Fantastic Games****". Далее можно добавлять символы к внутреннему буферу, а также заменять (или удалять) каким угодно образом. По умолчанию изначально в StringBuilder может храниться строка длиной не более 16 символов (она автоматически расширяется по мере необходимости), однако это исходное значение легко изменить, передавая конструктору соответствующий дополнительный аргумент: // Создание объекта StsingBuilder с исходным // размером в 256 символов. StringBuilder sb = new StringBuilder("**** Fantastic Games ****", 256); В случае добавления большего количества символов, чем было указано в качестве лимита, объект StringBuilder будет копировать свои данные в новый экземпляр и создавать для него буфер размером, равным указанному лимиту. На рис. 3.13 показан вывод приведенной выше вспомогательной функции. | C:\Windows\system32\c/nd.exe ***** Fun with Strings ***** j=> Using the StringBuilder: !**** Fantastic Games **** Half Life Beyond Good and Evil Deus Ex 2 , System Shock J**** Fantastic Games **** pHalf Life pBeyond Good and Evil Deus Ex Invisible War System Shock ;b as 96 chars. Рис. 3.13. Класс StringBuilder работает более эффективно, чем string
Глава 3. Главные конструкции программирования на С#: часть I 135 Исходный код. Проект FunWithStrings доступен в подкаталоге Chapter 3. Сужающие и расширяющие преобразования типов данных Теперь, когда известно, как взаимодействовать со встроенными типами данных, давайте рассмотрим связанную тему — преобразование типов данных. Создадим новый проект типа Console Application по имени TypeConversions и определим в нем следующий класс: class Program { static void Main(string[ ] args) { Console.WriteLine ("***** Fun with type conversions *****"); // Добавление двух переменных типа short //и отображение результата. short numbl = 9, numb2 = 10; Console.WriteLine("{0} + {1} = {2}", numbl, numb2, Add(numbl, numb2)); Console.ReadLine(); } static int Add(int x, int y) { return x + y; } } Обратите внимание на то, что метод Add () ожидает поступления двух параметров типа int. Тем не менее, в методе Main.() ему на самом деле передаются две переменных типа short. Хотя это может показаться несоответствием типов, программа будет компилироваться и выполняться без ошибок и возвращать в результате, как и ожидалось, значение 19. Причина, по которой компилятор будет считать данный код синтаксически корректным, связана с тем, что потеря данных здесь невозможна. Поскольку максимальное значение C2 767), которое может содержать тип short, вполне вписывается в рамки диапазона типа int (максимальное значение которого составляет 2 147 483 647), компилятор будет неявным образом расширять каждую переменную типа short до типа int. Формально термин "расширение" применяется для обозначения неявного восходящего приведения (upward cast), которое не приводит к потере данных. На заметку! Расширяющие и сужающие преобразования, поддерживаемые для каждого типа данных в С#, описаны в разделе "Type Conversion Tables" ("Таблицы преобразования типов") документации .NET Framework 4.0 SDK. Хотя в предыдущем примере подобное неявное расширение типов было полезно, в других случаях оно может стать источником возникновения ошибок компиляции. Например, давайте установим для numbl и numb2 значения, которые (при сложении вместе) будут превышать максимальное значение short, а также сделаем так, чтобы значение, возвращаемое методом Add (), сохранялось в новой локальной переменной short, а не просто напрямую выводилось в окне консоли: static void Main(string [ ] args) { Console.WriteLine("***** Fun with type conversions *****");
136 Часть II. Главные конструкции программирования на С# // Следующий код вызовет ошибку компиляции! short numbl = 30000, numb2 = 30000; short answer = Add(numbl, numb2); Console.WriteLine ("{0} + {1} = {2}", numbl, numb2, answer); Console.ReadLine(); } В таком случае компилятор сообщит об ошибке: Cannot implicitly convert type 'int' to 'short1. An explicit conversion exists (are you missing a cast?) He удается неявным образом преобразовать тип 'int* в 'short'. Существует возможность выполнения преобразования явным образом (не была ли пропущена операция по приведению типов?) Проблема в том, что хотя метод Add () способен возвращать переменную int со значением 60000 (поскольку это значение вполне вписывается в диапазон допустимых значений типа System. Int32), сохранение этого значения в переменной типа short невозможно, потому что оно выходит за рамки допустимого диапазона для этого типа. Формально это означает, что CLR-среде не удастся применить операцию сужения. Как не трудно догадаться, операция сужения представляет собой логическую противоположность операции расширения, поскольку предусматривает сохранение большего значения внутри переменной меньшего типа данных. Важно отметить, что все сужающие преобразования приводят к выдаче компилятором ошибки, даже когда имеются веские основания полагать, что операция сужающего преобразования должна на самом деле пройти успешно. Например, следующий код тоже приведет к генерации компилятором ошибки: // Еще один код, при выполнении которого // компилятор будет сообщать об ошибке! static void NarrowingAttempt() { byte myByte = 0; int mylnt = 200; myByte = mylnt; Console.WriteLine("Value of myByte: {0}", myByte); } Здесь значение, содержащееся в переменной типа int (по имени mylnt), вписывается в диапазон допустимых значений типа byte, следовательно, операция сужения по идее не должна приводить к генерации ошибки во время выполнения. Однако из-за того, что язык С# создавался с таким расчетом, чтобы он заботился о безопасности типов, компилятор все-таки сообщит об ошибке. Если нужно уведомить компилятор о готовности мириться с возможной в результате операции сужения потерей данных, необходимо применить операцию явного приведения типов, которая в С# обозначается с помощью (). Ниже показан модифицированный код Program, а на рис. 3.14 — его вывод. class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Fun with type conversions *****"); short numbl = 30000, numb2 = 30000; // Явное приведение int к short (с разрешением потери данных) . short answer = (short)Add(numbl, numb2); Console.WriteLine ("{0} + {1} = {2}", numbl, numb2, answer);
Глава 3. Главные конструкции программирования на С#: часть I 137 NarrowingAttempt(); Console.ReadLine(); } static int Add(int x, int y) { return x .+ y; } static void NarrowingAttempt () { byte myByte = 0; int mylnt = 200; // Явное приведение int к byte (без потери данных). myByte = (byte)mylnt; Console.WriteLine("Value of myByte: {0}", myByte); | C:\Windows\system32\cmd.e> 1—**T.r.^^ffii^HP ***** Fun with type conversions ***** 30000 + 30000 = -5536 Value of myByte: 200 Eress any key to continue . . . m ■■"i '"' -7iiH Рис. 3.14. При сложении чисел некоторые данные были потеряны Перехват сужающих преобразований данных Как было только что показано, явное указание операции приведения заставляет компилятор производить операцию сужающего преобразования даже тогда, когда это чревато потерей данных. В методе NarrowingAttempt () это не было проблемой, поскольку значение 200 вписывается в диапазон допустимых значений типа byte. Однако при сложении двух значений типа short в методе Main () конечный результат оказался совершенно не приемлемым C0 000 + 30 000 = -5536?). Для создания приложений, в которых потеря данных должна быть недопустимой, в С# предлагаются такие ключевые слова, как checked и unchecked, которые позволяют гарантировать, что потеря данных не окажется незамеченной. Чтобы посмотреть, как применяются эти ключевые слова, давайте добавим в Program новый метод, суммирующий две переменных типа byte, каждой из которых присвоено значение, не выходящее за пределы допустимого максимума B55 для данного типа). По идее, после сложения значений этих двух переменных (с приведением результата int к типу byte) должна быть получена точная сумма. static void ProcessBytes () { byte Ы = IOC- byte Ь2 = 250; byte sum = (byte)Add(bl, b2); // В sum должно содержаться значение 350. // Однако там оказывается значение 94! Console.WriteLine("sum = {0}", sum); } Удивительно, но при изучении вывода данного приложения обнаруживается, что в sum содержится значение 94 (а не 350, как ожидалось). Объясняется это очень просто. Из-за того, что в System.Byte может храниться только значение из диапазона от 0 до 255 включительно, в sum будет помещено значение переполнения C50 - 256 = 94).
138 Часть II. Главные конструкции программирования на С# По умолчанию, в случае, когда не предпринимается никаких соответствующих исправительных мер, условия переполнения (overflow) и потери значимости (underflow) происходят без выдачи ошибки. Обрабатывать условия переполнения и потери значимости в приложении можно двумя способами. Это можно делать вручную, полагаясь на свои знания и навыки в области программирования. Недостаток такого подхода в том, что даже в случае приложения максимальных усилий человек все равно остается человеком, и какие-то ошибки могут ускользнуть от его глаз. К счастью, в С# предусмотрено ключевое слово checked. Если оператор (или блок операторов) заключен в контекст checked, компилятор С# генерирует дополнительные CIL-инструкции, обеспечивающие проверку на предмет условий переполнения, которые могут возникать в результате сложения, умножения, вычитания или деления двух числовых типов данных. В случае возникновения условия переполнения во время выполнения будет генерироваться исключение System.OverflowException. Детали обеспечения структурированной обработки исключения и использования в связи с этим ключевых слов try и catch будут даны в главе 7. А пока, не вдаваясь особо в детали, можно изучить показанный ниже модифицированный код: static void ProcessBytes () { byte Ы = IOC- byte Ь2 = 250; //На этот раз компилятору указывается добавлять CIL-код, // необходимый для выдачи исключения в случае возникновения // условий переполнения или потери значимости. try { byte sum = checked ( (byte) Add (Ы, b2) ) ; Console.WriteLine("sum = {0}", sum); } catch (OverflowException ex) { Console.WriteLine(ex.Message); } } Здесь следует обратить внимание на то, что возвращаемое значение метода Add () было заключено в контекст checked. Благодаря этому, в связи с выходом значения sum за пределы диапазона допустимых значений типа byte во время выполнения теперь будет генерироваться исключение, а через свойство Message выводиться сообщение об ошибке, как показано на рис. 3.15. | C\Winde*s\systerri32\cmd I***** Fun with type conversions [30000 + 30000 = -5536 'alue of myByte: 200 rithmetic operation resulted in an overflow. Press any key to continue . . . Рис. 3.15. Ключевое слово checked вынуждает CLR-среду генерировать исключения в случае потери данных Если проверка на предмет возникновения условий переполнения должна выполняться не для одного, а для целого блока операторов, контекст checked можно определить следующим образом:
Глава 3. Главные конструкции программирования на С#: часть I 139 try { checked { byte sum = (byte) Add (Ы, b2) ; Console.WriteLine("sum = {0}" sum) ; catch (OverflowException ex) { Console.WriteLine(ex.Message); } И в том и в другом случае интересующий код будет автоматически проверяться на предмет возникновения возможных условий переполнения, и при обнаружении таковых приводить к генерации соответствующего исключения. Настройка проверки на предмет возникновения условий переполнения в масштабах проекта Если создается приложение, в котором переполнение никогда не должно проходить незаметно, может выясниться, что обрамлять ключевым словом checked приходится раздражающе много строк кода. На такой случай в качестве альтернативного варианта в компиляторе С# поддерживается флаг /checked. При активизации этого флага проверке на предмет возможного переполнения будут автоматически подвергаться все имеющиеся в коде арифметические операции, без применения для каждой из них ключевого слова checked. Обнаружение переполнения точно так же приводит к генерации соответствующего исключения во время выполнения. Для активизации этого флага в Visual Studio 2010 необходимо открыть страницу свойств проекта, перейти на вкладку Build (Сборка), щелкнуть на кнопке Advanced (Дополнительно) и в открывшемся диалоговом окне отметить флажок Check for arithmetic overflow/underflow (Выполнять проверку на предмет арифметического переполнения и потери значимости), как показано на рис. 3.16. General -- ——-— Language Version: Internal Compiler Error Reporting: j prompt ! default f?j Check for arithmetic overflow/underflow [_=] Do not reference mscoriib.dll Debug Info: Fife Alignment DLL Base Address: Г"^П Рис. 3.16. Включение функции проверки на предмет переполнения и потери значимости в масштабах всего проекта Ключевое слово unchecked Теперь давайте посмотрим, что можно сделать, если функция проверки на предмет переполнения и потери значимости в масштабах всего проекта включена, но есть ка-
140 Часть II. Главные конструкции программирования на С# кой-то блок кода, в котором потеря данных является допустимой. Из-за того, что действие флага /checked распространяется на всю арифметическую логику, в С# предусмотрено ключевое слово unchecked, которое позволяет отключить выдачу связанного с переполнением исключения в отдельных случаях. Применяется это ключевое слово похожим на checked образом, поскольку может быть указано как для одного оператора, так и для целого блока: // При условии, что флаг /checked активизирован, этот // блок не будет приводить к генерации исключения во время выполнения. unchecked { byte sum = (byte) (bl + Ь2) ; Console.WriteLine("sum = { 0} ", sum); } Итак, чтобы подвести итог по использованию в С# ключевых слов checked и unchecked, следует отметить, что по умолчанию арифметическое переполнение в исполняющей среде .NET игнорируется. Если необходимо обработать отдельные операторы, то должно использоваться ключевое слово checked, а если нужно перехватывать все связанные с переполнением ошибки в приложении, то понадобится активизировать флаг /checked. Что касается ключевого слова unchecked, то его можно применять при наличии блока кода, в котором переполнение является допустимым (и, следовательно, не должно приводить к генерации исключения во время выполнения). Роль класса System. Convert В завершении темы преобразования типов данных стоит отметить, что в пространстве имен System имеется класс по имени Convert, который тоже может применяться для расширения и сужения данных: static void NarrowWithConvert () { byte myByte = 0; int mylnt = 200; . myByte = Convert.ToByte(mylnt); Console.WriteLine("Value of myByte: {0}", myByte); } Одно из преимуществ подхода с применением класса System.Convert связано с тем, что он позволяет выполнять преобразования между типами данных нейтральным к языку образом (например, синтаксис приведения типов в Visual Basic полностью отличается от предлагаемого для этой цели в С#). Однако, поскольку в С# есть операция явного преобразования, использование класса Convert для преобразования типов данных обычно является делом вкуса. Исходный код. Проект TypeConversions доступен в подкаталоге Chapter 3. Неявно типизированные локальные переменные Вплоть до этого момента в настоящей главе при определении локальных переменных тип данных, лежащий в их основе, всегда указывался явно: static void DeclareExplicitVars () { // Явно типизированные локальные переменные // объявляются следующим образом: // dataType variableName = mitialValue;
Глава 3. Главные конструкции программирования на С#: часть I 141 int mylnt = 0; bool myBool = true; string myString = "Time, marches on..." ; } Хотя указывать явным образом тип данных для каждой переменной всегда считается хорошим стилем, в С# также поддерживается возможность неявной типизации локальных переменных с помощью ключевого слова var. Ключевое слово var можно использовать вместо указания конкретного типа данных (такого как int, bool или string). В этом случае компилятор автоматически выводит лежащий в основе тип данных на основе первоначального значения, которое используется для инициализации локальных данных. Чтобы посмотреть, как это выглядит на практике, давайте создадим новый проект типа Console Application (Консольное приложение) по имени ImplicitlyTypedLocalVars и объявим в нем те же локальные переменные, что использовались в предыдущем методе, следующим образом: static void DeclarelmplicitVars () { // Неявно типизированные локальные переменные объявляются следующим образом: // var variableName = initialValue; var mylnt = 0; var myBool = true; var myString = "Time, marches on..."; } На заметку! На самом деле лексема var ключевым словом в С# не является. С ее помощью можно объявлять переменные, параметры и поля и не получать никаких ошибок на этапе компиляции. При использовании этой лексемы в качестве типа данных, однако, она по контексту воспринимается компилятором как ключевое слово. Поэтому ради простоты здесь будет применяться термин "ключевое слово var", а не более сложное понятие "контекстная лексема var". В этом случае компилятор имеет возможность вывести по первоначально присвоенному значению, что переменная mylnt в действительности относится к типу System. Int32, переменная myBool — к типу System.Boolean, а переменная myString — к типу System. String. Чтобы удостовериться в этом, можно вывести имя типа каждой из этих переменных посредством рефлексии. Как будет показано в главе 15, под рефлексией понимается процесс определения состава типа во время выполнения. Например, с помощью рефлексии можно определить тип данных неявно типизированной локальной переменной. Для этого модифицируем наш метод, добавив в его код следующие операторы: static void DeclarelmplicitVars () { // Неявно типизированные локальные переменные. var mylnt = 0; var myBool = true; var myString = "Time, marches on..."; // Вывод имен типов, лежащих в основе этих переменных. Console.WriteLine("mylnt is a: {0}", mylnt.GetType().Name); Console.WriteLine("myBool is a: {0}", myBool.GetType().Name); Console.WriteLine("myString is a: {0}", myString.GetType().Name); } На заметку! Следует иметь в виду, что такую неявную типизацию можно использовать для любых типов, включая массивы, обобщенные типы (см. главу 10) и пользовательские специальные типы. Далее в книге будут встречаться и другие примеры применения неявной типизации.
142 Часть II. Главные конструкции программирования на С# Если теперь вызвать метод DeclareImplicitVars () в Main (), то получится вывод, показанный на рис. 3.17. ЯВ C:\Windows\system32Vcmdexe ^mksiMia^ ***** Fun with Implicit Typing ***** Ptorvlnt is a: Int32 ool is a: Boolean tring is a: String jPress any key to continue .... E Рис. 3.17. Применение рефлексии в отношении неявно типизированных локальных переменных Ограничения, связанные с неявно типизированными переменными Разумеется, с использованием ключевого слова var связаны различные ограничения. Самое первое и важное из них состоит в том, что неявная типизация применима только для локальных переменных в контексте какого-то метода или свойства. Применять ключевое слово var для определения возвращаемых значений, параметров или данных полей специального типа не допускается. Например, ниже показано определение класса, которое приведет к выдаче сообщений об ошибках на этапе компиляции: class ThisWillNeverCompile { // Ошибка! var не может применяться // для определения полей! private var mylnt = 10; // Ошибка! var не может применяться // для определения возвращаемого значения // или типа параметра! public var MyMethod (var x, var y) {} Кроме того, локальным переменным, объявленным с помощью ключевого слова var, обязательно должно быть присвоено начальное значение в самом объявлении, причем присваивать в качестве начального значения null не допускается! Последние ограничение вполне понятно, поскольку на основании одного лишь значения null компилятор не сможет определить, на какой тип в памяти указывает переменная. // Ошибка! Должно быть присвоено значение! var myData; // Ошибка! Значение должно присваиваться в самом объявлении* var mylnt; mylnt = 0; // Ошибка! Присваивание null в качестве // начального значения не допускается! var myObj = null; Присваивание значения null локальной переменной с выведенным после начального присваивания типом вполне допустимо (при условии, переменная отнесена к ссылочному типу): // Все в порядке, поскольку SportsCar // является переменной ссылочного типа! var myCar = new SportsCar (); myCar = null;
Глава 3. Главные конструкции программирования на С#: часть I 143 Более того, значение неявно типизированной локальной переменной может быть присвоено другим переменным, причем как неявно, так и явно типизированным: // Здесь тоже все в порядке! var mylnt = 0; var anotherInt = mylnt; string myString = "Wake up!"; var myData = myString; Кроме того, неявно типизированную локальную переменную можно возвращать вызывающему методу, при условии, что возвращаемый тип этого метода совпадает с тем, что лежит в основе определенных с помощью var данных: static int GetAnlntO { var retVal = 9; return retVal; } И, наконец, последний, но от того не менее «важный момент: определять неявно типизированную локальную переменную как допускающую значение null с использованием лексемы ? в С# нельзя (типы данных, допускающие значение null, рассматриваются в главе 4). // Определять неявно типизированные переменные как допускающие значение null // нельзя, поскольку таким переменным изначально не разрешено присваивать null! var? nope = new SportsCar(); var? stillNo = 12; var? noWay = null; Неявно типизированные данные являются строго типизированными Следует иметь в виду, что неявная типизация локальных переменных приводит к получению строго типизированных данных. Таким образом, применение ключевого слова var в С# отличается от техники, используемой в языках сценариев (таких как JavaScript или Perl), а также от применения типа данных Variant в СОМ, где переменная на протяжении своего существования в программе может хранить значения разных типов (это часто называется динамической типизацией). На заметку! В .NET 4.0 появилась возможность динамической типизации в С# с использованием нового ключевого слова dynamic. Более подробно об этом аспекте языка будет рассказываться в главе 18. Выведение типа позволяет языку С# оставаться строго типизированным и оказывает влияние только на объявление переменных во время компиляции. После этого данные трактуются как объявленные с выведенным типом; присваивание такой переменной значения другого типа будет приводить к возникновению ошибок на этапе компиляции. static void ImplicitTypinglsStrongTyping() { // Компилятор знает, что s имеет тип System.String. var s = "This variable can only hold string data!"; s = "This is fine.. . "; // Можно вызывать любой член лежащего в основе типа. string upper = s.ToUpper(); // Ошибка! Присваивание числовых данных строке невозможно! s = 44; }
144 Часть II. Главные конструкции программирования на С# Польза от неявно типизированных локальных переменных Теперь, когда был показан синтаксис, используемый для объявления неявно типизируемых локальных переменных, наверняка возник вопрос, в каких ситуациях его полезно применять? Самое важное, что необходимо знать — использование var для объявления локальных переменных просто так особой пользы не приносит, а в действительности может даже вызвать путаницу у тех, кто будет изучать данный код, поскольку лишает возможности быстро определить тип данных и, следовательно, понять, для чего предназначена переменная. Поэтому если точно известно, что переменная должна относиться к типу int, лучше сразу объявить ее с указанием этого типа. В наборе технологий LINQ, как будет показано в начале главы 13, применяются так называемые выражения запросов (query expression), которые позволяют получать динамически создаваемые результирующие наборы на основе формата запроса. В таких выражениях неявная типизация чрезвычайно полезна, так как в некоторых случаях явное указание типа попросту не возможно. Ниже приведен соответствующий пример кода LINQ, в котором предлагается определить базовый тип данных subset: static void QueryOverlnts () { int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 }; // Запрос LINQ var subset = from i in numbers where i < 10 select i; Console.Write("Values in subset: " ) ; //Значения в subset foreach (var i in subset) { Console.Write("{0} ", l) ; } Console.WriteLine (); // К какому типу относится subset? Console.WriteLine("subset is a: {0}", subset.GetType().Name); Console.WriteLine("subset is defined in: {0}", subset.GetType().Namespace); } Если интересно, проверьте свои предположения по поводу типа данных subset, выполнив предыдущий код (subset целочисленным массивом не является). В любом случае должно быть понятно, что неявная типизация занимает свое место в рамках набора технологий LINQ. На самом деле, можно даже утверждать, что единственным случаем, когда применение ключевого слова var вполне оправдано, является определение данных, возвращаемых из запроса LINQ. Нужно всегда помнить о том, что если в точности известно, что переменная должна представлять собой переменную типа int, лучше всегда сразу же объявлять ее с указанием этого типа. Излишняя неявная типизация (с помощью var) считается плохим стилем в производственном коде. Исходный код. Проект ImplicitlyTypedLocalVars доступен в подкаталоге Chapter 3. Итерационные конструкции в С# Все языки программирования предлагают способы для повторения блоков кода до тех пор, пока не будет соблюдено какое-то условие завершения. Какой бы язык не использовался ранее, операторы, предлагаемые для осуществления итераций в С#, не должны вызывать особых недоумений и требовать особых объяснений. В частности, четырьмя поддерживаемыми в С# для выполнения итераций конструкциями являются:
Глава 3. Главные конструкции программирования на С#: часть I 145 • цикл for; • цикл foreach/in; • цикл while; • цикл do/while. Давайте рассмотрим каждую из этих конструкций по очереди, предварительно создав новый проект типа Console Application по имени IterationsAndDecisions. Цикл for Если требуется проходить по блоку кода фиксированное количество раз, приличную гибкость демонстрирует оператор for. Он позволяет указать, сколько раз должен повторяться блок кода, а также задать конечное условие, при котором его выполнение должно быть завершено. Ниже приведен пример применения этого оператора: // База для цикла. static void ForAndForEachLoop () { // Переменная i доступна только в контексте этого цикла for. for(int i = 0; i < 4; i + + ) { Console.WriteLine("Number is: {0} " , 1) ; } // Здесь переменная i уже не доступна. } Все приемы, освоенные в языках С, C++ и Java, применимы также при создании операторов f or в С#. Как и в других языках, в С# можно создавать сложные конечные условия, определять бесконечные циклы и использовать ключевые слова goto, continue и break. Предполагается, что читатель уже знаком с данной итерационной конструкцией. Дополнительные сведения об использовании ключевого слова for в С# можно найти в документации .NET Framework 4.0 SDK. Цикл f oreach Ключевое слово f oreach в С# позволяет проходить в цикле по всем элементам массива (или коллекции, как будет показано в главе 10} без проверки его верхнего предела. Ниже приведено два примера использования цикла f oreach, в одном из которых производится проход по массиву строк, а в другом — проход по массиву целых чисел. // Проход по элементам массива с помощью foreach. static void ForAndForEachLoop () { string[] carTypes = {"Ford", "BMW", "Yugo", "Honda" }; foreach (string с in carTypes) Console.WriteLine(c); int[] mylnts = { 10, 20, 30, 40 }; foreach (int 1 in mylnts) Console.WriteLine(l); } Помимо прохода по простым массивам, foreach также позволяет осуществлять итерации по системным и определяемым пользователем коллекциям. Рассмотрение соответствующих деталей откладывается до главы 9, поскольку этот способ применения foreach требует понимания особенностей программирования с использованием интерфейсов, таких как IEnumerator и IEnumerable.
146 Часть II. Главные конструкции программирования на С# Использование var в конструкциях f oreach В итерационных конструкциях f oreach также можно применять неявную типизацию. Нетрудно догадаться, что компилятор в таких случаях будет корректно выводить соответствующий "тип типа". Рассмотрим приведенный ниже метод, в котором производится проход по локальному массиву целых чисел: static void VarlnForeachLoop () { intU mylr.Ls = { 10, 20, 30, 40 }; // Использование var в стандартном цикле foreach. foreach (var lt^m in mylnts) { Console .WnteLine ( "Item value: { 0 } " , item); // вывод значения элемента } } Следует отметить, что в данном примере веской причины для использования ключевого слова var в цикле foreach нет, поскольку четко видно, что итерация осуществляется по массиву целых чисел. Но, опять-таки, в модели программирования LINQ использование var в цикле foreach будет очень полезно, а иногда и вообще обязательно. Конструкции while и do/while Конструкцию while удобно применять, когда требуется, чтобы блок операторов выполнялся до тех пор, пока не будет удовлетворено какое-то конечное условие. Естественно, нужно позаботиться о том, чтобы это условие когда-нибудь действительно достигалось, иначе получится бесконечный цикл. Ниже приведен пример, в котором на экран будет выводиться сообщение In while loop (В цикле while) до тех пор, пока пользователь не завершит цикл вводом в командной строке слова yes: static void ExecuteWhileLoop () { string userlsDone = ""; // Проверка на соответствие строке в нижнем регистре, while(userlsDone.ToLower () |= "yes") { , Console.Write("Are you done? [yes] [no]: ") ; // запрос окончания userlsDone = Console.ReadLine (); Console.WriteLine ("In while loop"); } } Оператор do/while тесно связан с циклом while. Как и обычный while, цикл do/while применяется тогда, когда какое-то действие должно выполняться неопределенное количество раз. Разница между этими двумя циклами состоит в том, что цикл do /while гарантирует выполнение соответствующего блока кода хотя бы один раз (в то время как цикл while может его вообще не выполнить, если условие с самого начала оказывается ложным). static void ExecuteDoWhileLoop () { string userlsDone = " " ; do { Console.WriteLine ("In do/while loop"); Console.Write("Are you done? [yes] [no]: "); userlsDone = Console.ReadLine(); } while(userlsDone.ToLower() != "yes"); // Обратите внимание на точку с запятой!
Глава 3. Главные конструкции программирования на С#: часть I 147 Конструкции принятия решений и операции сравнения Теперь, когда было показано, как обеспечить выполнение блока операторов в цикле, давайте рассмотрим следующую связанную концепцию, а именно — управление выполнением программы. Для изменения хода выполнения программы в С# предлагаются две следующих конструкции: • оператор if /else; • оператор switch. Оператор if /else Сначала рассмотрим хорошо знакомый оператор if/else. В отличие от языков С и C++, в С# этот оператор может работать только с булевскими выражениями, но не с произвольными значениями вроде -1 и 0. С учетом этого, для получения литеральных булевских значений в операторах if /else обычно применяются операции, перечисленные в табл. 3.7. Таблица 3.7. Операции сравнения в С# р Пример использования Описание сравнения if (age == 30) Возвращает true, только если выражения одинаковы != if("Foo" != myStr) Возвращает true, только если выражения разные < if (bonus < 2000) Возвращает true, только если выражение слева > if (bonus > 2000) (bonus) меньше, больше, меньше или равно либо <= if (bonus <= 2000) больше или равно выражению справа B000) >= if (bonus >= 2000) Программисты, ранее работавшие с С и C++, должны иметь в виду, что старые приемы проверки неравенства значения нулю в С# работать не будут. Например, предположим, что требуется проверить, состоит ли текущая строка из более чем нуля символов. У программистов на С и C++ может возникнуть соблазн написать код следующего вида: static yoid ExecutelfElse () { // Такой код недопустим, поскольку Length возвращает int, а не bool. string stringData = "My textual data"; if(stringData.Length) { Console.WriteLine("string is greater than 0 characters"); } } Если необходимо использовать свойство String. Length для определения истинности или ложности, вычисляемое в условии выражение потребуется изменить так, чтобы оно давало в результате булевское значение: // Такой код является допустимым, поскольку // условие будет возвращать true или false. if(stringData.Length > 0) { Console.WriteLine("string is greater than 0 characters"); }
148 Часть II. Главные конструкции программирования на С# В операторе if могут применяться сложные выражения, и он может содержать операторы else, обеспечивая выполнение более сложных проверок. Синтаксис похож на применяемый в аналогичных ситуациях в языках С (C++) и Java. При построении сложных выражений в С# используется вполне ожидаемый набор условных операций, описанный в табл. 3.8. Таблица 3.8. Условные операции в С# Операция Пример Описание && if (age == 30 && name == "Fred") Условная операция AND (И). Возвращает true, если все выражения истинны I | if (age ==30 | | name == "Fred") Условная операция OR (ИЛИ). Возвращает true, если истинно хотя бы одно из выражений ! if (ImyBool) Условная операция NOT (HE). Возвращает true, если выражение ложно, или false, если истинно На заметку! Операции && и | | поддерживают сокращенный путь выполнения, если это необходимо. То есть сразу же после определения, что некоторое сложное выражение является ложным, оставшиеся подвыражения вычисляться не будут. Оператор switch Еще одной простой конструкцией, предназначенной в С# для реализации выбора, является оператор switch. Как и в остальных С-подобных языках, в С# этот оператор позволяет организовать выполнение программы на основе заранее определенного набора вариантов. Например, в приведенном ниже методе Main () для каждого из двух возможных вариантов выводится свое сообщение (блок default обрабатывает неверный выбор). // Переход на основе выбранного числового значения. static void ExecuteSwitch() { Console.WriteLine ( [C#], 2 [VB]"); Console.Write("Please pick your language preference: "); string langChoice = Console.ReadLine (); int n = int.Parse(langChoice); switch (n) { case 1: Console.WriteLine("Good choice, C# is a fine language."); break; case 2: Console.WriteLine("VB: OOP, multithreading, and more1"); break; default: Console.WriteLine("Well...good luck with that!"); break; На заметку! В языке С# каждый блок case, в котором содержатся выполняемые операторы (блок default в том числе), должен завершаться оператором break или goto, во избежание сквозного прохода.
Глава 3. Главные конструкции программирования на С#: часть I 149 Одна из замечательных особенностей оператора switch в С# заключается в том, что помимо числовых данных он также позволяет производить вычисления и со строковыми данными. Ниже для примера приведена модифицированная версия предыдущего оператора switch (обратите внимание, что при этом подвергать пользовательские данные синтаксическому разбору и преобразованию их в числовые значения не требуется). static void ExecuteSwitchOnString () { Console.WriteLine("C# or VB" ); Console.Write("Please pick your language preference: "); string langChoice = Console.ReadLine (); switch (langChoice) { case "C#": Console.WriteLine("Good choice, C# is a fine language."); break; case "VB": Console.WriteLine("VB: OOP, multithreading and more1"); break; default: Console.WriteLine("Well... good luck with that!");\ break; } } Исходный код. Проект IterationsAndDecisions доступен в подкаталоге Chapter 3. На этом рассмотрение поддерживаемых в С# ключевых слов для организации циклов и принятия решений, а также общих операций, которые можно использовать при написании сложных операторов, завершено. Здесь предполагалось, что у читателя имеется опыт работы с аналогичными ключевыми словами (if, for, switch и т.д.) в других языках программирования. Дополнительные сведения по данной теме можно найти в документации .NET Framework 4.0 SDK. Резюме Задачей настоящей главы было описание многочисленных ключевых аспектов языка программирования С#. Сначала рассматривались типичные конструкции, используемые в любом приложении. Затем была описана роль объекта приложения и рассказано о том, что в каждой исполняемой программе на С# должен обязательно присутствовать тип, определяющий метод Main () , который служит входной точкой в приложении. Внутри метода Main () обычно создается набор объектов, которые, работая вместе, приводят приложение в действие. Далее были рассмотрены базовые типы данных в С# и разъяснено, что используемые для их представления ключевые слова (вроде int) на самом деле являются сокращенными обозначениями полноценных типов из пространства имен System (в данном случае System. Int32). Благодаря этому, каждый тип данных в С# имеет набор встроенных членов. Также была описана роль операций расширения и сужения типов и таких ключевых слов, как checked и unchecked. Кроме того, рассматривались особенности неявной типизации с помощью ключевого слова var. Как было отмечено, неявная типизация наиболее полезна в модели программирования LINQ. И, наконец, в главе кратко рассматривались различные конструкции С#, предназначенные для создания циклов и принятия решений. Теперь, когда базовые характеристики языка С# известны, можно переходить к изучению его ключевой функциональности, а также объектно-ориентированных возможностей.
ГЛАВА 4 Вгавные конструкции программирования на С#: часть II В этой главе будет завершен обзор ключевых аспектов языка программирования С#. Сначала рассматриваются различные детали, касающиеся построения методов в С#, в частности, ключевые слова out, ref и params. Кроме того, будут описаны две новых функциональных возможности С#, которые появились в .NET 4.0 — необязательные и именованные параметры. Затем будет рассматриваться перегрузка методов, манипулирование массивами с использованием синтаксиса С# и функциональность связанного с массивами класса System.Array. Вдобавок в главе показано, как создавать перечисления и структуры в С#, и подробно описаны отличия между типами значений и ссылочными типами. И, наконец, рассматривается роль нулевых (nullable) типов данных и операций ? и ? ?. После изучения материалов настоящей главы можно переходить к ознакомлению с объектно-ориентированными возможностями языка С#. Методы и модификаторы параметров Для начала давайте изучим детали, касающиеся определения методов в С#. Как и метод Main () (см. главу 3), специальные методы могут как принимать, так и не принимать параметров, а также возвращать или не возвращать значения вызывающей стороне. Как будет показано в следующих нескольких главах, методы могут быть реализованы в контексте классов или структур (а также прототипированы внутри типов интерфейсов) и снабжаться различными ключевыми словами (internal, virtual, public, new и т.д.) для уточнения их поведения. До настоящего момента в этой книге формат каждого из демонстрировавшихся методов в целом выглядел так: // Статические методы могут вызываться // напрямую без создания экземпляра класса. class Program • { // static возвращаемыйТип ИмяМетода (параметры) {...} static int Add(mt x, int у) { return x + у; } }
Глава 4. Главные конструкции программирования на С#: часть II 151 Хотя определение метода в С# выглядит довольно понятно, существует несколько ключевых слов, с помощью которых можно управлять способом передачи аргументов интересующему методу. Все эти ключевые слова описаны в табл. 4.1. Таблица 4.1. Модификаторы параметров в С# Модификатор параметра Описание i ■ (отсутствует) Если параметр не сопровождается модификатором, предполагается, что он должен передаваться по значению, т.е. вызываемый метод должен получать копию исходных данных out Выходные параметры должны присваиваться вызываемым методом (и, следовательно, передаваться по ссылке). Если параметрам out в вызываемом методе значения не присвоены, компилятор сообщит об ошибке ref Это значение первоначально присваивается вызывающим кодом и при желании может повторно присваиваться в вызываемом методе (поскольку данные также передаются по ссылке). Если параметрам ref в вызываемом методе значения не присвоены, компилятор никакой ошибки генерировать не будет params Этот модификатор позволяет передавать в виде одного логического параметра переменное количество аргументов. В каждом методе может присутствовать только один модификатор params и он должен обязательно указываться последним в списке параметров. В реальности необходимость в использовании модификатора params возникает не особо часто, однако он применяется во многих методах внутри библиотек базовых классов Чтобы посмотреть, как эти ключевые слова используются на практике, давайте создадим новый проект типа Console Application по имени FunWithMethods и с его помощью изучим роль каждого из этих ключевых слов. Стандартное поведение при передаче параметров По умолчанию параметр передается по значению. Попросту говоря, если аргумент не снабжается каким-то конкретным модификатором параметра, функции передается копия данных. Как будет объясняться в конце настоящей главы, то, как именно выглядит эта копия, зависит от того, к какому типу относится параметр — типу значения или ссылочному типу. Пока что давайте создадим внутри класса Program следующий метод, который оперирует двумя числовыми типами данных, передаваемыми по значению: //По умолчанию аргументы передаются по значению. public static int Add(int x, int y) { int ans = x + y; // Вызывающий метод не увидит эти изменения, поскольку // изменяться в таком случае будет лишь копия исходных данных. х = 10000; у = 88888; return ans; } Числовые данные подпадают под категорию типов значения. Поэтому в случае изменения значений параметров внутри члена вызывающий метод будет оставаться в полном неведении об этом, поскольку значения будут изменяться лишь в копии исходных данных.
152 Часть II. Главные конструкции программирования на С# static void Main(string [ ] args) { Console.WriteLine("*****Fun with Methods *****\n"); // Передача двух переменных по значению. int x = 9, у = 10; Console.WriteLine("Before call: X: {0}, Y: {1}", x, y) ; // до вызова Console.WriteLine("Answer is: {0}", Add(x, y) ) ; // ответ Console.WriteLine("After call: X: {0}, Y: {1}", x, y) ; // после вызова Console.ReadLine(); } Как и следовало ожидать, значения х и у до и после вызова Add () будут выглядеть совершенно идентично: ***** Fun W1th Methods ***** Before call: X: 9, Y: 10 Answer is: 19 After call: X: 9, Y: 10 Модификатор out Теперь посмотрим, как используются выходные параметры. Методы, которым при определении (с помощью ключевого слова out) указано принимать выходные параметры, должны перед выходом обязательно присваивать им соответствующие значения (в противном случае компилятор сообщит об ошибке). В целях иллюстрации ниже приведена альтернативная версия метода Add (), которая предусматривает возврат суммы двух целых чисел с использованием модификатора out (обратите внимание, что физическим возвращаемым значением метода теперь является void). // Выходные параметры должны предоставляться вызываемым методом. public static void Add (int x, int y, out int ans) { ans = x + y; } В вызове метода с выходными параметрами тоже должен использоваться модификатор out. Локальным переменным, передаваемым в качестве выходных параметров, присваивать начальные значения не требуется (после вызова эти значения все равно будут утрачены). Причина, по которой компилятор позволяет передавать на первый взгляд неинициализированные данные, связана с тем, что в вызываемом методе операция присваивания должна выполняться обязательно. Ниже приведен пример. static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Methods *****"); // Присваивать первоначальное значение локальным // переменным, используемым в качестве выходных // параметров, не требуется, при условии, что в первый раз // они используются в качестве выходных аргументов. int ans; Add(90, 90, out ans); Console.WriteLine("90 + 90 = {0}", ans); Console.ReadLine(); } Этот пример был представлен исключительно для иллюстрации; на самом деле нет совершенно никаких оснований возвращать значение операции суммирования в выход-
Глава 4. Главные конструкции программирования на С#: часть II 153 ном параметре. Но сам модификатор out в С# действительно является очень полезным: он позволяет вызывающему коду получать в результате одного вызова метода сразу несколько значений. // Возврат множества выходных параметров. public static void FillTheseVals (out int a, out string b, out bool c) { a = 9; b = "Enjoy your string."; с = true; } В вызывающем коде в таком случае может находиться обращение к методу FillTheseValues (), как показано ниже. Обратите внимание, что модификатор out должен использоваться как при вызове, так и при реализации данного метода. static void Main(string[ ] args) { Console.WriteLine ("***** Fun with Methods *****"); int i; string str; bool b; FillTheseValues (out i, out str, out b) ; Console.WriteLine ("Int is: {0}", l); // целое число Console.WriteLine ("String is: {0}", str); // строка Console.WriteLine ("Boolean is: {0}", b) ; // булевское значение Console.ReadLine(); } И, наконец, не забывайте, что в любом методе, в котором определяются выходные параметры, перед выходом им обязательно должны быть присвоены действительные значения. Следовательно, для следующего кода компилятор будет выдавать ошибку, поскольку выходному параметру в области действия метода никакого значения присвоено не было: static void ThisWontCompile (out int a) { Console. WriteLine("Error' Forgot to assign output arg!"); // Ошибка! Забыли присвоить значение выходному аргументу! } Модификатор ref Теперь посмотрим, как в С# используется модификатор ref (от "reference" - ссылка). Параметры, сопровождаемые таким модификатором, называются ссылочными и применяются, когда нужно позволить методу выполнять операции и обычно также изменять значения различных элементов данных, объявляемых в вызывающем коде (например, в процедуре сортировки или обмена). Обратите внимание на следующие отличия между ссылочными и выходными параметрами. • Выходные параметры не нужно инициализировать перед передачей методу. Причина в том, что метод сам должен присваивать значения выходным параметрам перед выходом. • Ссылочные параметры нужно обязательно инициализировать перед передачей методу. Причина в том, что они подразумевают передачу ссылки на уже существующую переменную. Если первоначальное значение ей не присвоено, это будет равнозначно выполнению операции над неинициализированной локальной переменной.
154 Часть II. Главные конструкции программирования на С# Давайте рассмотрим применение ключевого слова ref на примере метода, меняющего две строки местами: // Ссылочные параметры. public static void SwapStrings(ref string si, ref string s2) { string tempStr = si; si = s2; s2 = tempStr; } Этот метод может быть вызван следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Methods *****"); string si = "Flip"; string s2 = "Flop"; Console.WriteLine("Before: {0}, {1} ", si, s2) ; //до SwapStrings (ref si, ref s2); Console.WriteLine("After: {0}, [1} ", si, s2); // после Console.ReadLine (); } Здесь в вызывающем коде производится присваивание первоначальных значений локальным строкам (si и s2). Благодаря этому после выполнения вызова SwapStrings () в строке si будет содержаться значение "Flop", а в строке s2 — значение "Flip": Before: Flip, Flop After: Flop, Flip На заметку! Поддерживаемое в С# ключевое слово ref рассматривается в разделе "Типы значения и ссылочные типы" далее в главе. Как будет показано, поведение этого ключевого слова немного меняется в зависимости от того, является аргумент типом значения (структурой) или ссылочный типом (классом). Модификатор params В С# поддерживается использование массивов параметров за счет применения ключевого слова params. Для овладения этой функциональностью требуются хорошие знания массивов С#, основные сведения о которых приведены в разделе "Массивы в С#" далее в главе. Ключевое слово params позволяет передавать методу переменное количество аргументов одного типа в виде единственного логического параметра. Аргументы, помеченные ключевым словом params, могут обрабатываться, если вызывающий код на их месте передает строго типизированный массив или разделенный запятыми список элементов. Конечно, это может вызывать путаницу. Чтобы стало понятнее, предположим, что требуется создать функцию, которая бы позволила вызывающему коду передавать любое количество аргументов и возвращала бы их среднее значение. Если прототипировать соответствующий метод так, чтобы он принимал массив значений типа double, вызывающий код должен будет сначала определить массив, затем заполнить его значениями и только потом, наконец, передать. Однако если определить метод CalculateAverage () так, чтобы он принимал массив параметров (params) типа double, тогда вызывающий код может просто передать разделенный запятыми список значений double, а исполняющая среда .NET автоматически упакует этот список в массив типа double.
Глава 4. Главные конструкции программирования на С#: часть II 155 // Возвращение среднего из некоторого количества значений double. static double CalculateAverage(params double [ ] values) { // Вывод количества значений Console. WriteLine ("You sent me {0} doubles.11, values . Length) ; double sum = 0; if(values.Length == 0) return sum; for (int i = 0; l < values.Length; i++) sum += values [i]; return (sum / values.Length); } Метод определен таким образом, чтобы принимать массив параметров со значениями типа double. По сути, этот метод ожидает произвольное количество (включая ноль) значений double и вычисляет по ним среднее значение. Благодаря этому, он может вызываться любым из показанных ниже способов: static void Main(string[] args) { Console.WriteLine ("***** Fun with Methods *****••); // Передача разделенного запятыми списка значений double... double average; average = CalculateAverageD.0, 3.2, 5.7, 64.22, 87.2); Console.WriteLine("Average of data is: { 0}" , average); // Среднее значение получается таким: // . . .или массива значений double. doublet] data = { 4.0, 3.2, 5.7 }; average = CalculateAverage(data); Console.WriteLine("Average of data is: {0}", average); // Среднее из 0 равно О! Console.WriteLine("Average of data is: {0}", CalculateAverage()); Console.ReadLine(); } Если бы в определении CalculateAverage () не было модификатора params, первый способ вызова этого метода приводил бы к ошибке на этапе компиляции, поскольку тогда компилятор искал бы версию CalculateAverage (), принимающую пять аргументов double. На заметку! Во избежание какой бы то ни было неоднозначности, в С# требуется, чтобы в любом методе поддерживался только один аргумент params, который должен быть последним в списке параметров. Как не трудно догадаться, такой подход является просто более удобным для вызывающего кода, поскольку в случае его применения необходимый массив создается самой CLR-средой. К моменту, когда этот массив попадает в область действия вызываемого метода, он будет трактоваться как полноценный массив .NET, обладающий всеми функциональными возможностями базового типа System.Array. Ниже показано, как мог бы выглядеть вывод приведенного выше метода: You sent me 5 doubles. Average of data is: 32.864 You sent me 3 doubles. Average of data is: 4.3 You sent me 0 doubles . Average of data is: 0
156 Часть II. Главные конструкции программирования на С# Определение необязательных параметров С выходом версии .NET 4.0 у разработчиков приложений на С# теперь появилась возможность создавать методы, способные принимать так называемые необязательные аргументы (optional arguments). Это позволяет вызывать единственный метод, опуская необязательные аргументы, при условии, что подходят их значения, установленные по умолчанию. На заметку! Как будет показано в главе 18, главным стимулом для добавления необязательных аргументов послужила необходимость в упрощении взаимодействия с объектами СОМ. В нескольких объектных моделях Microsoft (например, Microsoft Office) функциональность предоставляется через объекты СОМ, многие из которых были написаны давно и рассчитаны на использование необязательных параметров. Чтобы посмотреть, как работать с необязательными аргументами, давайте создадим метод по имени EnterLogDataO с одним необязательным параметром: static void EnterLogData(string message, string owner = "Programmer") { Console.Beep(); Console.WriteLine ("Error: {0}", message); Console.WriteLine("Owner of Error: {0}", owner); } Последнему аргументу string был присвоено используемое по умолчанию значение "Programmer" с применением операции присваивания внутри определения параметров. В результате метод EnterLogData () можно вызывать в Main () двумя способами: static void Main(string[] args) { Console.WriteLine ("***** Fun with Methods *****"); EnterLogData("Oh no! Grid can't find data"); EnterLogData("Oh no! I can't find the payroll data", "CFO"); Console.ReadLine(); } Поскольку в первом вызове EnterLogData () не было указано значение для второго аргумента string, будет использоваться его значение по умолчанию — "Programmer". Еще один важный момент, о котором следует помнить, состоит в том, что значение, присваиваемое необязательному параметру, должно быть известно во время компиляции и не может вычисляться во время выполнения (в этом случае на этапе компиляции сообщается об ошибке). Для иллюстрации предположим, что понадобилось модифицировать метод EnterLogData (), добавив в него еще один необязательный параметр: // Ошибка1 Значение, используемое по умолчанию для необязательного // аргумента, должно быть известно во время компиляции! static void EnterLogData(string message, string owner = "Programmer", DateTime timeStamp = DateTime.Now) { Console.Beep (); Console.WriteLine ("Error: {0}", message); Console.WriteLine("Owner of Error: {0}", owner); Console.WriteLine("Time of Error: {0 } ", timeStamp); } Этот код скомпилировать не получится, потому что значение свойства Now класса DateTime вычисляется во время выполнения, а не во время компиляции.
Глава 4. Главные конструкции программирования на С#: часть II 157 На заметку! Во избежание возникновения любой неоднозначности, необязательные параметры должны всегда размещаться в конце сигнатуры метода. Если необязательный параметр окажется перед обязательными, компилятор сообщит об ошибке. Вызов методов с использованием именованных параметров Еще одной функциональной возможностью, которая добавилась в С# с выходом версии .NET 4.0, является поддержка так называемых именованных аргументов (named arguments). По правде говоря, на первый взгляд может показаться, что такая языковая конструкция способна лишь запутать код. Это действительно может оказаться именно так! Во многом подобно необязательным аргументам, стимулом для включения поддержки именованных параметров главным образом послужило желание упростить работу с уровнем взаимодействия с СОМ (см. главу 18). Именованные аргументы позволяют вызывать метод с указанием значений параметров в любом желаемом порядке. Следовательно, вместо того, чтобы передавать параметры исключительно в соответствии с позициями, в которых они определены (как приходится поступать в большинстве случаев), можно указывать имя каждого аргумента, двоеточие и конкретное значение. Чтобы продемонстрировать применение именованных аргументов, добавим в класс Program следующий метод: static void DisplayFancyMessage(ConsoleColor textColor, ConsoleColor backgroundColor, string message) { // Сохранение старых цветов для обеспечения возможности //их восстановления сразу после вывода сообщения. ConsoleColor oldTextColor = Console.ForegroundColor; ConsoleColor oldbackgroundColor = Console.BackgroundColor; // Установка новых цветов и вывод сообщения. Console.ForegroundColor = textColor; Console.BackgroundColor = backgroundColor; Console.WriteLine(message); // Восстановление предыдущих цветов. Console.ForegroundColor = oldTextColor; Console.BackgroundColor = oldbackgroundColor; } Возможно, ожидается, что при вызове методу DisplayFancyMessage () должны передаваться две переменных типа ConsoleColor со следующим за ним значением типа string. Однако за счет применения именованных аргументов DisplayFancyMessage () вполне можно вызвать и так, как показано ниже: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Methods *****"); DisplayFancyMessage (message: "Wow! Very Fancy indeed!11, textColor: ConsoleColor.DarkRed, backgroundColor: ConsoleColor.White); DisplayFancyMessage(backgroundColor: ConsoleColor.Green, message: "Testing...11, textColor: ConsoleColor.DarkBlue); Console.ReadLine(); }
158 Часть II. Главные конструкции программирования на С# Одной малоприятной особенностью применения именованных аргументов является то, что в вызове метода позиционные параметры должны быть перечислены перед любыми именованными параметрами. Другими словами, именованные аргументы должны всегда размещаться в конце вызова метода. Ниже показан пример, иллюстрирующий сказанное. // Здесь все в порядке, поскольку позиционные // аргументы идут перед именованными. DisplayFancyMessage(ConsoleColor.Blue, message: "Testing...11, backgroundColor: ConsoleColor.White); // Здесь присутствует ошибка, поскольку позиционные // аргументы идут после именованных. DisplayFancyMessage(message: "Testing...", backgroundColor: ConsoleColor.White, ConsoleColor.Blue); Помимо этого ограничения может возникать вопрос, а когда вообще понадобится эта языковая конструкция? Зачем нужно менять позиции трех аргументов метода? Как оказывается, если в методе необходимо определять необязательные аргументы, то эта конструкция может оказаться очень полезной. Для примера перепишем метод DisplayFancyMessage () так, чтобы он поддерживал необязательные аргументы и предусматривал для них подходящие значения по умолчанию: static void DisplayFancyMessage(ConsoleColor textColor = ConsoleColor.Blue, ConsoleColor backgroundColor = ConsoleColor.White, string message = "Test Message") { } Из-за того, что каждый аргумент теперь имеет значение по умолчанию, в вызывающем коде с помощью именованных аргументов можно указывать только тот параметр или параметры, для которых не должны применяться значения по умолчанию. То есть, если нужно, чтобы значение "Hello! " появлялось в виде текста голубого цвета на белом фоне, в вызывающем коде можно использовать следующую строку: DisplayFancyMessage(message: "Hello!") Если же необходимо, чтобы строка "Test Message" выводилась синим цветом на зеленом фоне, то должен применяться такой код: DisplayFancyMessage(backgroundColor: ConsoleColor.Green); Как не трудно заметить, необязательные аргументы и именованные параметры действительно часто работают бок о бок. Чтобы завершить рассмотрение деталей построения методов в С#, необходимо обязательно ознакомиться с понятием перегрузки методов. Исходный код. Приложение FunWithEnums доступно в подкаталоге Chapter 4. Перегрузка методов Как и в других современных объектно-ориентированных языках программирования, в С# можно перегружать (overload) методы. Перегруженными называются методы с набором одинаково именованных параметров, отличающиеся друг от друга количеством (или типом) параметров.
Глава 4. Главные конструкции программирования на С#: часть II 159 Чтобы оценить полезность перегрузки методов, давайте представим себя на месте разработчика, использующего Visual Basic 6.0, и предположим, что требуется создать набор методов, возвращающих сумму значений различных типов (Integer, Double и т.д.). Из-за того, что в VB6 перегрузка методов не поддерживается, для решения этой задачи придется определить уникальный набор методов, каждый из которых, по сути, будет делать одно и тоже (возвращать сумму аргументов): 1 Примеры кода на VB6. Public Function Addlnts(ByVal x As Integer, ByVal у As Integer) As Integer Addlnts = x + у End Function Public Function AddDoubles(ByVal x As Double, ByVal у As Double) As Double AddDoubles = x + у End Function Public Function AddLongs (ByVal x As Long, ByVal у As Long) As Long AddLongs = x + у End Function Такой код не только труден в сопровождении, но и заставляет помнить имя каждого метода. С применением перегрузки, однако, можно дать возможность вызывающему коду вызывать только единственный метод по имени Add () . При этом важно обеспечить, чтобы каждая версия метода имела отличающийся набор аргументов (различий в одном только возвращаемом типе не достаточно). На заметку! Как будет показано в главе 10, в С# возможно создавать обобщенные методы, которые переводят концепцию перегрузки на новый уровень. С использованием обобщений для реализаций методов можно определять так называемые "заполнители", которые будут заполняться во время вызова этих методов. Чтобы попрактиковаться с перегруженными методами, создадим новый проект Console Application (Консольное приложение) по имени Methodoverloading и добавим в него следующее определение класса на С#: // Код на С#. class Program { static void Main(string [ ] args) { } // Перегруженный метод Add() . static int Add(int x, int y) { return x + y; } static double Add(double x, double y) { return x + y; } static long Add(long x, long y) { return x + y; } } Теперь можно вызывать просто метод Add () с требуемыми аргументами, а компилятор будет самостоятельно находить правильную реализацию, подлежащую вызову, на основе предоставляемых ему аргументов: static void Main(string[] args) { Console.WriteLine (••***** Fun with Method Overloading *~*>A\n"); // Вызов int-версии Add() Console.WriteLine(AddA0, 10)); // Вызов long-версии Add() ' Console.WnteLine(Add(900000000000, 900000000000));
160 Часть II. Главные конструкции программирования на С# // Вызов double-версии Add() Console.WriteLine(AddD.3, 4.4)); Console . ReadLme () ; } IDE-среда Visual Studio 2010 обеспечивает помощь при вызове перегруженных методов. При вводе имени перегруженного метода (как, например, хорошо знакомого Console .WriteLine ()) в списке IntelliSense предлагаются все его доступные версии. Обратите внимание, что по списку можно легко перемещаться, щелкая на кнопках со стрелками вниз и вверх (рис. 4.1). Program.cs* X Щ I jtfMethodOvertckading.Program 'I ^Main(stringQ args) // Calls int version of Add() Console.WriteLine(Add(ie, 16)); // Calls long version of Add() Ijl .1 // Calls double version of Add() Console.WriteLine(AddD.3, 4.4)); Console.WriteLine( [a 2 of 19 T void Console,WriteLfnefbool value) | } I ^ The value to write. | + l0v«? г loaded AddQ methods! Рис. 4.1. Окно IntelliSense, отображаемое в Visual Studio 2010 при работе с перегруженными методами На заметку! Приложение MethodOverloading доступно в подкаталоге Chapter 4. На этом обзор основных деталей создания методов с использованием синтаксиса С# завершен. Теперь давайте посмотрим, как в С# создавать и манипулировать массивами, перечислениями и структурами, и завершим главу изучением того, что собой представляют "нулевые типы данных" и такие поддерживаемые в С# операции, как ? и ??. Массивы в С# Как, скорее всего, уже известно, массивом (array) называется набор элементов данных, доступ к которым получается по числовому индексу. Если говорить более конкретно, то любой массив, по сути, представляет собой ряд связанных между собой элементов данных одинакового типа (например, массив int, массив string, массив SportCar и т.п.). Объявление массива в С# осуществляется довольно понятным образом. Чтобы посмотреть, как именно, давайте создадим новый проект типа Console Application (no имени FunWithArrays) и добавим в него вспомогательный метод SimpleArrays (), вызываемый из Main (). class Program { static void Main(string[] args) { Console.WriteLine ("***** Fun with Arrays *****"); SimpleArrays(); Console.ReadLine(); } I
Глава 4. Главные конструкции программирования на С#: часть II 161 static void SimpleArrays () { Console. WriteLine ("=> Simple Array Creation.11); // Создание массива int с тремя элементами {0, 1, 2}. int[] mylnts = new int[3]; // Инициализация массива с 100 элементами string, // проиндексированными от 0 до 99. string[] booksOnDotNet = new string[100]; Console.WriteLine (); } } Внимательно почитайте комментарии в коде. При объявлении массива с помощью такого синтаксиса указываемое в объявлении число обозначает общее количество элементов, а не верхнюю границу. Кроме того, нижняя граница в массиве всегда начинается с 0. Следовательно, в результате объявления int [ ] mylnts = new int [3] получается массив, содержащий три элемента, проиндексированных по позициям 0, 1,2. После определения переменной массива можно переходить к его заполнению элементами от индекса к индексу, как показано ниже на примере метода SimpleArrays () : static void SimpleArrays () { Console .WriteLine ("=> Simple Array Creation."); // Создание и заполнение массива тремя // целочисленными значениями. int[] mylnts = new int[3]; mylnts[0] = 100; mylnts[1] = 200; mylnts[2] = 300; // Отображение значений. foreach (int i in mylnts) Console.WriteLine A); Console.WriteLine (); } На заметку! Следует иметь в виду, что если массив только объявляется, но явно не инициализируется, каждый его элемент будет установлен в значение, принятое по умолчанию для соответствующего типа данных (например, элементы массива типа bool будут устанавливаться в false, а элементы массива типа int — в 0). Синтаксис инициализации массивов в С# Помимо заполнения массива элемент за элементом, можно также заполнять его с использованием специального синтаксиса инициализации массивов. Для этого необходимо перечислить включаемые в массив элемент в фигурных скобках ({ }). Такой синтаксис удобен при создании массива известного размера, когда нужно быстро задать его начальные значения. Ниже показаны альтернативные версии объявления массива. static void Arraylnitialization () { Console .WriteLine ("=> Array Initialization."); // Синтаксис инициализации массива с помощью // ключевого слова new. string[] stringArray = new string[] { "one", "two", "three" }; Console.WriteLine("stringArray has {0} elements", stringArray.Length);
162 Часть II. Главные конструкции программирования на С# // Синтаксис инициализации массива без применения // ключевого слова new. bool [ ] boolArray = { false, false, true }; Console.WriteLine("boolArray has {0} elements", boolArray.Length); // Синтаксис инициализации массива с указанием ключевого // слова new и желаемого размера. int[] intArray = new int[4] { 20, 22, 23, 0 }; Console.WriteLine("intArray has {0} elements", intArray.Length); Console.WriteLine(); } Обратите внимание, что в случае применения синтаксиса с фигурными скобками размер массива указывать не требуется (как видно на примере создания переменной stringArray), поскольку этот размер автоматически вычисляется на основе количества элементов внутри фигурных скобок. Кроме того, применять ключевое слово new не обязательно (как при создании массива boolArray). В объявлении intArray указанное числовое значение обозначает количество элементов в массиве, а не верхнюю границу. Если между объявленным размером и количеством инициализаторов имеется несоответствие, на этапе компиляции будет выдано сообщение об ошибке. Ниже приведен соответствующий пример: // Размер не соответствует количеству элементов! int[] intArray = new int[2] { 20, 22, 23, 0 }; Неявно типизированные локальные массивы В предыдущей главе рассматривалась тема неявно типизированных локальных переменных. Вспомните, что ключевое слово var позволяет определить переменную так, чтобы лежащий в ее основе тип выводился компилятором. Аналогичным образом можно также определять неявно типизированные локальные массивы. С использованием такого подхода можно определить новую переменную массива без указания типа элементов, содержащихся в массиве. static void DeclarelmplicitArrays () { Console .WriteLine ("=> Implicit Array Initialization."); // В действительности а - массив типа int[]. var a = new[] { 1, 10, 100, 1000 }; Console.WriteLine("a is a: {0}", a.ToString()); // В действительности Ь - массив типа double [] . var b = new[] { 1, 1.5, 2, 2.5 }; Console.WriteLine(Mb is a: {0}", b.ToString()); // В действительности с - массив типа string [] . var с = new[] { "hello", null, "world" }; Console.WriteLine("c is a: {0}", с.ToString()); Console.WriteLine(); } Разумеется, как и при создании массива с использованием явного синтаксиса С#, элементы, указываемые в списке инициализации массива, должны обязательно иметь один и тот же базовый тип (т.е. должны все быть intr string или SportsCar). В отличие от возможных ожиданий, неявно типизированный локальный массив не получает по умолчанию тип SystemObject, поэтому приведенный ниже код вызывает ошибку на этапе компиляции: // Ошибка! Используются смешанные типы! var d = new[J { 1, "one", 2, "two", false };
Глава 4. Главные конструкции программирования на С#: часть II 163 Определение массива объектов В большинстве случаев при определении массива тип элемента, содержащегося в массиве, указывается явно. Хотя на первый взгляд это выглядит довольно понятно, существует одна важная особенность. Как будет объясняться в главе 6, в основе каждого типа в системе типов .NET (в том числе фундаментальных типов данных) в конечном итоге лежит базовый класс System.Object. В результате получается, что в случае определения массива объектов находящиеся внутри него элементы могут представлять собой что угодно. Например, рассмотрим показанный ниже метод ArrayOfObjects () (который можно вызвать в Main () для целей тестирования). static void ArrayOfObjects () { Console. WriteLine ("=> Array of Objects."); // В массиве объектов могут содержаться элементы любого типа. object[] myObjects = new object[4]; myObjects[0] =10; myObjects[l] = false; myObjects[2] = new DateTimeA969, 3, 24); myObjects[3] = "Form & Void"; foreach (object obj in myObjects) { // Вывод имени типа и значения каждого элемента массива. Console.WriteLine("Туре: {0}, Value: {1}", obj.GetType (), obj ) ; } Console.WriteLine(); } В коде сначала осуществляется проход циклом по содержимому массива myObjects, а затем с использованием метода GetType () из System. Ob j ect выводится имя базового типа и значения всех элементов. Вдаваться в детали метода System. Ob ject.GetTypeO на этом этапе пока не требуется; сейчас главное уяснить только то, что с помощью этого метода можно получить полностью уточенное имя элемента (получение информации о типах и службы рефлексии подробно рассматриваются в главе 15). Ниже показано, как будет выглядеть вывод после вызова ArrayOf Ob j ects (). => Array of Objects. Type: System.Int32, Value: 10 Type: System.Boolean, Value: False Type: System.DateTime, Value: 3/24/1969 12:00:00 AM Type: System.String, Value: Form & Void Работа с многомерными массивами В дополнение к одномерным массивам, которые демонстрировались до сих пор, в С# также поддерживаются две разновидности многомерных массивов. Многомерные массивы первого вида называются прямоугольными массивами и содержат несколько измерений, где все строки имеют одинаковую длину. Объявляются и заполняются такие массивы следующим образом: static void RectMultidimensionalArray() { Console. WriteLine ("=> Rectangular multidimensional array."); // Прямоугольный многомерный массив. int[, ] myMatrix; myMatrix = new int[6,6];
164 Часть II. Главные конструкции программирования на С# // Заполнение массива F * б) . for (int i = 0; i < 6/ i++) for (int j = 0; j < 6; j++) myMatrix[i, j] = i * j; // Вывод массива F * б) . for (int i = 0; l < 6; i++) { for (int j = 0; j < 6; j++) Console.Write(myMatrix[l, j] + "\t"); Console.WriteLine(); } Console.WriteLine(); } Многомерные массивы второго вида называются зубчатыми (jagged) массивами и содержат некоторое количество внутренних массивов, каждый из которых может иметь собственный уникальный верхний предел, например: static void JaggedMultidimensionalArray() { Console. WriteLine ("=> Jagged multidimensional array.11); // Зубчатый многомерный массив (т.е. массив массивов) . // Здесь он будет состоять из 5 других массивов. int [ ] [ ] myJagArray = new int [ 5 ] [ ] ; // Создание зубчатого массива. for (int i=0; i < myJagArray.Length; i++) myJagArray [l] = new int[i + 7] ; // Вывод всех строк (не забывайте, что по умолчанию // каждый элемент устанавливается в 0) . for (int i = 0; i < 5; i++) { for(int j = 0; j < myJagArray[l].Length; j++) Console.Write(myJagArray[l] [j] + " ") ; Console.WriteLine (); } Console.WriteLine (); } На рис. 4.2 показано, как будет выглядеть вывод после вызова в Main () методов RectMultidimensionalArray () и JaggedMultidimensionalArray(). Of С .Window ;\systeml2\cmd.exe г'****"* i^iflin Fun with Arrays * Г1=> Rectangular multidimensional array. ю о о о о 0 12 3 4 0 2 4 6 8 0 3 6 9 12 0 4 8 12 16 0 5 10 15 20 |=> Jagged multidimensional array. ■0 0 0 0 0 0 0 ДО 0000000 looooooooo 0 000000000 poooooooooo ■Press any key to continue . . . 0 5 10 15 20 25 Рис. 4.2. Прямоугольные и зубчатые многомерные массивы
Глава 4. Главные конструкции программирования на С#: часть II 165 Использование массивов в качестве аргументов и возвращаемых значений После создания массив можно передавать как аргумент или получать в виде возвращаемого значения члена. Например, ниже приведен код метода Print Array (), который принимает в качестве входного параметра массив значений int и выводит каждое из них в окне консоли, а также метод GetString (), который заполняет массив значениями string и возвращает его вызывающему коду. static void PrintArray (int [ ] mylnts) { for(int i = 0; i < mylnts.Length; i++) Console.WriteLine("Item {0} is {1} ", i, mylnts[i]); } static string[] GetStringArray() { string[] theStrings = {"Hello", "from", "GetStringArray"}; return theStrings; } Вызываются эти методы так, как показано ниже: static void PassAndReceiveArrays () { Console.WriteLine("=>Arrays as params and return values."); // Передача массива в качестве параметра. int[] ages = {20, 22, 23, 0} ; PrintArray(ages); // Получение массива в качестве возвращаемого значения. string[] strs = GetStringArray(); foreach(string s in strs) Console.WriteLine(s); Console.WriteLine() ; } К этому моменту должно уже быть понятно, как определять, заполнять и изучать содержимое массивов в С#. В завершение картины рассмотрим роль класса System. Array. Базовый класс System. Array Каждый создаваемый массив получает большую часть функциональности от класса System. Array. Общие члены этого класса позволяют работать с массивом с использованием полноценной объектной модели. В табл. 4.2 приведено краткое описание некоторых наиболее интересных членов класса System.Array (полное описание этих и других членов данного класса можно найти в документации .NET Framework 4.0 SDK). Таблица 4.2. Некоторые члены класса System. Array Член класса System. Array Описание Clear () Статический метод, который позволяет устанавливать для всего ряда элементов в массиве пустые значения @ — для чисел, null — для объектных ссылок и false — для булевских выражений) СоруТо () Метод, который позволяет копировать элементы из исходного массива в целевой Length Свойство, которое возвращает информацию о количестве элементов в массиве
166 Часть II. Главные конструкции программирования на С# Окончание табл. 4.2 Член класса System. Array Описание Rank Свойство, которое возвращает информацию о количестве измерений в массиве Reverse () Статическое свойство, которое представляет содержимое одномерного массива в обратном порядке Sort () Статический метод, который позволяет сортировать одномерный массив внутренних типов. В случае реализации элементами в массиве интерфейса IComparer также позволяет сортировать и специальные типы (см. главу 9) Давайте теперь посмотрим, как некоторые из этих членов выглядят в действии. Ниже приведен вспомогательный метод, в котором с помощью статических методов Reverse () и Clear () в окне консоли выводится информация о массиве типов string: static void SystemArrayFunctionality () { Console.WriteLine ("=> Working with System.Array."); // Инициализация элементов при запуске. string[] gothicBands = {"Tones on Tail", "Bauhaus", "Sisters of Mercy"}; // Вывод элементов в порядке их объявления. Console.WriteLine("-> Here is the array:"); for (int i = 0; i < gothicBands.Length; i++) { // Вывод элемента. Console.Write(gothicBands [i] + " , " ) ; } Console.WriteLine("\n"); // Изменение порядка следования элементов на обратный... Array.Reverse(gothicBands); Console.WriteLine("-> The reversed array"); // ... и их вывод. for (int i=0; i < gothicBands.Length; i++) { // Вывод элемента. Console.Write(gothicBands[i] + ", "); } Console.WriteLine("\n"); // Удаление всех элементов, кроме одного. Console.WriteLine("-> Cleared out all but one..."); Array.Clear(gothicBands, 1, 2) ; for (int i = 0; l < gothicBands.Length; i++) { // Вывод элемента. Console.Write(gothicBands[i] + ", "); } Console.WriteLine(); } В результате вызова этого метода в Main () можно получить следующий вывод: => Working with System.Array. -> Here is the array: Tones on Tail, Bauhaus, Sisters of Mercy, -> The reversed array Sisters of Mercy, Bauhaus, Tones on Tail, -> Cleared out all but one. . . Sisters of Mercy, , ,
Глава 4. Главные конструкции программирования на С#: часть II 167 Обратите внимание, что многие из членов класса System.Array определены как статические и потому, следовательно, могут вызываться только на уровне класса (например, методы Array. Sort () и Array. Reverse ()). Таким методам передается массив, подлежащий обработке. Остальные члены System.Array (вроде свойства Length) действуют на уровне объекта и потому могут вызываться прямо на массиве. Исходный код. Приложение FunWithArrays доступно в подкаталоге Chapter 4. Тип enum Как рассказывалось в главе 1, в состав системы типов .NET входят классы, структуры, перечисления, интерфейсы и делегаты. Для начала рассмотрим роль перечислений (enum), создав новый проект типа Console Application по имени FunWithEnums. При построении той или иной системы зачастую удобно создавать набор символических имен, которые отображаются на известные числовые значения. Например, в случае создания системы начисления заработной платы может понадобиться ссылаться на сотрудников определенного типа (EmpType) с помощью таких констант, как VicePresident (вице-президент), Manager (менеджер), Contractor (подрядчик) и Grunt (рядовой сотрудник). Для этой цели в С# поддерживается понятие специальных перечислений. Например, ниже показано специальное перечисление по имени EmpType: // Специальное перечисление. enum EmpType { Manager, // = О Grunt, /7 = 1 Contractor, /7 = 2 VicePresident /7 = 3 } В перечислении EmpType определены четыре именованных константы, которые соответствуют дискретным числовым значениям. По умолчанию первому элементу присваивается значение 0, а всем остальным элементам значения присваиваются согласно схеме п+1. При желании исходное значение можно изменять как угодно. Например, если в данном примере нумерация членов EmpType должна идти с 102 до 105, необходимо поступить следующим образом: // Начинаем нумерацию со значения 102. enum EmpType { Manager = 102, Grunt, // = 103 Contractor, // = 104 VicePresident // = 105 } Нумерация в перечислениях вовсе не обязательно должна быть последовательной и содержать только уникальные значения. Например, вполне допустимо (по той или иной причине) сконфигурировать перечисление EmpType показанным ниже образом: // Значения элементов в перечислении вовсе не обязательно // должны идти в последовательном порядке. enum EmpType { Manager = 10, Grunt = 1, Contractor = 100, VicePresident = 9 }
168 Часть II. Главные конструкции программирования на С# Управление базовым типом, используемым для хранения значений перечисления По умолчанию для хранения значений перечисления используется тип System. Int32 (который в С# называется просто int); однако при желании его легко заменить. Перечисления в С# можно определять аналогичным образом для любых других ключевых системных типов (byte, short, int или long). Например, чтобы сделать для перечисления EmpType базовым тип byte, а не int, напишите следующий код: //На этот раз ЕшрТуре отображается на тип byte. enum EmpType : byte { Manager = 10, Grunt = 1, Contractor = 100, VicePresident = 9 } Изменять базовый тип перечислений удобно в случае создания таких приложений .NET, которые будут развертываться на устройствах с небольшим объемом памяти (таких как поддерживающие .NET сотовые телефоны или устройства PDA), чтобы экономить память везде, где только возможно. Естественно, если для перечисления в качестве базового типа указан byte, каждое значение в этом перечислении ни в коем случае не должно выходить за рамки диапазона его допустимых значений. Например, приведенная ниже версия EmpType вызовет ошибку на этапе компиляции, поскольку значение 999 не вписывается в диапазон допустимых значений типа byte: // Компилятор сообщит об ошибке! Значение 999 // является слишком большим для типа byte1 enum EmpType : byte { Manager = 10, Grunt = 1, Contractor = 100, VicePresident = 999 } Объявление переменных типа перечислений После указания диапазона и базового типа перечисление можно использовать вместо так называемых "магических чисел". Поскольку перечисления представляют собой не более чем просто определяемый пользователем тип данных, их можно применять в качестве возвращаемых функциями значений, параметров методов, локальных переменных и т.д. Для примера давайте создадим метод по имени AskForBonus (), принимающий в качестве единственного параметра переменную EmpType. На основе значения этого входного параметра в окне консоли будет выводиться соответствующий ответ на запрос о надбавке к зарплате. class Program { static void Main(string[] args) { Console.WriteLine ("**** Fun with Enums *****"); // Создание типа Contractor. EmpType emp = EmpType.Contractor; AskForBonus(emp); Console.ReadLine(); }
Глава 4. Главные конструкции программирования на С#: часть II 169 // Использование перечислений в качестве параметра. static void AskForBonus(EmpType e) { switch (e) { case EmpType.Manager: Console.WriteLine ("How about stock options instead?"); //He желаете ли взамен фондовые опционы? break; case EmpType.Grunt: Console.WriteLine("You have got to be kidding..."); // Шутить изволите. . . break; case EmpType.Contractor: Console.WriteLine("You already get enough cash..."); // У вас уже достаточно наличности... break; case EmpType.VicePresident: Console.WriteLine("VERY GOOD, Sir1"); // Очень хорошо, сэр! break; } } } Обратите внимание, что при присваивании значения переменной перечисления перед значением (Grunt) должно обязательно указываться имя перечисления (EmpType). Из-за того, что перечисления представляют собой фиксированные наборы пар "имя/ значение", устанавливать для переменной перечисления значение, которое не определено напрямую в перечисляемом типе, не допускается: static void ThisMethodWillNotCompile() { // Ошибка! Значения SalesManager в перечислении EmpType нет! EmpType emp = EmpType.SalesManager; // Ошибка! Забыли указать имя перечисления EmpType перед значением Grunt! emp = Grunt; } Тип System. Enum Интересный аспект перечислений в .NET связан с тем, что функциональность они получают от типа класса System.Enum. В этом классе предлагается набор методов, которые позволяют опрашивать и преобразовывать заданное перечисление. Одним из наиболее полезных среди них является метод Enum.GetUnderlyingType (), который возвращает тип данных, используемый для хранения значений перечислимого типа (в рассматриваемом объявлении EmpType это System.Byte): static void Main(string [ ] args) { Console.WriteLine("**** Fun with Enums *****"); // Создание типа Contractor. EmpType emp = EmpType.Contractor; AskForBonus(emp); // Отображение информации о типе, который используется // для хранения значений перечисления. Console.WriteLine("EmpType uses a {0} for storage", Enum.GetUnderlyingType(emp.GetType())); Console.ReadLine() ; }
170 Часть II. Главные конструкции программирования на С# Заглянув в окно Object Browser (Браузер объектов) в Visual Studio 2010, можно удостовериться, что метод Enum. GetUnderlyingType () требует передачи в качестве первого параметра System.Type. Как будет подробно разъясняться в главе 16, Туре представляет описание метаданных для заданной сущности .NET. Один из возможных способов получения метаданных (как показывалось ранее) предусматривает применение метода Get Туре (), который является общим для всех типов в библиотеках базовых классов .NET. Другой способ состоит в использовании операции typeof, поддерживаемой в С#. Одно из преимуществ этого способа связано с тем, что он не требует объявления переменной сущности, описание метаданных которой требуется получить: //На этот раз для извлечения информации //о типе применяется операция typeof. Console.WriteLine("EmpType uses a {0} for storage", Enum.GetUnderlyingType(typeof(EmpType))); Динамическое обнаружение пар "имя/значение" перечисления Помимо метода Enum.GetUnderlyingType (), все перечисления С# также поддерживают метод по имени ToStringO, который возвращает имя текущего значения перечисления в виде строки. Ниже приведен соответствующий пример кода: static void Main(string [ ] args) { Console.WriteLine ("**** Fun with Enums *****"); EmpType emp = EmpType.Contractor; // Выводит строку "emp is a Contractor". Console.WriteLine("emp is a {0}.", emp.ToString()); Console.ReadLine() ; } Чтобы выяснить не имя, а значение определенной переменной перечисления, можно просто привести ее к лежащему в основе типу хранения. Ниже показан пример, как это делается: static void Main(string[] args) { Console.WriteLine("**** Fun with Enums *****"); EmpType emp = EmpType.Contractor; // Выводит строку "Contractor = 100". Console.WriteLine("{0} = {1}", emp.ToString() , (byte)emp); Console.ReadLine(); } На заметку! Статический метод Enum. Format () позволяет производить более точное форматирование за счет указания флага, представляющего желаемый формат. Более подробную информацию о нем можно найти в документации .NET Framework 4.0 SDK. В System. Enum также предлагается еще один статический метод по имени GetValues (). Этот метод возвращает экземпляр System. Array. Каждый элемент в массиве соответствует какому-то члену в указанном перечислении. Для примера рассмотрим показанный ниже метод, который выводит в окне консоли пары "имя/значение", имеющиеся в передаваемом в качестве параметра перечислении: // Этот метод отображает детали любого перечисления. static void EvaluateEnum(System.Enum e) {
Глава 4. Главные конструкции программирования на С#: часть II 171 Console.WriteLine ("=> Information about {О}11, e.GetType().Name); Console.WriteLine("Underlying storage type: { 0}" , Enum.GetUnderlyingType(e.GetType()))/ // Получение всех пар "имя/значение" // для входного параметра. Array enumData = Enum.GetValues(e.GetType()); Console.WriteLine("This enum has {0} members.", enumData.Length), // Количество членов: /Л Вывод строкового имени и ассоциируемого значения //с использованием флага D для форматирования (см. главу 3) . for(int i=0; i < enumData.Length; i++) { Console.WriteLine("Name: enumData.GetValue(i) ) , } Console.WriteLine() ; {0}, Value: {0:D} } Чтобы протестировать этот новый метод, давайте обновим Main () так, чтобы в нем создавались переменные нескольких объявленных в пространстве имен System типов перечислений (вместе с перечислением EmpType): static void Main(string[] args) { Console.WriteLine("**** Fun with Enums EmpType e2 = EmpType.Contractor; // Эти типы представляют собой перечисления //из пространства имен System. DayOfWeek day = DayOfWeek.Monday; ConsoleColor cc = ConsoleColor.Gray; • * * * * ii EvaluateEnum(e2); EvaluateEnum(day); EvaluateEnum(cc); Console.ReadLine(); } На рис. 4.3 показано, как будет выглядеть вывод в этом случае. Как можно будет убедиться по ходу настоящей книги, перечисления очень широко применяются во всех библиотеках базовых классов .NET. Например, в ADO.NET множество перечислений используется для обозначения состояния соединения с базой данных (например, открыто оно или закрыто) и состояния строки в DataTable (например, является она измененной, новой или отсоединенной). Поэтому в случае применения любых перечислений следует всегда помнить о наличии возможности взаимодействовать с парами "имя/значение" в них с помощью членов System.Enum. Исходный код. Проект FunWithEnums доступен в подкаталоге Chapter 4. | C:\Windows\iysteml шшш [**** Fun with Enums Information about EmpType {Underlying storage type: System.Byte TThis enum has 4 members. {Name: Grunt, Value: 1 [Name: VicePresident, Value: 9 (Name: Manager, Value: 10 Name: Contractor, Value: 100 j=> Information about DayOfWeek I [Underlying storage type: System.Int32 IfThis enum has 7 members. Sunday, Value: 0 Monday, Value: 1 ilName: Tuesday, Value: 2 (Name: Wednesday, Value: 3 [Name: Thursday, Value: 4 lame: Friday, Value: 5 lame: Saturday, Value: 6 :> Information about ConsoleColor | {underlying storage type: System.Int32 IfThis enum has 16 members. Name: Black. Value: 0 Wame: DarkBlue, Value: 1 [Name: DarkGreen, Value: 2 Name: DarkCyan, Value: 3 JName: DarkRed, Value: 4 Name: DarkMaqenta, Value: 5 Name: Dark Ye How, Value: б Name: Gray, Value: 7 |Name: DarkGray Value: 8 Blue, Value: 9 [Name: Green, Value: 10 ■Name: Cyan, Value: 11 Name: Red, Value: 12 Name: Magenta, Value: 13 ■Name: Yellow, Value: 14 white, Value: 15 ijinitiiiiiiiiiiiiUMtinii Рис. 4.З. Динамическое получение пар "имя/значение" типов перечислений
172 Часть II. Главные конструкции программирования на С# Типы структур Теперь, когда роль типов перечислений должна быть ясна, давайте рассмотрим использование типов структур (struct) в .NET. Типы структур прекрасно подходят для моделирования математических, геометрических и прочих "атомарных" сущностей в приложении. Они (как и перечисления) представляют собой определяемые пользователем типы, однако (в отличие от перечислений) просто коллекциями пар "имя/значение" - не являются. Вместо этого они скорее представляют собой типы, способные содержать любое количество полей данных и членов, выполняющих над ними какие-то операции. На заметку! Если вы ранее занимались объектно-ориентированным программированием, можете считать структуры "облегченными классами", поскольку они тоже предоставляют возможность определять тип, поддерживающий инкапсуляцию, но не могут применяться для построения семейства взаимосвязанных типов. Когда есть потребность в создании семейства взаимосвязанных типов через наследование, нужно применять типы классов. На первый взгляд процесс определения и использования структур выглядит очень просто, но, как известно, сложности обычно скрываются в деталях. Чтобы приступить к изучению основных аспектов структур, давайте создадим новый проект по имени FunWithStructures. В С# структуры создаются с помощью ключевого слова struct. Поэтому далее определим в проекте с использованием этого ключевого слова структуру Point и добавим в нее две переменных экземпляра типа int и несколько методов для взаимодействия с ними. struct Point { // Поля структуры. public int X; public int Y; // Добавление 1 к позиции (X, Y) . public void Increment () { X++; Y++; } // Вычитание 1 из позиции (X, Y) . public void Decrement () { X--; Y—; } // Отображение текущей позиции. public void Display() { Console.WriteLineCX = {0}, Y= {1}", X, Y) ; } } Здесь определены два целочисленных поля (X и Y) с использованием ключевого слова public, которое представляет собой один из модификаторов управления доступом (см. главу 5). Объявление данных с использованием ключевого слова public гарантирует наличие у вызывающего кода возможности напрямую получать к ним доступ из данной переменной Point (через операцию точки). На заметку! Обычно определение общедоступных (public) данных внутри класса или структуры считается плохим стилем. Вместо этого рекомендуется определять приватные (private) данные и получать к ним доступ и изменять их с помощью общедоступных свойств. Более подробно об этом будет рассказываться в главе 5.
Глава 4. Главные конструкции программирования на С#: часть II 173 Ниже приведен код метода Main (), с помощью которого можно протестировать тип Point. static void Main(string[] args) I Console.WriteLine("***** a First Look at Structures *****"); // Создание Point с первоначальными значениями X и Y. Point myPoint; myPoint.X = 34 9; myPoint.Y =76; myPoint.Display(); // Настройка значений Х и Y. myPoint.Increment (); myPoint.Display(); Console.ReadLine (); } Вывод, как и следовало ожидать, выглядит следующим образом: ***** д First Look at Structures ***** X = 349, Y = 76 X = 350, Y = 77 Создание переменных типа структур Для создания переменной типа структуры на выбор доступно несколько вариантов. Ниже просто создается переменная Point с присваиванием значений каждому из ее общедоступных элементов данных типа полей перед вызовом ее членов. Если значения общедоступным элементам структуры (в данном случае X и Y) не присвоены перед ее использованием, компилятор сообщит об ошибке. // Ошибка! Полю Y не было присвоено значение. Point pi; pl.X = 10; pi.Display(); // Все в порядке1 Обоим полям были присвоены // значения перед использованием. Point p2; р2.Х = 10; p2.Y = 10; ' р2.Display(); В качестве альтернативного варианта переменные типа структур можно создавать с применением ключевого слова new, поддерживаемого в С#, что предусматривает вызов для структуры конструктора по умолчанию. По определению используемый по умолчанию конструктор не принимает никаких аргументов. Преимущество подхода с вызовом для структуры конструктора по умолчанию состоит в том, что в таком случае каждому элементу данных полей автоматически присваивается соответствующее значение по умолчанию: // Установка для всех полей значений по умолчанию //за счет применения конструктора по умолчанию. Point pi = new Point () ; // Выводит Х=0, Y=0 pi.Display(); Создавать структуру можно также с помощью специального конструктора, что позволяет указывать значения для полей данных при создании переменной, а не уста-
174 Часть II. Главные конструкции программирования на С# навливать их для каждого из них по отдельности. В главе 5 специальные конструкторы будут рассматриваться более подробно; здесь же, чтобы в общем посмотреть, как их использовать, изменим структуру Point и добавим в нее следующий код: struct Point { // Поля структуры. public int X; public int Y; // Специальный конструктор. public Point (int XPos, int YPos) { X = XPos; Y = YPos; } } После этого переменные типа Point можно создавать так, как показано ниже: // Вызов специального конструктора. Point p2 = new Point E0, 60); // Выводит Х=50, Y=60 р2.Display (); Как упоминалось ранее, на первый взгляд процесс работы со структурами выглядит довольно понятно. Однако чтобы лучше разобраться в нем, необходимо знать, в чем состоит разница между типами значения и ссылочными типами в .NET. Исходный код. Проект FunWithStructures доступен в подкаталоге Chapter 4. Типы значения и ссылочные типы На заметку! В приведенном далее обсуждении типов значения и ссылочных типов предполагается наличие базовых знаний объектно-ориентированного программирования. Дополнительные сведения по этому поводу даны в главах 5 и 6. В отличие от массивов, строк и перечислений, структуры в С# не имеют эквивалентного представления с похожим названием в библиотеке .NET (т.е. класса вроде System. Structure не существует), но зато они все неявно унаследованы от класса System. ValueType. Попросту говоря, роль класса System. ValueType заключается в гарантировании размещения производного типа (например, любой структуры) в стеке, а не в куче с автоматически производимой сборкой мусора. Данные, размещаемые в стеке, могут создаваться и уничтожаться очень быстро, поскольку срок их жизни зависит только от контекста, в котором они определены. За данными, размещаемыми в куче, наблюдает сборщик мусора .NET, и время их существования зависит от целого ряда различных факторов, которые более подробно рассматриваются в главе 8. С функциональной точки зрения единственной задачей System.ValueType является переопределение виртуальных методов, объявленных в System.Object, так, чтобы в них использовалась семантика, основанная на значениях, а не на ссылках. Как, возможно, уже известно, под переопределением понимается изменение реализации виртуального (или, что тоже возможно, абстрактного) метода, определенного внутри базового класса. Базовым классом для ValueType является System.Object. В действительности
Глава 4. Главные конструкции программирования на С#: часть II 175 методы экземпляра, определенные в System. ValueType, идентичны тем, что определены в System.Object: // Структуры и перечисления неявным образом // расширяют возможности System.ValueType. public abstract class ValueType : object { public virtual bool Equals(object obj ) ; public virtual int GetHashCode (); public Type GetTypeO ; public virtual string ToStringO; } Из-за того, что в типах значения используется семантика, основанная на значениях, время жизни структуры (которая включает все числовые типы данных, наподобие int, float и т.д., а также любое перечисление или специальную структуру) получается очень предсказуемым. При выходе переменной типа структуры за пределы контекста, в котором она определялась, она сразу же удаляется из памяти. // Локальные структуры извлекаются из стека // после завершения метода. static void LocalValueTypes () { //В действительности int представляет // собой структуру System.Int32. int i = 0; //В действительности Point представляет // собой тип структуры. Point p = new Point () ; } // Здесь i и р изымаются из стека. Типы значения, ссылочные типы и операция присваивания Когда один тип значения присваивается другому, получается почленная копия данных полей. В случае простого типа данных вроде System. Int32 единственным копируемым членом является числовое значение. Однако в ситуации с типом Point копироваться в новую переменную структуры будут два значения: X и Y. Чтобы удостовериться в этом, давайте создадим новый проект типа Console Application по имени ValueAndReferenceTypes, скопируем в новое пространство имен предыдущее определение Point и добавим в Program следующий метод: // Присваивание двух внутренних типов значения друг другу // приводит к созданию двух независимых переменных в стеке. static void ValueTypeAssignment() { Console.WriteLine("Assigning value types\n"); Point pi = new Point A0, 10); Point p2 = pi; // Вывод обеих переменных Point, pi.Display(); p2.Display(); // Изменение значение pl.X и повторный вывод. // Значение р2.Х не изменяется. pl.X = 100; Console.WriteLine("\n=> Changed pi.X\n"); pi.Display(); p2.Display(); }
176 Часть II. Главные конструкции программирования на С# В коде сначала создается переменная типа Point (pi), которая затем присваивается другой переменной типа Point (p2). Из-за того, что Point представляет собой тип значения, в стеке размещаются две копии My Point, каждая из которых может подвергаться отдельным манипуляциями. Поэтому при изменении значения pi .X значение р2 .X остается неизменным. Ниже показано, как будет выглядеть вывод в сл}£чае выполнения этого кода: Assigning value types X = 10, Y = 10 X = 10, Y = 10 => Changed pi.X X = 100, Y = 10 X = 10, Y = 10 В отличие от присваивания одного типа значения другому, в случае применения операции присваивания в отношении ссылочных типов (т.е. всех экземпляров класса) происходит переадресация на то, на что ссылочная переменная указывает в памяти. Чтобы увидеть это на примере, давайте создадим новый тип класса по имени PointRef с точно такими же членами, как у структуры Point, только переименуем конструктор в соответствие с именем этого класса: // Классы всегда представляют собой ссылочные типы. class PointRef { // Те же члены, что и в структуре Point. . . // Здесь нужно не забыть изменить имя конструктора на PointRef. public PointRef(int XPos, int YPos) { X = XPos; Y = YPos; } } Теперь воспользуемся этим типом PointRef в показанном ниже новом методе. Обратите внимание, что за исключением применения класса PointRef, а не структуры Point, код в целом выглядит точно так же, как и код приведенного ранее метода ValueTypeAssignment(). static void ReferenceTypeAssignment () { Console.WriteLine("Assigning reference types\n"); PointRef pi = new PointRef A0, 10); PointRef p2 = pi; // Вывод обеих переменных PointRef. pi.Display(); p2.Display(); // Изменение значения pl.X и повторный вывод. pl.X = 100; Console.WriteLine("\n=> Changed pi.X\n"); pi.Display(); p2.Display(); } В данном случае создаются две ссылки, указывающие на один и тот же объект в управляемой куче. Поэтому при изменении значения X по ссылке р2 значение pi; X остается прежним. Ниже показано, как будет выглядеть вывод в случае вызова этого нового метода из Main ().
Глава 4. Главные конструкции программирования на С#: часть II 177 Assigning reference types X = 10, Y = 10 X = 10, Y = 10 => Changed pl.X X = 100, Y = 0 X = 100, Y = 10 Типы значения, содержащие ссылочные типы Теперь, когда стало более понятно, в чем состоят основные отличия между типами значения и ссылочными типами, давайте рассмотрим более сложный пример. Сначала предположим, что в распоряжении имеется следующий ссылочный тип (класс) с информационной строкой (infoString), которая может устанавливаться с помощью специального конструктора: class Shapelnfo { public string infoString; public Shapelnfo(string info) { infoString = info; } 1 Пусть необходимо, чтобы переменная этого типа класса содержалась внутри типа значения по имени Rectangle, и чтобы вызывающий код мог устанавливать значение внутренней переменной экземпляра Shapelnfo, для чего также можно предусмотреть специальный конструктор. Ниже показано полное определение типа Rectangle. struct Rectangle { // Структура Rectangle содержит член ссылочного типа. public Shapelnfo rectlnfo; public int rectTop, rectLeft, rectBottom, rectRight; public Rectangle(string info, int top, int left, int bottom, int right) { rectlnfo = new Shapelnfo(info); rectTop = top; rectBottom = bottom; rectLeft = left; rectRight = right; } public void Display() { Console.WnteLine ("String = {0}, Top = {1}, Bottom = {2}," + "Left = {3}, Right = {4}", rectlnfo.infoString, rectTop, rectBottom, rectLeft, rectPight); } } Ссылочный тип содержится внутри типа значения, и отсюда, естественно, вытекает важный вопрос о том, что же произойдет в результате присваивания одной переменной типа Rectangle другой? Исходя из того, что известно о типах значения, можно верно предположить, что целочисленные данные (которые в действительности образуют структуру) должны являться независимой сущностью для каждой переменной Rectangle. Но что будет с внутренним ссылочным типом? Будут ли копироваться все данные о состоянии этого объекта, или же только ссылка на него? Чтобы получить ответ на этот вопрос, определим показанный ниже метод и вызовем его в Main ().
178 Часть II. Главные конструкции программирования на С# static void ValueTypeContainingRefType() { // Создание первой переменной Rectangle. Console.WriteLine("-> Creating rl11); Rectangle rl = new Rectangle ("First Rect", 10, 10, 50, 50); // Присваивание второй переменной Rectangle ссылки на rl. Console.WriteLine ("-> Assigning r2 to rl"); Rectangle r2 = rl; // Изменение некоторых значений в г2. Console.WriteLine ("-> Changing values of r2"); r2.rectlnfo.infoString = "This is new info1"; r2.rectBottom = 4 4 4 4; // Вывод обеих переменных, rl.Display(); r2.Display(); } Ниже показан вывод, полученный в результате вызова этого метода: -> Creating rl -> Assigning r2 to rl -> Changing values of r2 String = This is new info!, Top = 10, Bottom = 50, Left = 10, Right = 50 String = This is new info!, Top = 10, Bottom = 4444, Left = 10, Right = 50 Нетрудно заметить, что при изменении значения информационной строки с использованием ссылки г2, значение ссылки rl остается прежним. По умолчанию, когда внутри типа значения содержатся другие ссылочные типы, операция присваивания приводит к копированию ссылок. В результате получаются две независимых структуры, в каждой из которых содержится ссылка, указывающая на один и тот же объект в памяти (т.е. поверхностная копия). При желании получить детальную копию, при которой в новый объект копируются полностью все данные состояния внутренних ссылок, в качестве одного из способов можно реализовать интерфейс ICloneable (см. главу 9). Исходный код. Проект ValueAndReferenceTypes доступен в подкаталоге Chapter 4. Передача ссылочных типов по значению Очевидно, что ссылочные типы и типы значения могут передаваться членам в виде параметров. Способ передачи ссылочного типа (например, класса) по ссылке, однако довольно сильно отличается от способа его передачи по значению. Чтобы посмотреть, в чем разница, давайте создадим новый проект типа Console Application по имени Ref TypeValTypeParams и определим в нем следующий простой класс Person: class Person { public string personName; public int personAge; // Конструкторы. public Person(string name, int age) { personName = name; personAge = age; } public Person () {}
Глава 4. Главные конструкции программирования на С#: часть II 179 public void Display () { Console.WriteLine ("Name: {0}, Age: {1}", personName, personAge); } } Теперь создадим метод, позволяющий вызывающему коду передавать тип Person по значению (обратите внимание, что никакие модификаторы для параметров, подобные out или ref, здесь не используются): static void SendAPersonByValue(Person p) { // Изменение значения возраста в р. p.personAge =99; // Увидит ли вызывающий код это изменение? р = new Person("Nikki", 99); } Важно обратить внимание, что метод SendAPersonByValue () пытается присвоить входному аргументу Person ссылку на новый объект Person, а также изменить некоторые данные состояния. Испробуем этот метод, вызвав его в Main (): static void Main(string[] args) { // Передача ссылочных типов по значению. Console.WriteLine ("***** Passing Person object by value *****"); Person fred = new Person ("Fred", 12); Console.WriteLine("\nBefore by value call, Person is:"); // перед вызовом fred.Display(); SendAPersonByValue(fred) ; Console.WriteLine("\nAfter by value call, Person is:"); // после вызова fred. Display(); Console.ReadLine(); } Ниже показан результирующий вывод: ***** Passing Person object by value ***** Before by value call, Person is: Name: Fred, Age: 12 After by value call, Person is: Name: Fred, Age: 99 Как видите, значение PersoneAge изменилось. Кажется, что такое поведение противоречит смыслу передачи параметра "по значению". Из-за удавшейся попытки изменить состояние входного объекта Person возникает вопрос о том, что же тогда было скопировано? А вот что: ссылка на объект вызывающего кода. Поскольку метод SendAPersonByValue () теперь указывает на тот же самый объект, что и вызывающий код, получается, что данные состояния объекта можно изменять. Что нельзя делать, так это изменять объект, на который указывает ссылка. Передача ссылочных типов по ссылке Теперь создадим метод SendAPersonByReference (), в котором ссылочный тип передается по ссылке (обратите внимание, что здесь для параметра используется модификатор ref): public static void SendAPersonByReference(ref Person p) { // Изменение некоторых данных в р. p.personAge = 555;
180 Часть II. Главные конструкции программирования на С# // Теперь р указывает на новый объект в куче. р = new Person("Nikki", 222); } Нетрудно догадаться, что такой подход предоставляет вызывающему коду полную свободу в плане манипулирования входным параметром. Вызывающий код может не только изменять состояние объекта, но и переопределять ссылку так, чтобы она указывала на новый тип Person. Давайте испробуем новый метод SendAPersonByRef erence (), вызвав его в методе Main (), как показано ниже. static void Main (string [ ] args) { // Передача ссылочных типов по ссылке. Console. WriteLine ("***** Passing Person object by reference *****"); Person mel = new Person ("Mel", 23) ; Console.WriteLine("Before by ref call, Person is:"); mel.Display(); SendAPersonByReference (ref mel); Console.WriteLine("After by ref call, Person is:"); mel.Display (); Console.ReadLine(); } Ниже показано, как в этом случае выглядит вывод: ***** Passing Person object by reference ***** Before by ref call, Person is: Name: Mel, Age: 2 3 After by ref call, Person is: Name: Nikki, Age: 999 Как не трудно заметить, после вызова объект по имени Mel возвращается как объект по имени Nikki, поскольку методу удалось изменить тип, на который указывала входная ссылка в памяти. Ниже перечислены главные моменты, которые можно вынести из всего этого и о которых следует всегда помнить при передаче ссылочных типов. • В случае передачи ссылочного типа по ссылке вызывающий код может изменять значения данных состояния объекта, а также сам объект, на который указывает входная ссылка. • В случае передачи ссылочного типа по значению вызывающий код может изменять только значения данных состояния объекта, но не сам объект, на который указывает входная ссылка. Исходный код. Проект RefTypeValTypeParams доступен в подкаталоге Chapter 4. Заключительные детали относительно типов значения и ссылочных типов В завершение темы ссылочных типов и типов значения ознакомьтесь с табл. 4.3, где приведено краткое описание всех основных отличий между типами значения и ссылочными типами. Несмотря на все эти отличия, не следует забывать о том, что типы значения и ссылочные типы обладают способностью реализовать интерфейсы и могут поддерживать любое количество полей, методов, перегруженных операций, констант, свойств и событий.
Глава 4. Главные конструкции программирования на С#: часть II 181 Таблица 4.3. Отличия между типами значения и ссылочными типами Интересующий вопрос Тип значения Ссылочный тип Где размещается этот тип? Как представляется переменная? Какой тип является базовым? Может ли этот тип выступать в роли базового для других типов? Каково по умолчанию поведение при передаче параметров? Может ли в этом типе переопределяться метод System. Object.Finalize()? Можно ли для этого типа определять конструкторы? Когда переменные этого типа прекращают свое существование? В стеке В виде локальной копии Должен обязательно наследоваться от System. ValueType Нет. Типы значения всегда являются запечатанными, поэтому наследовать от них нельзя Переменные этого типа передаются по значению (т.е. вызываемой функции передается копия переменной) Нет. Типы значения, никогда не размещаются в куче и потому в финализации не нуждаются Да, но при этом следует помнить, что имеется зарезервированный конструктор по умолчанию (это значит, что все специальные конструкторы должны обязательно принимать аргументы) Когда выходят за рамки того контекста, в котором определялись В управляемой куче В виде ссылки, указывающей на занимаемое соответствующим экземпляром место в памяти Может наследоваться от любого другого типа (кроме System.ValueType), главное чтобы тот не был запечатанным (см. главу 6) Да. Если тип не является запечатанным, он может выступать в роли базового типа для других типов В случае типов значения объект копируется по значению, а в случае ссылочных типов ссылка копируется по значению Да, неявным образом (см. главу 8) Безусловно! Когда объект подвергается сборке мусора Нулевые типы в С# В заключение настоящей главы давайте рассмотрим роль нулевых типов данных (nullable data types) на примере создания консольного приложения по имени NullableTypes. Как уже известно, типы данных CLR обладают фиксированным диапазоном значений и имеют соответствующий представляющий их тип в пространстве имен System. Например, типу данных System.Boolean могут присваиваться только значения из набора {true, false}. Вспомните, что все числовые типы данных (а также тип Boolean) представляют собой типы значении. Таким типам значение null никогда не присваивается, поскольку оно служит для установки пустой ссылки на объект: static void Main(string [ ] args) { // Компилятор сообщит об ошибке! // Типам значения не может присваиваться значение null!
182 Часть II. Главные конструкции программирования на С# bool myBool = null; int mylnt = null; // Здесь все в порядке, потому что строки представляют собой ссылочные типы. string myString = null; } Возможность создавать нулевые типы появилась еще в версии .NET 2.0. Попросту говоря, нулевой тип может принимать все значения лежащего в его основе типа плюс значение null. Если объявить нулевым, например, тип bool, его допустимыми значениями будут true, false и null. Это может оказаться чрезвычайно удобным при работе с реляционными базами данных, поскольку в их таблицах довольно часто встречаются столбцы с неопределенными значениями. Помимо нулевого типа данных в С# больше не существует никакого удобного способа для представления элементов данных, не имеющих значения. Чтобы определить переменную нулевого типа, необходимо присоединить к имени лежащего в основе типа данных знак вопроса (?). Обратите внимание, что применение такого синтаксиса является допустимым лишь в отношении типов значения. При попытке создать нулевой ссылочный тип (в том числе нулевой тип string) компилятор будет сообщать об ошибке. Как и ненулевым переменным, нулевым локальным переменным должно быть присвоено начальное значение, прежде чем их можно будет использовать. static void LocalNullableVariables () { // Определение нескольких локальных переменных с нулевыми типами. int? nullablelnt = 10; double? nullableDouble = 3.14; bool? nullableBool = null; char? nullableChar = 'a1; int?[] arrayOfNullablelnts = new int? [10]; // Ошибка! Строки относятся к ссылочным типам! // string? s = "oops"; } Синтаксис с использованием знака ? в качестве суффикса в С# представляет собой сокращенный вариант создания экземпляра обобщенного типа структуры System.Nullable<T>. Хотя об обобщениях будет подробно рассказываться лишь в главе 10, уже сейчас важно понять, что тип System.Nullable<T> имеет набор членов, которые могут применяться во всех нулевых типах. Например, выяснить программным образом, было ли нулевой переменной действительно присвоено значение null, можно с помощью свойства HasValue или операции ! =, а получить присвоенное нулевому типу значение — либо напрямую, либо через свойство Value. На самом деле, из-за того, что суффикс ? является всего лишь сокращенным вариантом использования типа Nullable<T>, метод LocalNullableVariables () вполне можно было бы реализовать и следующим образом: static void LocalNullableVariablesUsingNullable () { // Определение нескольких нулевых типов за счет использования Nullable<T>. Nullable<int> nullablelnt = 10; Nullable<double> nullableDouble = 3.14; Nullable<bool> nullableBool = null; Nullable<char> nullableChar = 'a'; Nullable<int>[] arrayOfNullablelnts = new int? [10]; }
Глава 4. Главные конструкции программирования на С#: часть II 183 Работа с нулевыми типами Как упоминалось ранее, нулевые типы данных особенно полезны при взаимодействии с базами данных, поскольку некоторые столбцы внутри таблиц данных в них могут преднамеренно делаться пустыми (с неопределенными значениями). Для примера рассмотрим приведенный ниже класс, в котором имитируется процесс получения доступа к базе данных с таблицей, два столбца в которой могут принимать значения null. Обратите внимание, что в методе GetlntFromDatabase () значение нулевой целочисленной переменной экземпляра не присваивается, а в методе GetBoolFromDatabase () значение члену bool? присваивается: class DatabaseReader { // Нулевые поля данных. public int? numericValue = null; public bool? boolValue = true; // Обратите внимание на использование // нулевого возвращаемого типа. public int? GetlntFromDatabase() { return numericValue; } // Здесь тоже обратите внимание на использование // нулевого возвращаемого типа. public bool? GetBoolFromDatabase () { return boolValue; } } Теперь давайте создадим следующий метод Main (), в котором будет вызываться каждый из членов класса DatabaseReader и выясняться, какие значения были им присвоены, с помощью членов Has Value и Value, а также поддерживаемой в С# операции равенства (точнее — операции "не равно"): static void Main(string[] args) { Console.WriteLine ("***** Fun with Nullable Data *****\n"); DatabaseReader dr = new DatabaseReader (); // Получение значения int из "базы данных". int? i = dr.GetlntFromDatabase(); if (i.HasValue) // вывод значения i Console.WriteLine("Value of 'i' is: {0}", i.Value); else // значение i не определено Console.WriteLine("Value of 'i1 is undefined."); // Получение значения bool из "базы данных" . bool? b = dr.GetBoolFromDatabase(); if (b != null) // вывод значения b Console.WriteLine("Value of 'b' is: {0}", b.Value); else // значение b не определено Console.WriteLine("Value of 'b' is undefined."); Console.ReadLine();
184 Часть II. Главные конструкции программирования на С# Операция'??' Последним аспектом нулевых типов, о котором следует знать, является использование с ними операции ??, поддерживаемой в С#. Эта операция позволяет присваивать значение нулевому типу, если извлеченное значение равно null. Для примера предположим, что в случае возврата методом GetlntFromDatabase () значения null (разумеется, этот метод запрограммирован всегда возвращать null, но тут главное уловить общую идею) локальной целочисленной переменной нулевого типа должно присваиваться значение 100: static void Main(string[] args) { Console.WriteLine ("***** Fun with Nullable Data *****\n"); DatabaseReader dr = new DatabaseReader(); // В случае возврата GetlntFromDatabase() // значения null локальной переменной должно // присваиваться значение 100. int myData = dr.GetlntFromDatabase() ?? 100; Console.WriteLine("Value of myData: {0}", myData); Console.ReadLine(); } Преимущество подхода с применением операции ? ? в том, что он обеспечивает более компактную версию кода, чем применение традиционной условной конструкции if /else. При желании можно написать следующий функционально эквивалентный код, который в случае null устанавливает значение переменной равным 100: // Более длинная версия применения синтаксиса ? : ??. int? moreData = dr.GetlntFromDatabase(); if (ImoreData.HasValue) moreData = 100; Console.WriteLine("Value of moreData: {0}", moreData); Исходный код. Приложение NullableType доступно в подкаталоге Chapter 4. Резюме В настоящей главе сначала рассматривался ряд ключевых слов С#, которые позволяют создавать специальные методы. Вспомните, что по умолчанию параметры передаются по значению, однако их можно передавать и по ссылке за счет добавления к ним модификатора ref или out. Также было рассказано о роли необязательных и именованных параметров и о том, как определять и вызывать методы, принимающие массивы параметров. В главе рассматривалась перегрузка методов, определение массивов, перечислений и структур в С# и их представления в библиотеках базовых классов .NET. Были описаны некоторые детали, касающиеся т ипов значения и ссылочных типов, в том числе то, как они ведут себя при передаче в качестве параметров методам, и то, каким образом взаимодействовать с нулевыми типами данных с помощью операций ? и ? ?. На этом первоначальное знакомство с языком программирования С# завершено. В следующей главе начинается изучение деталей объектно-ориентированной разработки.
ГЛАВА 5 Определение инкапсулированных типов классов В предыдущих двух главах мы исследовали ряд основных синтаксических конструкций, присущих любому приложению .NET, которое вам придется разрабатывать. Здесь мы приступим к изучению объектно-ориентированных возможностей С#. Первое, что предстоит узнать — это процесс построения четко определенных типов классов, которые поддерживают любое количество конструкторов. После описания основ определения классов и размещения объектов в остальной части главы рассматривается тема инкапсуляции. По ходу изложения вы узнаете, как определяются свойства класса, а также какова роль статических полей, синтаксиса инициализации объектов, полей, доступных только для чтения, константных данных и частичных классов. Знакомство с типом класса С# Что касается платформы .NET, то наиболее фундаментальной программной конструкцией является тип класса. Формально класс — это определяемый пользователем тип, который состоит из данных полей (часто именуемых переменными-членами) и членов, оперирующих этими данными (конструкторов, свойств, методов, событий и т.п.). Все вместе поля данных класса представляют "состояние" экземпляра класса (иначе называемого объектом). Мощь объектных языков, подобных С#, состоит в их способности группировать данные и связанную с ними функциональность в определении класса, что позволяет моделировать программное обеспечение на основе сущностей реального мира. Для начала создадим новое консольное приложение С# по имени SimpleClassExample. Затем добавим в проект новый файл класса (по имени Car.cs), используя пункт меню Projects Add Class (Проект^ Добавить класс), выберем пиктограмму Class (Класс) в результирующем диалоговом окне, как показано на рис. 5.1, и щелкнем на кнопке Add (Добавить). Класс определятся в С# с помощью ключевого слова class. Вот как выглядит простейшее из возможных объявление класса: class Car { }
186 Часть II. Главные конструкции программирования на С# Add New Item - SimpleClasiExample ( & ЦИМИЦ 1 Visual Studic Installed Templates J л Visual C# Items Data General Web Windows Forms WPF Reporting 1 Online Templates 1 ■ Sort by j Default ... *\ :d (ill [ Search Installed Temptates... Г]| СП Code File Visual C# hems j C,Mi 1 ' 1 Type Visual C# Items С*Ц Class N Visual C# Items | An empty class definition '4 1 Class I «W* ADO.NET EntotyOTjerrt3ene...V>sual C# Items 3cw Interface v,suar c# Items ! I <rj 1 1 Name Car.cs 1 Add ]| Cancel | 1 Рис. 5.1. Добавление нового типа класса С# После определения типа класса нужно определить набор переменных-членов, которые будут использоваться для представления его состояния. Например, вы можете решить, что объекты Саг (автомобили) должны иметь поле данных типа int, представляющее текущую скорость, и поле данных типа string для представления дружественного названия автомобиля. С учетом этих начальных положений дизайна класс Саг будет выглядеть следующим образом: class Car { // 'Состояние' объекта Саг. public string petName; public int currSpeed; } Обратите внимание, что эти переменные-члены объявлены с использованием модификатора доступа public. Общедоступные (public) члены класса доступны непосредственно, как только создается объект данного типа. Как вам, возможно, уже известно, термин "объект" служит для представления экземпляра данного типа класса, созданного с помощью ключевого слова new. На заметку! Поля данных класса редко (если вообще когда-нибудь) должны определяться с модификатором public. Чтобы обеспечить целостность данных состояния, намного лучше объявлять данные приватными (private) или, возможно, защищенными (protected) и открывать контролируемый доступ к данным через свойства типа (как будет показано далее в этой главе). Однако чтобы сделать первый пример насколько возможно простым, оставим данные общедоступными. После определения набора переменных-членов, представляющих состояние класса, следующим шагом в проектировании будет создание членов, которые моделируют его поведение. Для данного примера класс Саг определяет один метод по имени SpeedUp () и еще один — по имени PrintState(). Модифицируйте код класса следующим образом: class Car { // 'Состояние' объекта Саг. public string petName;
Глава 5. Определение инкапсулированных типов классов 187 public int currSpeed; // Функциональность Car. public void PrintStateO { Console.WriteLine ("{0 } is going {1} MPH.11, petName, currSpeed); } public void SpeedUp(int delta) { currSpeed += delta; } } Метод PrintState () — это более или менее диагностическая функция, которая просто выводит текущее состояние объекта Саг в окно командной строки. Метод SpeedUp () повышает скорость Саг, увеличивая ее на величину, переданную во входящем параметре типа int. Теперь обновите код метода Main(), как показано ниже: static void Main(string[] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); // Разместить в памяти и сконфигурировать объект Саг. Car myCar = new Car(); myCar.petName = "Henry"; myCar.currSpeed = 10; // Повысить скорость автомобиля в несколько раз //и вывести новое состояние. for (int i = 0; i <= 10; i++) { myCar.SpeedUp E); myCar.PrintState (); } Console.ReadLine(); } Запустив программу, вы увидите, что переменная Car (myCar) поддерживает свое текущее состояние на протяжении жизни всего приложения, как показано в следующем выводе: ***** Fun W1th Class Types ***** Henry is going 15 MPH. Henry is going 20 MPH. Henry is going 25 MPH. Henry is going 30 MPH. Henry is going 35 MPH. Henry is going 40 MPH. Henry is going 45 MPH. Henry is going 50 MPH. Henry is going 55 MPH. Henry is going 60 MPH. Henry is going 65 MPH. Размещение объектов с помощью ключевого слова new Как было показано в предыдущем примере кода, объекты должны быть размещены в памяти с использованием ключевого слова new. Если ключевое слово new не указать и попытаться воспользоваться переменной класса в следующем операторе кода, будет получена ошибка компиляции. Например, следующий метод Main() компилироваться не будет:
188 Часть II. Главные конструкции программирования на С# static void Main(string[] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); // Ошибка1 Забыли использовать new для создания объекта! Car myCar; myCar.petName = "Fred"; } Чтобы корректно создать объект с использованием ключевого слова new, можно определить и разместить в памяти объект Саг в одной строке кода: static void Main(string[] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); Car myCar = new Car(); myCar.petName = "Fred"; } В качестве альтернативы, определение и размещение в памяти экземпляра класса может осуществляться в разных строках кода: static void Main(string[] args) { Console.WriteLine (••***** Fun with Class Types *****\n"); Car myCar; myCar = new Car() ; myCar.petName = "Fred"; } Здесь первый оператор кода просто объявляет ссылку на еще не созданный объект типа Саг. Только после явного присваивания ссылка будет указывать на действительный объект в памяти. В любом случае, к этому моменту мы получили простейший тип класса, который определяет несколько элементов данных и некоторые базовые методы. Чтобы расширить функциональность текущего класса Саг, необходимо разобраться с ролью конструкторов. Понятие конструктора Учитывая, что объект имеет состояние (представленное значениями его переменных-членов), программист обычно желает присвоить осмысленные значения полям объекта перед тем, как работать с ним. В настоящий момент тип Саг требует присваивания значений полям perName и currSpeed. Для текущего примера это не слишком проблематично, поскольку общедоступных элементов данных всего два. Однако нередко классы состоят из нескольких десятков полей. Ясно, что было бы нежелательно писать 20 операторов инициализации для всех 20 элементов данных такого класса. К счастью, в С# поддерживается механизм конструкторов, которые позволяют устанавливать состояние объекта в момент его создания. Конструктор (constructor) — это специальный метод класса, который вызывается неявно при создании объекта с использованием ключевого слова new. Однако в отличие от "нормального" метода, конструктор никогда не имеет возвращаемого значения (даже void) и всегда именуется идентично имени класса, который он конструирует. Роль конструктора по умолчанию Каждый класс С# снабжается конструктором по умолчанию, который при необходимости может быть переопределен. По определению такой конструктор никогда не при-
Глава 5. Определение инкапсулированных типов классов 189 нимает аргументов. После размещения нового объекта в памяти конструктор по умолчанию гарантирует установку всех полей в соответствующие стандартные значения (значениях по умолчанию для типов данных С# описаны в главе 3). Если вы не удовлетворены такими присваиваниями по умолчанию, можете переопределить конструктор по умолчанию в соответствии со своими нуждами. Для иллюстрации модифицируем класс С#, как показано ниже: class Car { // 'Состояние' объекта Саг. public string petName; public int currSpeed; // Специальный конструктор по умолчанию. public Car () { petName = "Chuck"; currSpeed = 10; } } В данном случае мы заставляем объекты Саг начинать свою жизнь под именем Chuck и скоростью 10 миль в час. При этом создавать объекты со значениями по умолчанию можно следующим образом: а static void Main(string[] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); // Вызов конструктора по умолчанию. Car chuck = new Car(); // Печатает "Chuck is going 10 MPH." chuck.PrintState(); } Определение специальных конструкторов Обычно помимо конструкторов по умолчанию в классах определяются дополнительные конструкторы. При этом пользователь объекта обеспечивается простым и согласованным способом инициализации состояния объекта непосредственно в момент его создания. Взгляните на следующее изменение класса Саг, который теперь поддерживает целых три конструктора: class Car { // 'Состояние' объекта Саг. public string petName; public int currSpeed; // Специальный конструктор по умолчанию. public Car() { petName = "Chuck"; currSpeed = 10; } // Здесь currSpeed получает значение //no умолчанию типа int (t)) . public Car(string pn) { petName = pn; }
190 Часть II. Главные конструкции программирования на С# // Позволяет вызывающему коду установить полное состояние Саг. public Car(string pn, int cs) { petName = pn; currSpeed = cs; } } Имейте в виду, что один конструктор отличается от другого (с точки зрения компилятора С#) количеством и типом аргументов. В главе 4 было показано, что определение методов с одним и тем же именем, но разным количеством и типами аргументов, называется перегрузкой. Таким образом, класс Саг имеет перегруженный конструктор, чтобы предоставить несколько способов создания объекта во время объявления. В любом случае, теперь можно создавать объекты Саг, используя любой из его общедоступных, конструкторов. Например: static void Main(string[] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); // Создать объект Car no имени Chuck со скоростью 10 миль в час. Car chuck = new Car(); chuck.PrintState(); // Создать объект Car no имени Mary со скоростью 0 миль в час. Car тагу = new Car ("Mary") ; тагу.PrintState(); // Создать объект Car no имени Daisy со скоростью 75 миль в час. Car daisy = new Car ("Daisy", 75); daisy.PrintState (); } Еще раз о конструкторе по умолчанию Как вы только что узнали, все классы "бесплатно" снабжаются конструктором по умолчанию. Таким образом, если добавить в текущий проект новый класс по имени Motorcycle, определенный следующим образом: class Motorcycle { public void PopAWheelyO { Console.WriteLine("Yeeeeeee Haaaaaeewww'"); } } то сразу можно будет создавать экземпляры Motorcycle с помощью конструктора по умолчанию: static void Main(string[] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); Motorcycle mc = new Motorcycle(); mc.PopAWheely(); } Однако, как только определен специальный конструктор, конструктор по умолчанию молча удаляется из класса и становится недоступным! Воспринимайте это так: если вы не определили специального конструктора, компилятор С# снабжает класс конструктором по умолчанию, чтобы позволить пользователю объекта размещать его в памяти с набором
Глава 5. Определение инкапсулированных типов классов 191 данных, имеющих значения по умолчанию. В случае же, когда определяется уникальный конструктор, компилятор предполагает, что вы решили взять власть в свои руки. Таким образом, чтобы позволить пользователю объекта создавать экземпляр типа посредством конструктора по умолчанию, а также специального конструктора, понадобится явно переопределить конструктор по умолчанию. И, наконец, в подавляющем большинстве случаев реализация конструктора класса по умолчанию намеренно остается пустой, поскольку все, что требуется — это создание объекта со значениями всех полей по умолчанию. Внесем в класс Motorcycle следующие изменения: class Motorcycle { public int driverlntensity; public void PopAWheelyO { for (int 1=0; l <= driverlntensity; i++) { Console.WriteLine("Yeeeeeee Haaaaaeewww! ") ; } } // Вернуть конструктор по умолчанию, который будет устанавливать // для всех членов данных значения по умолчанию. public Motorcycle() {} // Специальный конструктор. public Motorcycle (int intensity) { driverlntensity = intensity; } } Роль ключевого слова this В языке С# имеется ключевое слово this, которое обеспечивает доступ к текущему экземпляру класса. Одно из возможных применений ключевого слова this состоит в том, чтобы разрешать неоднозначность контекста, которая может возникнуть, когда входящий параметр назван так же, как поле данных данного типа. Разумеется, в идеале необходимо просто придерживаться соглашения об именовании, которое не может привести к такой неоднозначности; однако чтобы проиллюстрировать такое использование ключевого слова this, добавим в класс Motorcycle новое поле типа string (по имени name), представляющее имя водителя. После этого добавим метод по имени SetDriverNameO, реализованный следующим образом: class Motorcycle { public int driverlntensity; // Новые члены, представляющие имя водителя. public string name; public void SetDriverName(string name) { name = name; } } Хотя этот код нормально компилируется, Visual Studio 2010 отобразит предупреждающее сообщение о том, что переменная присваивается сама себе! Чтобы проиллюст-' рировать это, добавим в Main() вызов SetDriverNameO и выведем значение поля name. Обнаружится, что значением поля name осталась пустая строка! // Усадим на Motorcycle байкера по имени Tiny? Motorcycle с = new Motorcycle E); с.SetDriverName("Tiny"); c.PopAWheely(); Console.WriteLine("Rider name is {0}", c.name); // Выводит пустое значение name!
192 Часть II. Главные конструкции программирования на С# Проблема в том, что реализация SetDriverNameO выполняет присваивание входящему параметру его же значения, поскольку компилятор предполагает, что name здесь ссылается на переменную, существующую в контексте метода, а не на поле name в контексте класса. Для информирования компилятора, что необходимо установить значение поля данных текущего объекта, просто используйте this для разрешения этой неоднозначности: public void SetDnverName (string name) { this.name = name; } Имейте в виду, что если неоднозначности нет, то вы не обязаны использовать ключевое слово this, когда классу нужно обращаться к собственным данным или членам. Например, если переименовать член данных name типа string в driverName (что также потребует обновления метода Main()), то применение this станет не обязательным, поскольку исчезает неоднозначность контекста: class Motorcycle { public int driverlntensity; public string driverName; public void SetDriverName(string name) { // Эти два оператора функционально эквивалентны. driverName = name; this.driverName = name; } Помимо небольшого выигрыша от использования this в неоднозначных ситуациях, это ключевое слово может быть полезно при реализации членов, поскольку такие IDE-среды, как SharpDevelop и Visual Studio 2010, включают средство IntelliSense, когда вводится this. Это может здорово помочь, когда вы забыли название члена класса и хотите быстро вспомнить его определение. Взгляните на рис. 5.2. Motorcycle.cs* X иЩЩ »I ♦SetDriverName(itring name) ^StmpleOassExample.Motorcycle .using System.Text; ''namespace SimpleClassExample "' class Motorcycle { public int driverlntensity; public string driverName; public void SetDriverName(string nai { this.JdriverName - name; } <Ctrl*Art*Space> public H driverlntensity f ori ♦ driverName { ♦ Equals i <* GetHashCode > j ■• GetType )* MemberwiseClone rstrd:#E ; ♦ SetDnverName : ♦ ToString Intensity; i++) teee Haaeaaeewww Iм); void MotorcyclePopAWhedyQl Рис. 5.2. Активизация средства IntelliSense для this
Глава 5. Определение инкапсулированных типов классов • 193 На заметку! Применение ключевого слова this внутри реализации статического члена приводит к ошибке компиляции. Как будет показано, статические члены оперируют на уровне класса (а не объекта), а на этом уровне нет текущего объекта, потому и не существует this! Построение цепочки вызовов конструкторов с использованием this Другое применение ключевого слова this состоит в проектировании класса, использующего технику под названием сцепление конструкторов или цепочка конструкторов (constructor chaining). Этот шаблон проектирования полезен, когда имеется класс, определяющий несколько конструкторов. Учитывая тот факт, что конструкторы часто проверяют входящие аргументы на соблюдение различных бизнес-правил, возникает необходимость в избыточной логике проверки достоверности внутри множества конструкторов. Рассмотрим следующее измененное объявление класса Motorcycle: class Motorcycle { public int driverlntensity; public string driverName; public Motorcycle () { } // Избыточная логика конструктора! public Motorcycle (int intensity) { if (intensity > 10) { intensity = 10; } driverlntensity = intensity; } public Motorcycle(int intensity, string name) { if (intensity > 10) { intensity = 10; } driverlntensity = intensity; driverName = name; } } Здесь (возможно, стараясь обеспечить безопасность гонщика) в каждом конструкторе предпринимается проверка, что уровень мощности не превышает 10. Хотя все это правильно и хорошо, в двух конструкторах появляется избыточный код. Это далеко от идеала, поскольку придется менять код в нескольких местах в случае изменения правил (например, если предельное значение мощности будет установлено равным 5). Один из способов исправить создавшуюся ситуацию состоит в определении в классе Motorcycle метода, который выполнит проверку входных аргументов. Если поступить так, то каждый конструктор должен будет вызывать этот метод перед присваиванием значений полям. Хотя такой подход позволяет изолировать код, который придется обновлять при изменении бизнес-правил, теперь появилась другая избыточность: class Motorcycle { public int driverlntensity; public string driverName;
194 Часть II. Главные конструкции программирования на С# // Конструкторы. public Motorcycle() { } public Motorcycle (int intensity) { Setlntensity(intensity); } public Motorcycle(int intensity, string name) { Setlntensity(intensity); driverName = name; } public void Setlntensity(int intensity) { if (intensity > 10) { intensity = 10; } driverlntensity = intensity; Более ясный подход предусматривает назначение конструктора, который принимает максимальное количество аргументов, в качестве "ведущего конструктора", с реализацией внутри него необходимой логики проверки достоверности. Остальные конструкторы смогут использовать ключевое слово this, чтобы передать входные аргументы ведущему конструктору и при необходимости предоставить любые дополнительные параметры. В результате беспокоиться придется только о поддержке единственного конструктора для всего класса, в то время как остальные конструкторы остаются в основном пустыми. Ниже приведена финальная реализация класса Motorcycle (с дополнительным конструктором в целях иллюстрации). При связывании конструкторов в цепочку обратите внимание, что ключевое слово this располагается вне тела конструктора (и отделяется от его имени двоеточием): class Motorcycle { public int driverlntensity; public string driverName; // Связывание конструкторов в цепочку. public Motorcycle () {} public Motorcycle (int intensity) : this(intensity, "") { } public Motorcycle(string name) : this@, name) { } // Это 'ведущий конструктор' , выполняющий всю реальную работу. public Motorcycle(int intensity, string name) { if (intensity > 10) { intensity = 10; } driverlntensity = intensity; driverName = name;
Глава 5. Определение инкапсулированных типов классов 195 Имейте в виду, что применение ключевого слова this для связывания вызовов конструкторов в цепочку вовсе не обязательно. Однако использование такой техники позволяет получить лучше сопровождаемое и более краткое определение кода. С помощью этой техники также можно упростить решение программистских задач, поскольку реальная работа'делегируется единственному конструктору (обычно имеющему максимальное количество параметров), в то время как остальные просто передают ему ответственность. Обзор потока конструктора Напоследок отметим, что как только конструктор передал аргументы выделенному ведущему конструктору (и этот конструктор обработал данные), вызывающий конструктор продолжает выполнение всех остальных операторов. Чтобы прояснить мысль, модифицируем конструкторы класса Motorcycle, добавив вызов Console.WriteLine(): class Motorcycle { public int driverlntensity; public string driverName; // Связывание конструкторов в цепочку. public Motorcycle () Console.WriteLine ("In default ctor"); public Motorcycle (int intensity) : this(intensity, "") Console.WriteLine ("In ctor taking an int"); public Motorcycle(string name) : this@, name) Console.WriteLine("In ctor taking a string"); // Это 'ведущий конструктор' , выполняющий всю реальную работу. public Motorcycle(int intensity, string name) Console.WriteLine("In master ctor "); if (intensity > 10) { intensity = 10; } driverlntensity = intensity; driverName = name; } } Теперь изменим метод Main(), чтобы он обрабатывал объект Motorcycle, как показано ниже: static void Main(string[] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); // Создание Motorcycle. Motorcycle с = new Motorcycle E); с.SetDriverName("Tiny"); c.PopAWheely(); Console.WriteLine("Rider name is {0}", с.driverName); // вывод имени гонщика Console.ReadLine();
196 Часть II. Главные конструкции программирования на С# Вывод, полученный в результате выполнения предыдущего метода Main(), выглядит следующим образом: ***** Fun with Class Types ***** In master ctor In ctor taking an int Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Rider name is Tiny Поток логики конструкторов описан ниже. • Создается объект за счет вызова конструктора, который принимает один аргумент типа int. • Конструктор передает полученные данные ведущему конструктору, добавляя необходимые дополнительные начальные аргументы, не указанные вызывающим кодом. • Ведущий конструктор присваивает входные данные полям объекта. • Управление возвращается первоначально вызванному конструктору, который выполняет остальные операторы кода. В построении цепочки конструкторов замечательно то, что этот шаблон программирования работает с любой версией языка С# и платформой .NET. Однако если в случае если целевой платформой является .NET 4.0 и выше, можно еще более упростить задачу программирования за счет использования необязательных аргументов в качестве альтернативы традиционным цепочкам конструкторов. Еще раз об необязательных аргументах Вы главе 4 вы узнали об необязательных и именованных аргументах. Вспомните, что необязательные аргументы позволяют определять значения по умолчанию для входных аргументов. Если вызывающий код удовлетворяют эти значения по умолчанию, указывать уникальные значения не обязательно, но это можно делать, когда объект требуется снабдить специальными данными. Рассмотрим следующую версию класса Motorcycle, который теперь предоставляет несколько возможностей конструирования объектов, используя единственное определение конструктора. class Motorcycle { // Единственный конструктор, использующий необязательные аргументы. public Motorcycle(int intensity = 0, string name = "") { if (intensity > 10) { intensity = 10; } driverlntensity = intensity; driverName = name; } }
Глава 5. Определение инкапсулированных типов классов 197 С этим единственным конструктором можно создать объект Motorcycle, используя ноль, один или два аргумента. Вспомните, что синтаксис именованных аргументов позволяет, по сути, пропускать приемлемые установки по умолчанию (см. главу 4). static void MakeSomeBikes() { // driverName = "", driverlntensity = 0 Motorcycle ml = new Motorcycle(); Console.WriteLine("Name= {0}, Intensity= {1}", ml.driverName, ml.driverlntensity); // driverName = "Tiny" , driverlntensity = 0 Motorcycle m2 = new Motorcycle(name:"Tiny"); Console.WriteLine("Name= {0}, Intensity= {1}", m2.driverName, m2.driverlntensity); // driverName = "", driverlntensity = 7 Motorcycle m3 = new Motorcycle G) ; Console.WriteLine("Name= {0}, Intensity= {1}", m3.driverName, m3.driverlntensity); } Хотя применение необязательных/именованных аргументов — очень удобный путь упрощения определения набора конструкторов, используемых определенным классом, следует всегда помнить, что этот синтаксис привязывает компиляцию приложений к С# 2010, а их выполнение — к .NET 4.O. Если требуется построить классы, которые должны выполняться на платформе .NET любой версии, лучше придерживаться классической технологии цепочек конструкторов. В любом случае, теперь можно определить класс с данными полей (переменными- членами) и различными членами, которые могут быть созданы с помощью любого количества конструкторов. Теперь давайте формализуем роль ключевого слова static. Исходный код. Проект SimpleClassExample доступен в подкаталоге Chapter 5. Понятие ключевого слова static Класс С# может определять любое количество статических членов с использованием ключевого слова static. При этом соответствующий член должен вызываться непосредственно на уровне класса, а не на объектной ссылке. Чтобы проиллюстрировать разницу, обратимся к System.Console. Как уже было показано, метод WriteLine() не вызывается на уровне объекта: // Ошибка! WriteLine() не является методом уровня объекта1 Console с = new Console (); с.WriteLine("I can't be printed..."); Вместо этого статический член WriteLine () предваряется именем класса: // Правильно! WriteLine() - статический метод. Console.WriteLine("Thanks..."); Проще говоря, статические члены — это элементы, задуманные (проектировщиком класса) как общие, так что нет нужды создавать экземпляр типа при их вызове. Когда в любом классе определены только статические члены, такой класс можно считать "обслуживающим классом". Например, если воспользоваться браузером объектов Visual Studio 2010 (выбрав пункт меню View^Object Browser (Вид1^Браузер объектов)) для просмотра пространства имен System сборки mscorlib.dll, можно увидеть, что все члены классов Console, Math, Environment и GC представляют свою функциональность через статические члены.
198 Часть II. Главные конструкции программирования на С# Определение статических методов Предположим, что имеется новый проект консольного приложения по имени StaticMethods, и в нем — класс по имени Teenager, определяющий статический метод Complain(). Этот метод возвращает случайную строку, полученную вызовом статической вспомогательной функции по имени GetRandomNumber (): class Teenager { public static Random r = new Random(); public static int GetRandomNumber(short upperLimit) { return r.Next(upperLimit); } public static string Complain () { stnng[] messages = {"Do I have to?", "He started it!", "I 'm too tired...", "I hate school'", "You are sooooooo wrong!"}; return messages[GetRandomNumberE)]; } } Обратите внимание, что переменная-член System.Random и вспомогательная функция GetRandomNumber () также были объявлены статическими членами класса Teenager, учитывая правило, что статические члены, такие как метод Complain(), могут оперировать только другими статическими членами. На заметку! Здесь следует повторить: статические члены могут оперировать только статическими данными и вызывать статические методы определяющего их класса. Попытка использования нестатических данных класса или вызова нестатического метода класса внутри реализации статического члена приводит к ошибке времени компиляции. Подобно любому статическому члену, для вызова Complain () нужен префикс — имя определяющего его класса: static void Main(string [ ] args) { Console.WriteLine("***** Fun with Class Types *****\n"); for (int l =0; l < 5; i++) Console.WriteLine(Teenager.Complain()); Console.ReadLine(); } Исходный код. Проект StaticMethods доступен в подкаталоге Chapter 5. Определение статических полей данных В дополнение к статическим методам, в классе (или структуре) также могут быть определены статические поля, такие как переменная-член Random, представленная в предыдущем классе Teenager. Знайте, что когда класс определяет нестатические данные (правильно называемые данными экземпляра), то каждый объект этого типа поддерживает независимую копию поля. Например, представим класс, который моделирует депозитный счет, определенный в новом проекте консольного приложения по имени StaticData:
Глава 5. Определение инкапсулированных типов классов 199 // Простой класс депозитного счета. class SavingsAccount { public double currBalance; public SavingsAccount(double balance) { currBalance = balance; } } При создании объектов SavingsAccount память под поле currBalance выделяется для каждого объекта. Статические данные, с другой стороны, распределяются однажды и разделяются всеми объектами того же класса. Чтобы проиллюстрировать удобство статических данных, добавьте в класс SavingsAccount статический элемент данных по имени currlnterestRate, принимающий значение по умолчанию 0.04: // Простой класс депозитного счета. class SavingsAccount { public double currBalance; // Статический элемент данных. public static double currlnterestRate = 0.04; public SavingsAccount(double balance) { currBalance = balance; Если создать три экземпляра SavingsAccount, как показано ниже: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Static Data *****\n"); SavingsAccount si = new SavingsAccount E0); SavingsAccount s2 = new SavingsAccountA00); SavingsAccount s3 = new SavingsAccountA0000.75); Console.ReadLine(); } то размещение данных в памяти будет выглядеть примерно так, как показано на рис. 5.3. SavingsAccount:SI currBalance=50 SavingsAccount:S2 currBalance=100 SavingsAccount:S3 currBalance=10000.75 / curr!nterestRate=.04 / / Рис. 5.З. Статические данные размещаются один раз и разделяются между всеми экземплярами класса Теперь давайте изменим класс SavingsAccount, добавив к нему два статических метода для получения и установки значения процентной ставки:
200 Часть II. Главные конструкции программирования на С# // Простой класс депозитного счета. class SavingsAccount { public double currBalance; // Статический элемент данных. public static double currlnterestRate = 0.04; public SavingsAccount(double balance) { currBalance = balance; } // Статические члены для установки/получения процентной ставки. public static void SetlnterestRate(double newRate ) { currlnterestRate = newRate; } public static double GetlnterestRate () { return currlnterestRate; } } Рассмотрим следующее применение класса: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Static Data *****\n"); SavingsAccount si = new SavingsAccount E0); SavingsAccount s2 = new SavingsAccount A00); // Вывести текущую процентную ставку. Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetlnterestRate()); // Создать новый объект; это не 'сбросит' процентную ставку. SavingsAccount s3 = new SavingsAccountA0000.75) ; Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetlnterestRate()) ; Console.ReadLine(); } Вывод предыдущего метода Main() показан ниже: ••••• Fun W1th Static Data ***** In static ctor1 Interest Rate is: 0.04 Interest Rate is: 0.04 Как видите, при создании новых экземпляров класса SavingsAccount значение статических данных не сбрасывается, поскольку CLR выделяет для них место в памяти только один раз. После этого все объекты типа SavingsAccount оперируют одним и тем же значением. При проектировании любого класса С# одна из задач связана с выяснением того, какие части данных должны быть определены как статические члены, а какие — нет. Хотя на этот счет не существует строгих правил, помните, что поле статических данных разделяется между всеми объектами данного класса. Поэтому, если необходимо, чтобы часть данных совместно использовалась всеми объектами, статические члены будут самым подходящим вариантом. Предположим, что переменная currlnterestRate не определена с ключевым словом static. Это означает, что каждый объект SavingAccount имеет собственную копию currlnterestRate. Пусть создано 100 объектов SavingAccount, и требуется изменить значение процентной ставки. Это потребует стократного вызова методв SetlnterestRate ()! Ясно, что такой способ моделирования общих для объектов класса данных нельзя считать удобным. Еще раз: статические данные идеальны, когда имеется значение, которое должно быть общим для всех объектов данной категории.
Глава 5. Определение инкапсулированных типов классов 201 Определение статических конструкторов Вспомните, что конструкторы служат для установки значений поля данных объекта во время его создания. Таким образом, если вы хотите присвоить значение статическому члену внутри конструктора уровня экземпляра, то удивитесь, обнаружив, что это значение будет сбрасываться каждый раз, когда создается новый объект! Например, предположим, что класс SavingsAccount изменен следующим образом: class SavingsAccount { public double currBalance; public static double currlnterestRate; public SavingsAccount(double balance) { currlnterestRate = 0.04; currBalance = balance; } }" При выполнении предыдущего метода Main() обнаруживается, что переменная currlnterestRate будет сбрасываться при каждом создании нового объекта SavingsAccount, всегда возвращаясь к значению 0.04. Ясно, что установка значений статических данных в нормальном конструкторе экземпляра сводит на нет весь их смысл. Всякий раз, когда создается новый объект, данные уровня класса сбрасываются! Один из способов правильной установки статического поля состоит в использовании синтаксиса инициализации члена, как это делалось изначально: class SavingsAccount { public double currBalance; // Статические данные. public static double currlnterestRate = 0.04; } Этот подход гарантирует, что статическое поле будет установлено только однажды, независимо от того, сколько объектов будет создано. Однако что, если значение статических данных нужно получить во время выполнения? Например, в типичном банковском приложении значение переменной — процентной ставки должно быть прочитано из базы данных или внешнего файла. Решение подобных задач требует контекста метода (такого как конструктор), чтобы можно было выполнить операторы кода. Именно по этой причине в С# предусмотрена возможность определения статического конструктора. Взгляните на следующее изменение в коде: class SavingsAccount { public double currBalance; public static double currlnterestRate; public SavingsAccount(double balance) { currBalance = balance; } // Статический конструктор. static SavingsAccount () { Console.WriteLine("In static ctor1"); currlnterestRate = 0.04; }
202 Часть II. Главные конструкции программирования на С# Упрощенно, статический конструктор — это специальный конструктор, который является идеальным местом для инициализации значений статических данных, когда их значение не известно на момент компиляции (например, когда его нужно прочитать из внешнего файла или сгенерировать случайное число). Ниже приведено несколько интересных моментов, касающихся статических конструкторов. • В отдельном классе может быть определен только один статический конструктор. Другими словами, статический конструктор нельзя перегружать. • Статический конструктор не имеет модификатора доступа и не может принимать параметров. • Статический конструктор выполняется только один раз, независимо от того, сколько объектов отдельного класса создается. • Исполняющая система вызывает статический конструктор, когда создает экземпляр класса или перед первым обращением к статическому члену этого класса. • Статический конструктор выполняется перед любым конструктором уровня экземпляра. Учитывая сказанное, при создании новых объектов Savings Ac count значения статических данных сохраняются, поскольку статический член устанавливается только один раз внутри статического конструктора, независимо от количества созданных объектов. Определение статических классов Ключевое слово static возможно также применять прямо на уровне класса. Когда класс определен как статический, его нельзя создать с использованием ключевого слова new, и он может включать в себя только статические члены или поля. Если это правило нарушить, возникнет ошибка компиляции. На заметку! Классы или структуры, предоставляющие только статическую функциональность, часто называют служебными (utility). При проектировании такого класса рекомендуется применять ключевое слово static к определению класса. На первый взгляд это может показаться довольно бесполезным средством, учитывая невозможность создания экземпляров класса. Однако следует учесть, что класс, не содержащий ничего кроме статических членов и/или константных данных, прежде всего, не нуждается в выделении памяти. Рассмотрим следующий новый статический тип: // Статические классы могут содержать только статические члены! static class TimeUtilClass { public static void PrintTime () { Console.WriteLine(DateTime.Now.ToShortTimeString()); } public static void PrintDateO { Console.WriteLine (DateTime.Today.ToShortDateString()); } } Учитывая, что этот класс определен с ключевым словом static, создавать экземпляры TimeUtilClass с помощью ключевого слова new нельзя. Напротив, вся функциональность доступна на уровне класса: static void Main(string[] args) { Console.WriteLine ("***** Fun with Static Data *****\n"); // Это работает нормально. TimeUtilClass.PrintDate(); TimeUtilClass.PrintTime();
Глава 5. Определение инкапсулированных типов классов 203 // Ошибка компиляции! Создавать экземпляр статического класса нельзя! TimeUtilClass u = new TimeUtilClass () ; } До появлении статических классов в .NET 2.0 единственный способ предотвратить создание экземпляров класса, предлагающего только статическую функциональность, состоял либо в переопределении конструктора по умолчанию с модификатором private, либо в объявлении класса как абстрактного с использованием ключевого слова abstract (подробности об абстрактных типах ищите в главе 6). Рассмотрим следующие подходы: class TimeUtilClass2 { // Переопределение конструктора по умолчанию как // приватного для предотвращения создания экземпляров. private TimeUtilClass2 (){} public static void PrintTime() { Console.WriteLine(DateTime.Now.ToShortTimeString ()); } public static void PrintDateO { Console.WriteLine(DateTime.Today.ToShortDateString ()); } } // Определение типа как абстрактного для предотвращения создания экземпляров. abstract class TimeUtilClass3 { public static void PrintTime() { Console.WriteLine(DateTime.Now.ToShortTimeString()); } public static void PrintDateO { Console.WriteLine(DateTime.Today.ToShortDateString()); } } Хотя эти конструкции допустимы и сейчас, использование статических классов представляет собой более ясное и более безопасное в отношении типов решение, учитывая тот факт, что предыдущие два приема допускают объявление нестатических членов внутри класса без ошибок. В результате возникнет большая проблема! При наличии класса, который больше не допускает создания экземпляров, в распоряжении окажется фрагмент функциональности (т.е. все нестатические члены), который не может быть использован. К этому моменту должно быть ясно, как определять простые классы, включающие конструкторы, поля и различные статические (и нестатические) члены. Обладая такими базовыми знаниями, можем приступать к ознакомлению с тремя основными принципами объектно-ориентированного программирования. Исходный код. Проект StaticData доступен в подкаталоге Chapter 5. Основы объектно-ориентированного программирования Все основанные на объектах языки (С#, Java, C++, Smalltalk, Visual Basic и т.п.) должны отвечать трем основным принципам объектно-ориентированного программирования (ООП), которые перечислены ниже. • Инкапсуляция. Как данный язык скрывает детали внутренней реализации объектов и предохраняет целостность данных? • Наследование. Как данный язык стимулирует многократное использование кода? • Полиморфизм. Как данный язык позволяет трактовать связанные объекты сходным образом?
204 Часть II. Главные конструкции программирования на С# Прежде чем погрузиться в синтаксические детали реализации каждого принципа, важно понять базовую роль каждого из них. Ниже предлагается обзор каждого принципа, а в остальной части этой и последующих главах рассматриваются детали. Роль инкапсуляции Первый основной принцип ООП называется инкапсуляцией. Этот принцип касается способности языка скрывать излишние детали реализации от пользователя объекта. Например, предположим, что используется класс по имени DatabaseReader, который имеет два главных метода: Open() HClose(). // Этот класс инкапсулирует детали открытия и закрытия базы данных. DatabaseReader dbReader = new DatabaseReader (); dbReader.Open(@"C:\AutoLot.mdf"); // Сделать что-то с файлом данных и закрыть файл. dbReader.Close() ; Фиктивный класс DatabaseReader инкапсулирует внутренние детали нахождения, загрузки, манипуляций и закрытия файла данных. Программистам нравится инкапсуляция, поскольку этот принцип ООП упрощает кодирование. Нет необходимости беспокоиться о многочисленных строках кода, которые работают "за кулисами", чтобы реализовать функционирование класса DatabaseReader. Все, что потребуется — это создать экземпляр и отправлять ему соответствующие сообщения (например, "открыть файл по имени AutoLot.mdf, расположенный на диске С:"). С идеей инкапсуляции программной логики тесно связана идея защиты данных. В идеале данные состояния объекта должны быть специфицированы с использованием ключевого слова private (или, возможно, protected). Таким образом, внешний мир должен вежливо попросить, если захочет изменить или получить лежащее в основе значение. Это хороший принцип, поскольку общедоступные элементы данных можно легко повредить (даже нечаянно, а не преднамеренно). Чуть позже будет дано формальное определение этого аспекта инкапсуляции. Роль наследования Следующий принцип ООП — наследование — касается способности языка позволять строить новые определения классов на основе определений существующих классов. По сути, наследование позволяет расширять поведение базового (или родительского) класса, наследуя основную функциональность в производном подклассе (также именуемом дочерним классом). На рис. 5.4 показан простой пример. Прочесть диаграмму на рис. 5.4 можно так: "шестиугольник является фигурой, которая является объектом". При наличии классов, связанных этой формой наследования, между типами устанавливается отношение "является" ("is-a"). Такое отношение называют классическим наследованием. Здесь можно предположить, что Shape определяет некоторое количество членов, общих для всех наследников (скажем, значение для представления цвета фигуры и другие значения, задающие высоту и ширину). Учитывая, что класс Hexagon расширяет Shape, он наследует основную функциональность, определенную классами Shape и Object, a также определяет дополнительные собственные детали, касающиеся шестиугольников (какими бы они ни были). На заметку! На платформе .NET класс System.Object всегда находится на вершине любой иерархии классов и определяет базовую функциональность, которая подробно описана в главе 6.
Глава 5. Определение инкапсулированных типов классов 205 Object Class т) Рис. 5.4. Отношение "является" ("is-a") Есть и другая форма повторного использования кода в мире ООП: модель включения/делегации, также известная под названием отношение "имеет" ("has-a") или агрегация. Эта форма повторного использования не применяется для установки отношений "родительский-дочерний". Вместо этого такое отношение позволяет одному классу определять переменную-член другого класса и опосредованно представлять его функциональность (при необходимости) пользователю объекта. Например, предположим, что снова моделируется автомобиль. Может понадобиться выразить идею, что автомобиль "имеет" радиоприемник. Было бы нелогично пытаться наследовать класс Саг от Radio или наоборот (ведь Саг не "является" Radio). Взамен имеются два независимых класса, работающих совместно, причем класс Саг создает и представляет функциональность Radio: class Radio { public void Power(bool turnOn) { Console.WriteLine("Radio on: {0} turnOn)/ } class Car { // Car 'имеет' Radio. private Radio myRadio = new Radio(); public void TurnOnRadio(bool onOff) { // Делегированный вызов внутреннего объекта. myRadio.Power(onOff); } Обратите внимание, что пользователь объекта не имеет понятия, что класс Саг использует внутренний объект Radio. static void Main(string [ ] args) { // Вызов передается Radio внутренне. Car viper = new Car () ; viper.TurnOnRadio(false);
206 Часть II. Главные конструкции программирования на С# Роль полиморфизма Последний принцип ООП — полиморфизм. Он обозначает способность языка трактовать связанные объекты в сходной манере. В частности, этот принцип ООП позволяет базовому классу определять набор членов (формально называемый полиморфным интерфейсом), которые доступны всем наследникам. Полиморфный интерфейс класса конструируется с использованием любого количества виртуальных или абстрактных членов (подробности ищите в главе 6). По сути, виртуальный член — это член базового класса, определяющий реализацию по умолчанию, которая может быть изменена (или, говоря более формально, переопределена) в производном классе. В отличие от него, абстрактный метод — это член базового класса, который не предусматривает реализации по умолчанию, а предлагает только сигнатуру. Когда класс наследуется от базового класса, определяющего абстрактный метод, этот метод обязательно должен быть переопределен в производном классе. В любом случае, когда производные классы переопределяют члены, определенные в базовом классе, они по существу переопределяют свою реакцию на один и тот же запрос. Чтобы увидеть полиморфизм в действии, давайте представим некоторые детали иерархии фигур, показанной на рис. 5.4. Предположим, что в классе Shape определен виртуальный метод Draw(), не принимающий параметров. Учитывая тот факт, что каждая фигура должна визуализировать себя уникальным образом, подклассы (такие как Hexagon и Circle) вольны переопределить этот метод по своему усмотрению (рис. 5.5). Object Г*. Class Вызов Draw () на объекте Circle приводит к рисованию двумерного круга. Вызов Draw () на объекте Hexagon приводит к рисованию двумерного шестиугольника. Рис. 5.5. Классический полиморфизм Когда полиморфный интерфейс спроектирован, можно сделать ряд предположений в коде. Например, учитывая, что классы Hexagon и Circle унаследованы от общего родителя (Shape), массив типов Shape может содержать всех наследников базового класса. Более того, учитывая, что Shape определяет полиморфный интерфейс для всех производных типов (в данном примере — метод Draw()), можно предположить, что каждый член массива обладает этой функциональностью. Рассмотрим следующий метод Main(), который заставляет массив типов-наследников Shape визуализировать себя с использованием метода Draw(): class Program { static void Main(string[] args) { Shape[] myShapes = new Shape[3]; myShapes[0] = new Hexagon (); myShapes[1] = new Circle ()/ myShapes[2] = new Hexagon (); Shape Class a Methods ♦ Draw ' 'Щ i Circle Class ■J- «•Shape 3 -—| Hexagon I Ciass
Глава 5. Определение инкапсулированных типов классов 207 foreach (Shape s in myShapes) { // Используется полиморфный интерфейс! s.Draw (); } Console.ReadLine (); } На этом краткое знакомство с основными принципами ООП завершено. Оставшаяся часть главы посвящена дальнейшим подробностям инкапсуляции в С#. Детали наследования и полиморфизма рассматриваются в главе 6. Модификаторы доступа С# При работе с инкапсуляцией всегда следует принимать во внимание то, какие аспекты типа видимы различным частям приложения. В частности, типы (классы, интерфейсы, структуры, перечисления и делегаты), а также их члены (свойства, методы, конструкторы и поля) определяются с использованием определенного ключевого слова, управляющего "видимостью" элемента другим частям приложения. Хотя в С# определены многочисленные ключевые слова для управления доступом, их значение может отличаться в зависимости от места применения (к типу или члену). В табл. 5.1 описаны роли и применение модификаторов доступа. Таблица 5.1. Модификаторы доступа С# ф р К чему может быть применен доступа Назначение public Типы или члены типов private Члены типов или вложенные типы protected Члены типов или вложенные типы internal Типы или члены типов protected Члены типов или вложенные типы internal Общедоступные (public) элементы не имеют ограничений доступа. Общедоступный член может быть доступен как из объекта, так и из любого производного класса. Общедоступный тип может быть доступен из других внешних сборок Приватные (private) элементы могут быть доступны только в классе (или структуре), в котором они определены Защищенные (protected) элементы могут использоваться классом, который определил их, и любым дочерним классом. Однако защищенные элементы не доступны внешнему миру через операцию точки (.) Внутренние (internal) элементы доступны только в пределах текущей сборки. Таким образом, если в библиотеке классов .NET определен набор внутренних типов, то другие сборки не смогут ими пользоваться Когда ключевые слова protected и internal комбинируются в объявлении элемента, такой элемент доступен внутри определяющей его сборки, определяющего класса и всех его наследников
208 Часть II. Главные конструкции программирования на С# В этой главе рассматриваются только ключевые слова public и private. В последующих главах будет рассказываться о роли модификаторов internal и protected internal (удобных при построении библиотек кода .NET) и модификатора protected (удобного при создании иерархий классов). Модификаторы доступа по умолчанию По умолчанию члены типов являются неявно приватными (private) и неявно внутренними (internal). Таким образом, следующее определение класса автоматически установлено как internal, в то время как конструктор по умолчанию этого типа автоматически является private: // Внутренний класс с приватным конструктором по умолчанию. class Radio { Radio () { } } Чтобы позволить другим частям программы обращаться к членам объекта, эти члены потребуется пометить как общедоступные (public). К тому же, если необходимо открыть Radio внешним сборкам (опять-таки, это удобно при построении библиотек кода .NET; см. главу 14), следует добавить к нему модификатор public. // Общедоступный класс с приватным конструктором по умолчанию. public class Radio { Radio () { } } Модификаторы доступа и вложенные типы Как было показано в табл. 5.1, модификаторы доступа private, protected и protected internal могут применяться к вложенному типу. Вложение типов детально рассматривается в главе 6. Пока же достаточно знать, что вложенный (nested) тип — это тип, объявленный непосредственно внутри объявления класса или структуры. Для примера ниже приведено приватное перечисление (по имени Color), вложенное в общедоступный класс (по имени SportsCarepublic class SportsCar { // Нормально! Вложенные типы могут быть помечены как private, private enum CarColor { Red, Green, Blue } } Здесь допускается применять модификатор доступа private к вложенному типу. Однако не вложенные типы (вроде SportsCar) могут определяться только с модификаторами public или internal. Поэтому следующее определение класса неверно: // Ошибка1 Не вложенный тип не может быть помечен как private1 private class SportsCar {} После ознакомления с модификаторами доступа можно приступать к формальным исследованиям первого принципа ООП.
Глава 5. Определение инкапсулированных типов классов 209 Первый принцип: службы инкапсуляции С# Концепция инкапсуляции вращается вокруг принципа, гласящего, что внутренние данные объекта.не должны быть напрямую доступны через экземпляр объекта. Вместо этого, если вызывающий код желает изменить состояние объекта, то должен делать это через методы доступа (accessor, или метод get) и изменения (mutator, или метод set). В С# инкапсуляция обеспечивается на синтаксическом уровне с использованием ключевых слов public, private, internal и protected. Чтобы проиллюстрировать необходимость в службах инкапсуляции, предположим, что создано следующее определение класса: // Класс с единственным общедоступным полем. class Book { public int numberOfPages; } Проблема с общедоступными данными состоит в том, что сами по себе эти данные не имеют возможности "понять", является ли присваиваемое значение допустимым в рамках существующих бизнес-правил системы. Как известно, верхний предел значений для типа int в С# довольно велик B 147 483 647), поэтому компилятор разрешит следующее присваивание: //Хм Ничего себе — мини-новелла! static void Main(string[] args) { Book miniNovel = new Book(); miniNovel.numberOfPages = 30000000; } Хотя границы типа данных int не превышены, ясно, что мини-новелла на 30 миллионов страниц выглядит несколько неправдоподобно. Как видите, общедоступные поля не дают возможности перехватывать ошибки, связанные с преодолением верхних (или нижних) логических границ. Если в текущей системе установлено бизнес-правило, гласящее, что книга должна иметь от 1 до 1000 страниц, его придется обеспечить программно. По этой причине общедоступным полям обычно нет места в определении класса производственного уровня. На заметку! Говоря точнее, члены класса, представляющие состояние объекта, не должны помечаться как public. В то же время, как будет показано далее в главе, вполне допускается иметь общедоступные константы и поля только для чтения. Инкапсуляция предоставляет способ предохранения целостности данных о состоянии объекта. Вместо определения общедоступных полей (которые легко приводят к повреждению данных), необходимо выработать привычку определения приватных данных, управление которыми осуществляется опосредованно, с применением одной из двух техник: • определение пары методов доступа и изменения; • определение свойства .NET. Какая бы техника не была выбрана, идея состоит в том, что хорошо инкапсулированный класс должен защищать свои данные и скрывать подробности своего устройства от любопытных глаз из внешнего мира. Это часто называют программированием черного ящика. Преимущество такого подхода состоит в том, что объект может свободно изменять внутреннюю реализацию любого метода. За счет обеспечения неизменности сигнатуры метода, работа существующего кода, который использует этот метод, не нарушается.
210 Часть II. Главные конструкции программирования на С# Инкапсуляция с использованием традиционных методов доступа и изменения В оставшейся части этой главы будет построен довольно полный класс, моделирующий обычного сотрудника. Для начала создадим новое консольное приложение по имени EmployeeApp и добавим в него новый файл класса (под названием Employee.cs), используя пункт меню Projects Add class (Проекте Добавить класс). Дополним класс Employee следующими полями, методами и конструкторами: class Employee { // Поля данных. private string empName; private int empID; private float currPay; // Конструкторы. public Employee () { }. public Employee(string name, int id, float pay) { empName = name; empID = id; currPay = pay; } // Методы. public void GiveBonus(float amount) { currPay += amount; } public void DisplayStats () { Console.WriteLine("Name: {0}", empName); Console.WriteLine ("ID: {0}", empID); Console.WriteLine("Pay: {0}", currPay); ( } } Обратите внимание, что поля класса Employee определены с ключевым словом private. С учетом этого, поля empName, empID и currPay напрямую через объектную переменную не доступны: static void Main(string[] args) { // Ошибка! Невозможно напрямую обращаться к приватным полям объекта! Employee emp = new Employee (); emp.empName = "Marv"; } Если необходимо, чтобы внешний мир мог взаимодействовать с полным именем сотрудника, по традиции понадобится определить методы доступа (метод get) и изменения (метод set). Роль метода get состоит в возврате вызывающему коду значения лежащих в основе статических данных. Метод set позволяет вызывающему коду изменять текущее значение лежащих в основе статических данных при условии соблюдения бизнес-правил. Для целей иллюстрации инкапсулируем поле empName. Для этого к существующему классу Employee следует добавить показанные ниже общедоступные члены. Обратите внимание, что метод SetNameO выполняет проверку входящих данных, чтобы удостовериться, что строка имеет длину не более 15 символов. Если это не так, на консоль выводится сообщение об ошибке и происходит возврат без изменения значения поля empName.
Глава 5. Определение инкапсулированных типов классов 211 На заметку! В классе производственного уровня в логике конструктора следовало бы предусмотреть проверку длины строки с именем сотрудника. Пока опустим эту деталь, но улучшим код позже, при рассмотрении синтаксиса свойств .NET. class Employee { // Поля данных. private string empName; // Метод доступа (метод get) . public string GetName() { return empName; } // Метод изменения (метод set) . public void SetName(string name) { // Перед присваиванием проверить входное значение, if (name.Length > 15) Console. WriteLine ("Error' Name must be less than 16 characters!11); else empName = name; } } Эта техника требует наличия двух уникально именованных методов для управления единственным элементом данных. Для иллюстрации модифицируем метод Main() следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Encapsulation *****\n"); Employee emp = new Employee("Marvin", 456, 30000); emp.GiveBonusA000); emp.DisplayStats(); // Использовать методы get/set для взаимодействия с именем объекта. emp.SetName("Marv"); Console.WriteLine("Employee is named: {0}", emp.GetName ()); Console.ReadLine(); } Благодаря коду в методе SetName (), попытка присвоить строку длиннее 15 символов приводит к выводу на консоль жестко закодированного сообщения об ошибке: static void Main(string[] args) { Console.WriteLine ("***** Fun with Encapsulation *****\n"); // Длиннее 15 символов! На консоль выводится сообщение об ошибке. Employee emp2 = new Employee (); emp2.SetName("Xena the warrior princess"); Console.ReadLine(); } Пока все хорошо. Приватное поле empName инкапсулировано с использованием двух методов GetName() и SetName(). Для дальнейшей инкапсуляции данных в классе Employee понадобится добавить ряд дополнительных методов (например, GetID(), SetID(), GetCurrentPayO, SetCurrentPayO). Каждый метод, изменяющий данные, может иметь в себе несколько строк кода для проверки дополнительных бизнес-правил.
212 Часть II. Главные конструкции программирования на С# Хотя можно поступить именно так, для инкапсуляции данных класса в С# предлагается удобная альтернативная нотация. Инкапсуляция с использованием свойств .NET Вдобавок к возможности инкапсуляции полей данных с использованием традиционной пары методов get/set, в языках .NET имеется более предпочтительный способ инкапсуляции данных с помощью свойств. Прежде всего, имейте в виду, что свойства — это всего лишь упрощенное представление "реальных" методов доступа и изменения. Это значит, что разработчик класса по-прежнему может реализовать любую внутреннюю логику, которую нужно выполнить перед присваиванием значения (например, преобразовать в верхний регистр, очистить от недопустимых символов, проверить границы числовых значений и т.д.). Ниже приведен измененный класс Employee, который теперь обеспечивает инкапсуляцию каждого поля с применением синтаксиса свойств вместо традиционных методов get/set. class Employee { // Поля данных. private string empName; private int empID; private float currPay; // Свойства. public string Name { get { return empName; } set { if (value.Length > 15) Console.WriteLine ("Error! Name must be less than 16 characters!11); else empName = value; } } // Можно было бы добавить дополнительные бизнес-правила для установки // этих свойств, но в данном примере в этом нет необходимости. public int ID { get { return empID; } set { empID = value; } } public float Pay { get { return currPay; } set { currPay = value; } } - } Свойство С# состоит из определений контекстов чтения get (метод доступа) и set (метод изменения), вложенных непосредственно в контекст самого свойства. Обратите внимание, что свойство указывает тип данных, которые оно инкапсулирует, как тип возвращаемого значения. Кроме того, в отличие от метода, в определении свойства не используются скобки (даже пустые). Обратите внимание на комментарий к текущему свойству ID:
Глава 5. Определение инкапсулированных типов классов 213 // int представляет тип инкапсулируемых свойством данных. // Тип данных должен быть идентичен связанному полю (empID). public int ID // Обратите внимание на отсутствие скобок. { get { return empID; } set { empID = value; } } В контексте set свойства используется лексема value, которая представляет входное значение, присваиваемое свойству вызывающим кодом. Эта лексема не является настоящим ключевым словом С#, а представляет собой то, что называется контекстуальным ключевым словом. Когда лексема value находится внутри контекстаcset, она всегда обозначает значение, присваиваемое вызывающим кодом, и всегда имеет тип, совпадающий с типом самого свойства. Поэтому свойство Name может проверить допустимую длину string следующим образом: public string Name { get { return empName; } set { // Здесь value имеет тип string, if (value.Length > 15) Console.WriteLine("Error! Name must be less than 16 characters1"); else empName = value; } } При наличии этих свойств вызывающему коду кажется, что он имеет дело с общедоступным элементом данных; однако "за кулисами" при каждом обращении вызывается корректный get или set, обеспечивая инкапсуляцию: static void Main(string [ ] args) { Console.WriteLine("***** Fun with Encapsulation *****\n"); Employee emp = new Employee("Marvin", 456, 30000); emp.GiveBonusA000); emp.DisplayStats(); // Установка и получение свойства Name. emp.Name = "Marv"; Console.WriteLine("Employee is named: {0}", emp.Name); Console.ReadLine(); } Свойства (в противоположность методам доступа и изменения) также облегчают манипулирование типами, поскольку способны реагировать на внутренние операции С#. Для иллюстрации представим, что тип класса Employee имеет внутреннюю приватную переменную-член, хранящую возраст сотрудника. Ниже показаны необходимые изменения (обратите внимание на использование цепочки вызовов конструкторов): class Employee { // Новое поле и свойство. private int empAge; public int Age { get { return empAge; } set { empAge = value; } }
214 Часть II. Главные конструкции программирования на С# // Обновленные конструкторы. public Employee () {} public Employee(string name, int id, float pay) :this(name, 0, id, pay){} public Employee(string name, int age, int id, float pay) { empName = name; empID = id; empAge = age; currPay = pay; } // Обновленный метод DisplayStats() теперь учитывает возраст. public void DisplayStats() { Console.WriteLine("Name: {0}", empName); Console.WriteLine ("ID: {0}", empID); Console.WriteLine ("Age : {0}", empAge); Console.WriteLine("Pay: {0}", currPay); } } Теперь предположим, что создан объект Employee по имени joe. Необходимо, чтобы в день рождения сотрудника возраст увеличивался на 1 год. Используя традиционные методы set /get, пришлось бы написать код вроде следующего: Employee joe = new Employee() ; joe.SetAge(joe.GetAge() + 1); Однако если empAge инкапсулируется через свойство по имени Age, можно записать проще: Employee joe = new Employee() ; joe.Age++; Использование свойств внутри определения класса Свойства, а в особенности их часть set — это общепринятое место для размещения бизнес-правил класса. В настоящее время класс Employee имеет свойство Name, которое гарантирует длину имени не более 15 символов. Остальные свойства (ID, Pay и Age) также могут быть обновлены с добавлением соответствующей логики. Хотя все это хорошо, но следует принимать во внимание также и то, что обычно происходит внутри конструктора класса. Конструктор получает входные параметры, проверяет корректность данных и затем выполняет присваивания внутренним приватным полям. В настоящее время ведущий конструктор не проверяет входные строковые данные на допустимый диапазон, поэтому можно было бы изменить его следующим образом: public Employee (string name, int age, int id, float pay) { // Это может оказаться проблемой... if (name.Length > 15) Console.WriteLine ("Error' Name must be less than 16 characters1"); else empName = name; empID = id; empAge = age; currPay = pay; }
Глава 5. Определение инкапсулированных типов классов 215 Наверняка вы заметили проблему, связанную с этим подходом. Свойство Name и ведущий конструктор предпринимают одну и ту же проверку ошибок! В результате получается дублирование кода. Чтобы упростить код и разместить всю проверку ошибок в центральном месте, для установки и получения данных внутри класса разумно всегда использовать свойства. Ниже показан соответствующим образом обновленный конструктор: public Employee (string name, int age, int id, float pay) { // Уже лучше! Используйте свойства для установки данных класса. // Это сократит количество дублированных проверок ошибок. Name = name; Age = age; ID = id; Pay = pay; } Помимо обновления конструкторов с целью использования свойств для присваивания значений, имеет смысл сделать это повсюду в реализации класса, чтобы гарантировать неукоснительное соблюдение бизнес-правил. Во многих случаях единственное место, где можно напрямую обращаться к приватным данным — это внутри самого свойства. С учетом сказанного модифицируем класс Employee, как показано ниже: class Employee { // Поля данных. private string empName; private int empID; private float currPay; private int empAge; // Конструкторы. public Employee () { } public Employee(string name, int id, float pay) :this(name, 0, id, pay) { } public Employee(string name, int age, int id, float pay) { Name = name; Age = age; ID = id; Pay = pay; } // Методы. public void GiveBonus(float amount) { Pay += amount; } public void DisplayStats () { Console.WriteLine("Name: {0}", Name); Console.WriteLine("ID: {0}", ID); Console.WriteLine("Age: {0}", Age); Console.WriteLine("Pay: {0}", Pay); } // Свойства остаются прежними... Внутреннее представление свойств Многие программисты склонны именовать традиционные методы доступа и изменения с применением префиксов get _ и set _ (например, get _ Name() и set _ NameO).
216 Часть II. Главные конструкции программирования на С# В языке С# такое соглашение об именовании само по себе не проблематично. Однако важно понимать, что "за кулисами" свойства представлены в коде CIL с использованием тех же самых префиксов. Например, открыв сборку EmployeeApp.exe в утилите ildasm.exe, можно увидеть, что каждое свойство отображается на скрытые методы get_XXX () и set_XXX (), вызываемые внутри CLR (рис. 5.6). Р Н.\Му Books\C* BoottXC» and the .NET Platfor.. fFite" View Help its H:\My Books\C# Book\C# and the .NET Platform 5th ed\First С ► MANIFEST Щ EmployeeApp В £ EmployeeApp. Employ ее ► .class private auto ansi beforefieldinit V currPay : private float32 V empAge : private ht32 s/ empID : private int32 v empName : private string ■ .ctor: void(string,int32,float32) ■ .ctor: void(string,int32,int32Jfloat32) ■ .ctor : void() ■ DisplayStats : void() ■ GiveBonus: void(float32) ■ get_ID : mt32() ■ get.Name: strmg() ■ get_Pay : float32() ■ set_Age : void(int32) ■ setJD : void(int32) Ш set_Name : void(string) ■ set_Pay : void(float32) к Aae : ins.tancejnt_3.20 assembly EmployeeApp Рис. 5.6. Свойство внутреннее представлено методами get/set Предположим, что тип Employee теперь имеет приватную переменную-член по имени empSSN для представления номера карточки социального страхования (SSN) лица; этой переменной будет манипулировать свойство по имени SocialSecurityNumber (также предположим, что специальный конструктор и метод DisplayStats () обновлены с учетом нового элемента данных). // Добавим поддержку нового поля, представляющего SSN сотрудника. class Employee { private string empSSN; public string SocialSecurityNumber { get { return empSSN; } set { empSSN = value; } } // Конструкторы. public Employee() {} public Employee(string name, int id, float pay) :this(name, 0, id, pay, ""){} public Employee (string name, int age, int id, float pay, string ss-n) Name = name; Age = age; ID = id; Pay = pay;
Глава 5. Определение инкапсулированных типов классов 217 SocialSecurityNumber = ssn; } public void DisplayStats () { Console.WriteLine ("Name: {0}", Name); Console.WriteLine("ID: {0}", ID); Console.WriteLine ("Age: {0}", Age); Console.WriteLine("Pay: {0}", Pay); Console.WriteLine("SSN: {0}", SocialSecurityNumber); } } Если в этом классе также определить два метода с именами getSocialSecurity Number () и set_SocialSecurityNumber(), возникнет ошибка времени компиляции: // Помните, что свойство в действительности отображается на пару get_/set_! class Employee { public string get_SocialSecurityNumber() { return empSSN; } public void set_SocialSecurityNumber(string ssn) { empSSN = ssn; } } На заметку! В библиотеках базовых классов .NET всегда отдается предпочтение использованию для инкапсуляции свойств, а не традиционных методов доступа и изменения. Поэтому при построении специальных классов, которые интегрируются с платформой .NET, избегайте объявления традиционных методов get и set. Управление уровнями видимости операторов get/set свойств Если не указать иного, видимость логики get и set управляется исключительно модификаторами доступа из объявления свойства: // Учитывая объявление свойства, логика get и set является общедоступной. public string SocialSecurityNumber { get { return .empSSN; } set { empSSN = value; } } В некоторых случаях было бы удобно задавать уникальные уровни доступа для логики get и set. Для этого просто добавьте префикс — ключевое слово доступа к соответствующим ключевым словам get или set (контекст без префикса получает видимость объявления свойства): // Пользователи объекта могут только получать значение, однако класс // Employee и производные типы могут также устанавливать значение. public string SocialSecurityNumber { get { return empSSN; } protected set { empSSN = value; }
218 Часть II. Главные конструкции программирования на С# В этом случае логика set свойства SocialSecurityNumber может быть вызвана только текущим классом и его производными классами, а потому не может быть вызвана из экземпляра объекта. Ключевое слово protected будет детально описываться в следующей главе, при рассмотрении наследования и полиморфизма. Свойства, доступные только для чтения и только для записи При инкапсуляции данных может понадобиться сконфигурировать свойство, доступное только для чтения. Для этого нужно просто опустить блок set. Аналогично, если требуется создать свойство, доступное только для записи, следует опустить блок get. Например (хоть это и не требуется в рассматриваемом примере), вот как сделать свойство SocialSecurityNumber доступным только для чтения: public string SocialSecurityNumber { get { return empSSN; } } После этого единственным способом модификации номера карточки социального страхования будет передача его в аргументе конструктора. Теперь попытка установить новое значение SSN для сотрудника внутри ведущего конструктора приведет к ошибке компиляции: public Employee(string name, int age, int id, float pay, string ssn) { Name = name; Age = age; ID = id; Pay = pay; // Теперь это невозможно, поскольку свойство // предназначено только для чтения! SocialSecurityNumber = ssn; } Если сделать это свойство доступным только для чтения, в логике конструктора останется лишь пользоваться лежащей в основе переменной-членом ssn. Статические свойства В С# также поддерживаются статические свойства. Вспомните из начала этой главы, что статические члены доступны на уровне класса, а не на уровне экземпляра (объекта) этого класса. Например, предположим, что в классе Employee определен статический элемент данных для представления названия организации, нанимающей сотрудников. Инкапсулировать статическое свойство можно следующим образом: // Статические свойства должны оперировать статическими данными! class Employee { private static string companyName; public static string Company { get { return companyName; } set { companyName = value; } } }
Глава 5. Определение инкапсулированных типов классов 219 Манипулировать статическими свойствами можно точно так же, как статическими методами: // Взаимодействие со статическим свойством. static void Main(string[] args) { Console.WriteLine("***** Fun with Encapsulation *****\n"); // Установить компанию. Employee.Company = "My Company"; Console.WriteLine("These folks work at {0}.", Employee.Company); Employee emp = new Employee("Marvin", 24, 456, 30000, 11-11-1111"); emp.GiveBonusA000); emp.DisplayStats() ; Console.ReadLine(); } И, наконец, вспомните, что классы могут поддерживать статические конструкторы. Поэтому если нужно гарантировать, что имя в статическом поле companyName всегда будет установлено в My Company, понадобится написать следующий код: // Статические конструкторы используются для инициализации статических данных. public class Employee { private Static companyName As string static Employee () { companyName = "My Company"; } } Используя такой подход, нет необходимости явно вызывать свойство Company для того, чтобы установить начальное значение: // Автоматическая установка значения "My Company" через статический конструктор. static void Main(string [ ] args) { Console.WriteLine("These folks work at {0}", Employee.Company); } В завершение исследований инкапсуляции с использованием свойств С# следует запомнить, что эти синтаксические сущности служат для тех же целей, что и традиционные методы get/set. Преимущество свойств состоит в том, что пользователи объектов могут манипулировать внутренними данными, применяя единый именованный элемент Исходный код. Проект EmployeeApp доступен в подкаталоге Chapter 5. Понятие автоматических свойств С выходом платформы .NET 3.5 в языке С# появился еще один способ определения простых служб инкапсуляции с минимальным кодом, а именно — синтаксис автоматических свойств. Для целей иллюстрации создадим новый проект консольного приложения С# по имени AutoProps. Добавим в него новый файл класса С# (Car.cs), в котором определен показанный ниже класс, инкапсулирующий единственную порцию данных с использованием классического синтаксиса свойств.
220 Часть II. Главные конструкции программирования на С# // Тип Саг, использующий стандартный синтаксис свойств. class Car { private string carName = string.Empty; public string PetName { get { return carName; } set { carName = value; } } } Хотя большинство свойств С# содержат в своем контексте бизнес-правила, не так уж редко бывает, что некоторые свойства не делают буквально ничего, помимо простого присваивания и возврата значений, как в приведенном выше коде. В таких случаях было бы слишком громоздко многократно определять приватные поля и некоторые простые определения свойств. Например, при построении класса, которому нужно 15 приватных элементов данных, в конечном итоге получаются 15 связанных с ними свойств, которые, по сути, представляют собой не более чем тонкие оболочки для инкапсуляции. Чтобы упростить процесс простой инкапсуляции данных полей, можно применять синтаксис автоматических свойств. Как следует из названия, это средство перекладывает работу по определению лежащего в основе приватного поля и связанного свойства С# на компилятор, используя небольшое усовершенствование синтаксиса. Рассмотрим переделанный класс Саг, в котором этот синтаксис применяется для быстрого создания трех свойств: class Car { // Автоматические свойства' public string PetName { get; set; } public int Speed { get; set; } public string Color { get; set; } } При определении автоматических свойств указывается модификатор доступа, лежащий в основе тип данных, имя свойства и пустые контексты get/set. Во время компиляции тип будет оснащен автоматически сгенерированным полем и соответствующей реализацией логики set/get. На заметку! Имя автоматически сгенерированного лежащего в основе приватного поля в коде С# не доступно. Единственный способ увидеть его — воспользоваться таким инструментом, как ildasm.exe. Однако в отличие от традиционных свойств С#, создавать автоматические свойства, предназначенные только для чтения или только для записи, нельзя. Хотя может показаться, что для этого достаточно опустить get или set в объявлении свойства, как показано ниже: // Свойство только для чтения? Ошибка! public int MyReadOnlyProp { get; } // Свойство только для записи? Ошибка! public int MyWriteOnlyProp { set; } Но на самом деле это приведет к ошибке компиляции. Определяемое автоматическое свойство должно поддерживать функциональность и чтения, и записи. Тем не менее, можно реализовать автоматическое свойство с более ограничивающими контекстами get или set:
Глава 5. Определение инкапсулированных типов классов 221 // Контекст get общедоступный, a set — защищенный. // Контекст get/set можно также объявить приватным, public int SomeOtherProperty { get; protected set; } Взаимодействие с автоматическими свойствами Поскольку компилятор будет определять лежащие в основе приватные поля во время компиляции, класс с автоматическими свойствами всегда должен использовать синтаксис свойств для установки и чтения лежащих в основе значений. Это важно отметить, потому что многие программисты напрямую используют приватные поля внутри определения класса, что в данном случае невозможно. Например, если бы класс Саг включал метод DisplayStatsO, он должен был бы реализовать этот метод, используя имя свойства: class Car { // Автоматические свойства! public string PetName { get; set; } public int Speed { get; set; } public string Color { get; set; } public void DisplayStatsO { Console.WriteLine("Car Name: {0}", PetName); Console.WriteLine("Speed: {0}", Speed); Console.WriteLine("Color: {0}", Color); } } При использовании объекта, определенного с автоматическими свойствами, можно присваивать и получать значения, используя ожидаемый синтаксис свойств: static void Main(string[ ] args) { Console.WriteLine("***** Fun with Automatic Properties *****\n"); Car с = new Car () ; c.PetName = "Frank"; c.Speed = 55; с Color = "Red"; Console.WriteLine("Your car is named {0}? That's odd...", c.PetName); c.DisplayStats(); Console.ReadLine(); } Замечания относительно автоматических свойств и значений по умолчанию При использовании автоматических свойств для инкапсуляции числовых и булевских данных можно сразу применять автоматически сгенерированные свойства типа прямо в своей кодовой базе, поскольку их скрытым полям будут присвоены безопасные значения по умолчанию, которые могут быть использованы непосредственно. Однако будьте осторожны, если синтаксис автоматического свойства применяется для упаковки переменной другого класса, потому что скрытое поле ссылочного типа также будет установлено в значение по умолчанию, т.е. null. Рассмотрим следующий новый класс по имени Garage, в котором используются два автоматических свойства:
222 Насть II. Главные конструкции программирования на С# class Garage { // Скрытое поле int установлено в О! public int NumberOfCars { get; set; } // Скрытое поле Car установлено в null! public Car MyAuto { get; set; } } Имея установленные С# значения по умолчанию для полей данных, значение NumberOfCars можно вывести в том виде, как оно есть (поскольку ему автоматически присвоено значение О). Однако если напрямую обратиться к MyAuto, то во время выполнения сгенерируется исключение ссылки на null, потому что лежащей в основе переменной-члену типа Саг не был присвоен новый объект: static void Main(string[] args) { Garage g = new Garage () ; // Нормально, печатается значение по умолчанию, равное 0. Console.WriteLine("Number of Cars: {0}", g.NumberOfCars); // Ошибка времени выполнения! Лежащее в основе поле в данный момент равно null! Console.WriteLine(g.MyAuto.PetName); Console.ReadLine(); } Учитывая, что лежащие в основе приватные поля создаются во время компиляции, в синтаксисе инициализации полей С# нельзя непосредственно размещать экземпляр ссылочного типа с помощью new. Это должно делаться конструкторами класса, что обеспечит создание объекта безопасным образом. Например: class Garage { // Скрытое поле установлено в 0! public int NumberOfCars { get; set; } // Скрытое поле установлено в null! public Car MyAuto { get; set; } // Для переопределения значений по умолчанию, присвоенных // скрытым полям, должны использоваться конструкторы, public Garage() { MyAuto = new Car (); NumberOfCars = 1; } public Garage (Car car, int number) { MyAuto = car; NumberOfCars = number; } } После этой модификации объект Car можно поместить в объект Garage, как показано ниже: static void Main(string[] args) { Console.WriteLine ("***** Fun with Automatic Properties *****\n"); // Создать автомобиль. Car с = new Car () ;
Глава-5. Определение инкапсулированных типов классов 223 c.PetName = "Frank"; с.Speed =55; с.Color = "Red"; c.DisplayStats(); // Поместить автомобиль в гараж. Garage g = new Garage () ; g.MyAuto = с; // Вывод количества автомобилей в гараже. Console.WriteLine("Number of Cars in garage: {0}", g.NumberOfCars); // Вывод названия автомобиля. Console.WriteLine("Your car is named: {0}", g.MyAuto.PetName); Console.ReadLine(); } Нельзя не согласиться с тем, что это очень полезное свойство языка программирования С#, поскольку свойства для класса можно определить с использованием простого синтаксиса. Естественно, если свойство помимо получения и установки лежащего в основе приватного поля требует дополнительного кода (такого как логика проверки достоверности, запись в журнал событий, взаимодействие с базой данных), придется определить его как "нормальное" свойство .NET вручную. Автоматические свойства С# никогда не делают ничего кроме предоставления простой инкапсуляции для лежащих в основе приватных данных (сгенерированных компилятором). Исходный код. Проект AutoProps доступен в подкаталоге Chapter 5, Понятие синтаксиса инициализации объектов Как можно было видеть на протяжении этой главы, при создании нового объекта конструктор позволяет указывать начальные значения. Также было показано, что свойства позволяют получать и устанавливать лежащие в основе данные в безопасной манере. При работе с классами, которые написаны другими, включая классы из библиотеки базовых классов .NET, нередко можно заметить, что в них есть более одного конструктора, позволяющего устанавливать каждую порцию данных внутреннего состояния. Учитывая это, программист обычно старается выбрать наиболее подходящий конструктор, после чего присваивает недостающие значения, используя доступные в классе свойства. Чтобы облегчить процесс создания и запуска объекта, в С# предлагается синтаксис инициализатора объекта. С помощью этого механизма можно создать новую объектную переменную и присвоить значения множеству свойств и/или общедоступных полей в нескольких строках кода. Синтаксически инициализатор объекта выглядит как список значений, разделенных запятыми, помещенный в фигурные скобки ({}). Каждый элемент в списке инициализации отображается на имя общедоступного поля или свойства инициализируемого объекта. Рассмотрим пример применения этого синтаксиса. Создадим новое консольное приложение по имени Object Initializers. Ниже показан класс Point, в котором используются автоматические свойства (что вообще-то не обязательно в данном примере, но помогает сократить код): class Point { public int X { get; set; } public int Y { get; set; }
224 Часть II. Главные конструкции программирования на С# public Point(int xVal, int yVal) { X = xVal; Y = yVal; } public Point () { } public void DisplayStats () { Console.WriteLine("[{0}, {1}]M, X, Y) ; } } Теперь посмотрим, как создавать объекты Point: static void Main(string[] args) { Console.WnteLine ("***** Fun with Object Init Syntax *****\nM); // Создать объект Point с установкой каждого свойства вручную. Point firstPomt = new Point (); firstPoint.X = 10; firstPoint.Y = 10; firstPoint.DisplayStats(); // Создать объект Point с использованием специального конструктора. Point anotherPoint = new PointB0, 20); anotherPoint.DisplayStats(); // Создать объект Point с использованием синтаксиса инициализатора объекта. Point finalPoint = new Point { X = 30, Y = 30 }; finalPoint.DisplayStats(); Console.ReadLine(); } При создании последней переменной Point не используется специальный конструктор (как это принято делать традиционно), а вместо этого устанавливаются значения общедоступных свойств X и Y. "За кулисами" вызывается конструктор типа по умолчанию, за которым следует установка значений указанных свойств. В конечном счете, синтаксис инициализации объектов — это просто сокращенная нотация синтаксиса создания переменной класса с помощью конструктора по умолчанию, с последующей установкой свойств данных состояния. Вызов специальных конструкторов с помощью синтаксиса инициализации В предыдущих примерах типы Point инициализировались неявным вызовом конструктора по умолчанию этого типа: // Здесь конструктор по умолчанию вызывается неявно. Point finalPoint = new Point { X = 30, Y = 30 }; При желании конструктор по умолчанию можно вызывать и явно: // Здесь конструктор по умолчанию вызывается явно Point finalPoint = new Point () { X = 30, Y = 30 }; Имейте в виду, что при конструировании типа с использованием нового синтаксиса инициализации можно вызывать любой конструктор, определенный в классе. В настоящий момент в типе Point определен двухаргументный конструктор для установки позиции (х, у). Таким образом, следующее объявление Point в результате приведет к установке X равным 100 и Y равным 100, независимо от того факта, что в аргументах конструктора указаны значения 10 и 16:
Глава 5. Определение инкапсулированных типов классов 225 // Вызов специального конструктора. Point pt = new Point A0, 16) { X = 100, Y = 100 }; Имея текущее определение типа Point, вызов специального конструктора с применением синтаксиса инициализации не особенно полезен (и чересчур многословен). Однако если тип Point предоставляет новый конструктор, позволяющий вызывающему коду установить цвет (через специальное перечисление PointColor), комбинация специальных конструкторов и синтаксиса инициализации объекта становится ясной. Изменим Point следующим образом: public enum PointColor { LightBlue, BloodRed, Gold } class Point { public int X { get; set; } public int Y { get; set; } public PointColor Color{ get; set; } public Point(int xVal, int yVal) { X = xVal; Y = yVal; Color = PointColor.Gold; } public Point(PointColor ptColor) { Color = ptColor; } public Point () : this(PointColor.BloodRed){ } public void DisplayStats () { Console.WnteLine("[{0}, {1}]M, X, Y) ; Console.WriteLine ("Point is {0}", Color); } } С помощью этого нового конструктора можно создать золотую точку (в позиции (90, 20)), как показано ниже: // Вызов более интересного специального конструктора / / с синтаксисом инициализации Point goldPoint = new Point (PointColor .Gold) { X = 90, Y = 20 }; Console.WriteLine("Value of Point is: {0 } ", goldPoint.DisplayStats ()); Инициализация вложенных типов Как было ранее кратко упомянуто в этой главе (и будет подробно рассматриваться в главе 6), отношение "имеет" ("has-a") позволяет составлять новые классы, определяя переменные-члены существующих классов. Например, предположим, что имеется класс Rectangle, который использует тип Point для представления координат верхнего левого и нижнего правого углов. Поскольку автоматические свойства устанавливают все внутренние переменные классов в null, новый класс будет реализован с использованием "традиционного" синтаксиса свойств. class Rectangle { private Point topLeft = new Point (); private Point bottomRight = new Point ();
226 Часть II, Главные конструкции программирования на С# public Point TopLeft get { return topLeft; } set { topLeft = value; } public Point BottomRight get { return bottomRight; } set { bottomRight = value; } public void DisplayStats () Console.WriteLine(M[TopLeft: {0}, {1}, {2} BottomRight: {3}, {4}, {5}]", topLeft.X, topLeft.Y, topLeft.Color, bottomRight.X, bottomRight.Y, bottomRight.Color); } } С помощью синтаксиса инициализации объекта можно было бы создать новую переменную Rectangle и установить внутренние экземпляры Point следующим образом: // Создать и инициализировать Rectangle. Rectangle myRect = new Rectangle { TopLeft = new Point { X = 10, Y = 10 }, BottomRight = new Point { X = 200, Y = 200} }; Преимущество синтаксиса инициализации объектов в том, что он в основном сокращает объем кода (предполагая, что нет подходящего конструктора). Вот как выглядит традиционный подход для установки того же экземпляра Rectangle: // Традиционный подход. Rectangle r = new Rectangle (); Point pi = new Point (); pl.X = 10; pl.Y = 10; r.TopLeft = pi; Point p2 = new Point () ; p2.X = 200; p2.Y = 200; r.BottomRight = p2; Хотя поначалу синтаксис инициализации объекта может показаться не слишком привычным, как только вы освоитесь с кодом, то будете удивлены, насколько быстро можно устанавливать состояние нового объекта с минимальными усилиями. В завершение главы рассмотрим три небольшие темы, которые способствуют лучшему пониманию построения хорошо инкапсулированных классов: константные данные, поля, доступные только на чтения, и определения частичных классов. Исходный код. Проект Objectlnitialozers доступен в подкаталоге Chapter 5. Работа с данными константных полей В С# имеется ключевое слово const для определения константных данных, которые никогда не могут изменяться после начальной установки. Как и можно было предположить, это полезно при определении наборов известных значений, логически привязанных к конкретному классу или структуре, для использования в приложениях.
Глава 5. Определение инкапсулированных типов классов 227 Предположим, что создается служебный класс по имени MyMathClass, в котором нужно определить значение PI (будем считать его равным 3.14). Начнем с создания нового проекта консольного приложения по имени ConstData. Учитывая, что другие разработчики не должны иметь возможность изменять значение PI в коде, его можно смоделировать с" помощью следующей константы: namespace ConstData { class MyMathClass { public const double PI = 3.14; } class Program { static void Main(string[] args) { Console.WriteLine ("***** Fun with Const *****\nM); Console.WriteLine("The value of PI is: { 0 } " , MyMathClass.PI); // Ошибка! Нельзя изменять константу! MyMathClass.PI = 3.1444; Console.ReadLine(); } } } Обратите внимание, что обращение к константным данным, определенным в классе MyMathClass, осуществляется с использованием префикса в виде имени класса (т.е. MyMathClass.PI). Это связано с тем, что константные поля класса являются неявно статическими. Однако допустимо определять и обращаться к локальным константным переменным внутри члена типа, например: static void LocalConstStringVariable() { // Локальные константные данные доступны непосредственно. const string fixedStr = "Fixed string Data"; Console.WriteLine(fixedStr); // Ошибка! fixedStr = "This will not work!"; } Независимо от того, где определяется константный элемент данных, следует всегда помнить, что начальное значение константы всегда должно быть указано в момент ее определения. Таким образом, если модифицировать класс MyMathClass так, что значение PI будет присваиваться в конструкторе класса, то возникнет ошибка времени компиляции: class MyMathClass { // Попытка установить PI в конструкторе? public const double PI; public MyMathClass() { // Ошибка1 PI = 3.14; } } Причина этого ограничения в том, что значение константных данных должно быть известно во время компиляции. Конструкторы же, как известно, вызываются во время выполнения.
228 Насть II. Главные конструкции программирования на С# Понятие полей только для чтения Близко к понятию константных данных лежит понятие данных полей, доступных только для чтения (которые не следует путать со свойствами только для чтения). Подобно константам, поля только для чтения не могут быть изменены после начального присваивания. Однако, в отличие от констант, значение, присваиваемое такому полю, может быть определено во время выполнения, и потому может быть на законном основании присвоено в контексте конструктора, но нигде более. Это может быть очень полезно, когда значение поля неизвестно вплоть до момента выполнения (возможно, потому, что для получения значения необходимо прочитать внешний файл), но нужно гарантировать, что оно не будет изменено после первоначального присваивания. Ддя иллюстрации рассмотрим следующее изменение в классе MyMathClass: class MyMathClass { // Поля только для чтения могут присваиваться //в конструкторах, но нигде более. public readonly double PI; public MyMathClass () { PI = 3.14; } } Любая попытка выполнить присваивание полю, помеченному как readonly, вне контекста конструктора приведет к ошибке компиляции: class MyMathClass { public readonly double PI; public MyMathClass () { PI = 3.14; } // Ошибка! public void ChangePIO { PI = 3.14444; } } Статические поля только для чтения В отличие от константных полей, поля только для чтения не являются неявно статическими. Поэтому если необходимо представить PI на уровне класса, то для этого понадобится явно использовать ключевое слово static. Если значение статического поля только для чтения известно во время компиляции, то начальное присваивание выглядит очень похожим на константу: class MyMathClass { public static readonly double PI = 3.14; } class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Const *****"); Console.WriteLine("The value of PI is: {0}", MyMathClass.PI); Console.ReadLine(); } }
Глава 5. Определение инкапсулированных типов классов 229 Однако если значение статического поля только для чтения не известно до момента выполнения, можно прибегнуть к использованию статического конструктора, как было описано ранее в этой главе: class MyMathClass { public static readonly double PI; static MyMathClass () { PI = 3.14; } } Исходный код. Проект ConstData доступен в подкаталоге Chapter 5. Понятие частичных типов В этой главе осталось еще разобраться с ролью ключевого слова partial. Класс производственного уровня легко может содержать многие сотни строк кода. К тому же, учитывая, что типичный класс определен внутри одного файла *.cs, может получиться очень длинный файл. В процессе создания классов нередко большая часть кода может быть проигнорирована, будучи однажды написанной. Например, данные полей, свойства и конструкторы, как правило, остаются неизменными во время эксплуатации, в то время как методы имеют тенденцию модифицироваться довольно часто. При желании можно разнести единственный класс на несколько файлов С#, чтобы изолировать рутинный код от более ценных полезных членов. Для примера загрузим ранее созданный проект EmployeeApp в Visual Studio и откроем файл Employee.cs для редактирования. Сейчас этот единственный файл содержит код для всех аспектов класса: class Employee { // Поля данных // Конструкторы // Методы // Свойства } Механизм частичных классов позволяет вынести конструкторы и поля данных в совершенно новый файл по имени Employee.Internal.cs (обратите внимание, что имя файла не имеет значения; здесь оно выбрано в соответствии с назначением класса). Первый шаг состоит в добавлении ключевого слова partial к текущему определению класса и вырезании кода, который должен быть помещен в новый файл: // Employee.cs partial class Employee { // Методы // Свойства } Предполагая, что новый класс добавлен к проекту, можно переместить поля данных и конструкторы в новый файл посредством простой операции вырезания и вставки. Кроме того, необходимо добавить ключевое слово partial к этому аспекту определения класса. // Employee.Internal.cs partial class Employee {
230 Часть II, Главные конструкции программирования на С# // Поля данных // Конструкторы } На заметку! Помните, что каждый аспект определения частичного класса должен быть помечен ключевым словом partial! Скомпилировав модифицированный проект, вы не должны заметить никакой разницы. Основная идея, положенная в основу частичного класса, реализуется только во время проектирования. Как только приложение скомпилировано, в сборке оказывается один цельный класс. Единственное требование при определении частичных типов связано с тем, что разные части должны иметь одно и то же имя и находиться в пределах одного и того же пространства имен .NET. Откровенно говоря, определения частичных классов применяются нечасто. Однако среда Visual Studio постоянно использует их в фоновом режиме. Позже в этой книге, когда речь пойдет о разработке приложений с графическим пользовательским интерфейсом посредством Windows Forms, Windows Presentation Foundation или ASP.NET, будет показано, что Visual Studio изолирует сгенерированный визуальным конструктором код в частичном классе, позволяя сосредоточиться на специфичной программной логике приложения. Исходный код. Проект EmployeeApp доступен в подкаталоге Chapter 5. Резюме Цель этой главы заключалась в ознакомлении с ролью типов классов С#. Вы видели, что классы могут иметь любое количество конструкторов, которые позволяют пользователю объекта устанавливать состояние объекта при его создании. В главе также было проиллюстрировано несколько приемов проектирования классов (и связанных с ними ключевых слов). Ключевое слово this используется для получения доступа к текущему объекту, ключевое слово static позволяет определять поля и члены, привязанные к классу (а не объекту), а ключевое слово const (и модификатор readonly) дает возможность определять элементы данных, которые никогда не изменяются после первоначальной установки. Большая часть главы была посвящена деталям первого принципа ООП: инкапсуляции. Здесь вы узнали о модификаторах доступа С# и роли свойств типа, синтаксиса инициализации объектов и частичных классов. Обладая всеми этими знаниями, теперь вы готовы к тому, чтобы перейти к следующей главе, в которой узнаете о построении семейства взаимосвязанных классов с использованием наследования и полиморфизма.
ГЛАВА 6 Понятия наследования и полиморфизма В предыдущей главе рассматривался первый принцип ООП — инкапсуляция. Вы узнали, как построить отдельный правильно спроектированный тип класса с конструкторами и различными членами (полями, свойствами, константами и полями, доступными только для чтения). В настоящей главе мы сосредоточимся на остальных двух принципах объектно-ориентированного программирования: наследовании и полиморфизме. Прежде всего, вы узнаете, как строить семейства связанных классов с применением наследования^ Как будет показано, эта форма повторного использования кода позволяет определять общую функциональность в родительском классе, которая может быть использована и, возможно, изменена в дочерних классах. По пути вы узнаете, как устанавливать полиморфный интерфейс в иерархиях классов, используя виртуальные и абстрактные члены. Завершается глава рассмотрением роли начального родительского класса в библиотеках базовых классов .NET — System.Object. Базовый механизм наследования Вспомните из предыдущей главы, что наследование — это аспект ООП, облегчающий повторное использование кода. Строго говоря, повторное использование кода существует в двух видах: наследование (отношение "является") и модель включения/делегации (отношение "имеет"). Начнем главу с рассмотрения классической модели наследования типа "является". При установке между классами отношения "является" строится зависимость между двумя или более типами классов. Базовая идея, лежащая в основе классического наследования, заключается в том, что новые классы могут создаваться с использованием существующих классов в качестве отправной точки. Давайте начнем с очень простого примера, создав новый проект консольного приложения по имени Basiclnheritance. Предположим, что спроектирован класс по имени Саг, моделирующий некоторые базовые детали автомобиля: // Простой базовый класс. class Car { public readonly int maxSpeed; private int currSpeed; public Car(int max) { maxSpeed = max; }
232 Часть II. Главные конструкции программирования на С# public Car() { maxSpeed = 55; } public int Speed { get { return currSpeed; } set { currSpeed = value; if (currSpeed > maxSpeed) { currSpeed = maxSpeed; } } } } Обратите внимание, что в Car применяется инкапсуляция для управления доступом к приватному полю currSpead с использованием общедоступного свойства по имени Speed. Имея такое определение, с типом Саг можно работать следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Basic Inheritance *****\n"); // Создать экземпляр типа Car и установить максимальную скорость. Car myCar = new Car (80); // Установить текущую скорость и вывести ее на консоль. myCar .Speed = 50; Console.WriteLine("My car is going {0} MPH", myCar.Speed); Console.ReadLine(); } Указание родительского класса для существующего класса Теперь предположим, что планируется построить новый класс по имени MiniVan. Подобно базовому классу Саг, необходимо, чтобы MiniVan поддерживал максимальную скорость, текущую скорость и свойство по имени Speed, позволяющее пользователю модифицировать состояние объекта. Ясно, что классы Саг и MiniVan взаимосвязаны; фактически можно сказать, что MiniVan "является" Саг. Отношение "является" (формально называемое классическим наследованием) позволяет строить новые определения классов, расширяющие функциональность существующих классов. Существующий класс, который будет служить основой для нового класса, называется базовым или родительским классом. Назначение базового класса состоит в определении всех общих данных и членов для классов, которые расширяют его. Расширяющие классы формально называются производными или дочерними классами. В С# для установки между классами отношения "является" используется операция двоеточия в определении класса: // MiniVan 'является' Саг. class MiniVan : Car { } Так в чем же состоит выигрыш от наследования MiniVan от базового класса Саг? Попросту говоря, объекты MiniVan имеют доступ ко всем общедоступным членам, определенным в базовом классе.
Глава 6. Понятия наследования и полиморфизма 233 На заметку! Хотя конструкторы обычно определяются как общедоступные, производный класс никогда не наследует конструкторы своего родительского класса. Учитывая отношение между этими двумя типами классов, класс MiniVan можно использовать следующим образом: static void Main(string [ ] args) { Console.WriteLine("***** Basic Inheritance *****\n"); // Создать объект MiniVan. MiniVan myVan = new MiniVan(); myVan.Speed = 10; Console.WriteLine("My van is going {0} MPH", myVan.Speed) ; Console.ReadLine(); } Обратите внимание, что хотя к классу MiniVan не добавлены никакие члены, имеется прямой доступ к public-свойству Speed родительского класса, и таким образом, его код используется повторно. Это намного лучше, чем создавать класс MiniVan, имеющий в точности те же члены, что и Саг, такие как свойство Speed. В случае дублирования кода в этих двух классах придется сопровождать два фрагмента одинакового кода, что очевидно является непроизводительным расходом времени. Всегда помните, что наследование предохраняет инкапсуляцию, а потому следующий код вызовет ошибку компиляции, поскольку приватные члены никогда не могут быть доступны через ссылку на объект: static void Main(string[] args) { Console.WriteLine("***** Basic Inheritance *****\nM); // Создать объект MiniVan. MiniVan myVan = new MiniVan(); myVan.Speed = 10; Console.WriteLine("My van is going {0} MPH", myVan.Speed) ; // Ошибка! Доступ к приватным членам невозможен! myVan.currSpeed = 55; Console.ReadLine(); } Кстати говоря, если в MiniVan будет определен собственный набор членов, он не получит доступа ни к одному приватному члену базового класса Саг. Опять-таки, приватные члены могут быть доступны только в классе, в котором они определены. // MiniVan унаследован от Саг. class MiniVan : Car { public void TestMethodO { //OK! Доступ к public-членам родителя в производном типе возможен. Speed = 10; // Ошибка! Нельзя осуществлять доступ к private-членам родителя //из производного типа! currSpeed =10; } }
234 Часть II. Главные конструкции программирования на С# О множественном наследовании ГЬворя о базовых классах, важно иметь в виду, что язык С# требует, чтобы любой конкретный класс имел в точности один непосредственный базовый класс. Невозможно создать тип класса, который напрямую унаследован от двух и более базовых классов (эта техника, поддерживаемая в неуправляемом C++, называется множественным наследованием). Попытка создать класс, в котором указано два непосредственных родительских класса, как показано в следующем коде, приводит к ошибке компиляции: //Не разрешается! Платформа .NET не допускает // множественное наследование классов! class WontWork : BaseClassOne, BaseClassTwo {} Как будет показано в главе 9, платформа .NET позволяет конкретному классу или структуре реализовывать любое количество дискретных интерфейсов. Благодаря этому, тип С# может представлять набор поведений, избегая сложностей, присущих множественному наследованию. Кстати говоря, хотя класс может иметь только один непосредственный базовый класс, допускается наследование одного интерфейса от множества других интерфейсов. Используя эту технику, можно строить изощренные иерархии интерфейсов, моделирующих сложные поведения (см. главу 9). Ключевое слово sealed В С# поддерживается еще одно ключевое слово — sealed, которое предотвращает наследование. Если класс помечен как sealed (запечатанный), компилятор не позволяет наследовать от него. Например, предположим, решено, что нет смысла в дальнейшем наследовании от класса MiniVan: // Класс Minivan не может быть расширен! sealed class MiniVan : Car { } Если вы (или коллега по команде) попытаетесь унаследовать от этого класса, то получите ошибку времени компиляции: // Ошибка! Нельзя расширять класс, // помеченный ключевым словом sealed! class DeluxeMiniVan : MiniVan {} Чаще всего запечатывание имеет смысл при проектировании служебного класса. Например, в пространстве имен System определено множество запечатанных классов. В этом легко убедиться, открыв окно Object Browser в Visual Studio 2010 (через меню View (Вид)) и выбрав класс String, определенный в пространстве имен System внутри сборки mscorlib.dll. На рис. 6.1 обратите внимание на использование ключевого слова sealed, выделенного в окне Summary (Сводка). Таким образом, подобно MiniVan, если попытаться построить новый класс, расширяющий System.String, возникнет ошибка компиляции: // Ошибка! Нельзя расширять класс, помеченный как sealed! class MyString : String {}
Глава 6. Понятия наследования и полиморфизма 235 Res ol veEve nt H an d ler [ <§ RuntimeArgumentHc RuntimeReldHandle RuntimeMethodHan< RuntimeTypeHandle SByte SerializableAttribute Single StackOverflowExcepti STAThreadAttribute PI StringComparer StringComparison рйЙ -♦ CtoneO ■'♦ Compare($tring, int string, int, int, System.StringComparison) i_. Ф Compare(string, int, string, int >nt System.Globalization.Ciirturi Ф Comparefstring, int. string, int «nt, bool, System.Globalizatron.C Ф CompareEtring, int, string, int int bool) ■'♦ Compare(string, int string, int int) "♦ Compare(string, string, bool, System.Globalization.Culturelnfo) , '" public sealed class String Member of S Summary: Represents text as a series of Unicode characters. Рис. 6.1. В библиотеках базовых классов определено множество запечатанных типов На заметку! В главе 4 было показано, что структуры С# всегда неявно запечатаны (см. табл. 4.3). Поэтому ни унаследовать одну структуру от другой, ни класс от структуры, ни структуру от класса не получится. Структуры могут использоваться только для моделирования отдельных атомарных, определенных пользователем типов. Для реализации отношения "является" необходимо применять классы. Как можно догадаться, существует множество других деталей наследования, о которых вы узнаете в оставшейся части главы. А пока просто имейте в виду, что операция двоеточия позволяет устанавливать между классами отношения "базовый-производный", а ключевое слово sealed предотвращает наследование. Изменение диаграмм классов Visual Studio В главе 2 кратко упоминалось, что среда Visual Studio 2010 позволяет устанавливать между классами отношения "базовый-производный" визуально во время проектирования. Чтобы использовать этот аспект IDE-среды, первый шаг состоит во включении нового файла диаграммы классов в текущий проект. Для этого выберите в меню пункт ProjectoAdd New Item (ПроектОДобавить новый элемент) и затем пиктограмму Class Diagram (Диаграмма классов); на рис. 6.2 имя файла ClassDiagraml.cd было изменено на Cars.cd. Add New Item - вагкЙ^^^^^Н ■ Sort by: I Installed Templates ;__J J * Visual C# Items Code 1 Data General Web Windows Forms WPf Reporting ш Ш m j § 1 Online Templates [J§| ; Щ 1 Name Cars^d Default -1 Settings File Text File Assembly Information File Class Diagram Applicabon Manifest File Windows Script Host Debugger Visualize» !»■ b i Class Diagram i Visual C# Items Visual C* Items Visual C* Items Visual C* items V.sualC# Items Visual C# hems L_ Я Visual C# hems IJ irch In;tallfd Templates... 1 Claw Diagram Type: Visual C# hems A blank class diagram ! ' jj ill : 1 il 1 Add )[_ Ceocei Рис. 6.2. Вставка в проект новой диаграммы классов
236 Часть II. Главные конструкции программирования на С# После щелчка на кнопке Add [Добавить) появится пустая поверхность проектирования. Для добавления классов к диаграмме просто перетаскивайте каждый файл из окна Solution Explorer на эту поверхность. Также помните, что удаление элемента в визуальном конструкторе (за счет его выбора и нажатия клавиши <Delete>), не приводит к удалению ассоциированного исходного кода, а просто убирает элемент из поверхности проектирования. Текущая иерархия классов показана на рис. 6.3. Carsxd X ЕЗЯ Саг Class 8 Fields £р currSpeed ♦ maxSpeed 9 Properties * Speed S Methods :* Car (+1 overload) ? MiniVan gf Class -♦Car ! Рис. 6.З. Визуальный конструктор Visual Studio На заметку! Итак, если необходимо автоматически добавить все текущие типы проекта на поверхность проектирования, выберите узел Project (Проект) в Solution Explorer и щелкните на кнопке View Class Diagram (Показать диаграмму классов) в правом верхнем углу окна Solution Explorer. Помимо простого отображения отношений между типами внутри текущего приложения, вспомните из главы 2, что можно также создавать совершенно новые типы и наполнять их членами, используя панель инструментов Class Designer (Конструктор классов) и окно Class Details (Детали класса). Если хотите использовать эти визуальные инструменты в процессе дальнейшего чтения книги — пожалуйста. Однако всегда анализируйте сгенерированный код, чтобы четко понимать, что эти инструменты делают за вашей спиной. Исходный код. Проект Basiclnheritance доступен в подкаталоге Chapter 6. Второй принцип ООП: подробности о наследовании Ознакомившись с базовым синтаксисом наследования, давайте рассмотрим более сложный пример и узнаем о многочисленных деталях построения иерархий классов. Для этого воспользуемся классом Employee, спроектированным в главе 5. Для начала создадим новое консольное приложение С# по имени Employees. Выберите пункт меню Project ^Add Existing Item (ПроектОДобавить существующий элемент) и перейдите к месту нахождения файлов Employee.cs и Employee.Internals.cs, которые были созданы в примере EmployeeApp из предыдущей главы. Выберите оба
Глава 6. Понятия наследования и полиморфизма 237 файла (щелкая на них при нажатой клавише <Ctrl>) и щелкните на кнопке ОК. Среда Visual Studio 2010 отреагирует копированием каждого файла в текущий проект Прежде чем начать построение производных классов, следует уделить внимание одной детали. Поскольку первоначальный класс Employee был создан в проекте по имени EmployeeApp, этот класс находится в идентично названном пространстве имен .NET. Пространства имен подробно рассматриваются в главе 14, а пока для простоты просто переименуйте текущее пространство имен (в обоих файлах) на Employees, чтобы оно соответствовало имени нового проекта: //Не забудьте изменить название пространства имен в обоих файлах! namespace Employees { partial class Employee } На заметку! Чтобы подстраховаться, скомпилируйте и запустите новый проект, нажав <Ctrl+F5>. Пока программа ничего не делает, однако это позволит убедиться в отсутствии ошибок компиляции. Нашей целью будет создание семейства классов, моделирующих различные типы сотрудников компании. Предположим, что необходимо воспользоваться функциональностью класса Employee при создании двух новых классов (Salesperson и Manager). Иерархия классов, которую мы вскоре построим, будет выглядеть примерно так, как показано на рис. 6.4 (имейте в виду, что в случае использования синтаксиса автоматических свойств С# отдельные поля в диаграмме не будут видны). Employee Class В Fields * Ф ,< ,<* * * company Name currPay empAge empID empName empSSN s Properties ЯГ dr ar sar sff ar Age Company ID Name Pay SocialSecurityNumber В Methods * * -♦ DisplayStats Employee (♦ 3 overloads) GiveBonus Class •*• Employee S Fields ф numberOfOptions s Properties Ш StockOptions Salesperson S Class + Employee 3 Fields $ numberOfSales a Properties 2sP Sal «Number Рис. 6.4. Начальная иерархия классов
238 Часть II. Главные конструкции программирования на С# Как показано на рис. 6.4, класс Salesperson "является" Employee (как и Manager). Вспомните, что в модели классического наследования базовые классы (вроде Employee) используются для определения характеристик, общих для всех наследников. Подклассы (такие как Salesperson и Manager) расширяют общую функциональность, добавляя дополнительную специфическую функциональность. Для нашего примера предположим, что класс Manager расширяет Employee, храня количество опционов на акции, в то время как класс Salesperson поддерживает количество продаж. Добавьте новый файл класса (Manager.ее), определяющий тип Manager следующим образом: // Менеджерам нужно знать количество их опционов на акции. class Manager : Employee { public int StockOptions { get; set; } } Затем добавьте новый файл класса (SalesPerson.cs), в котором определен класс Salesperson с соответствующим автоматическим свойством: // Продавцам нужно знать количество продаж. class Salesperson : Employee { public int SalesNumber { get; set; } } Теперь, после установки отношения "является", Salesperson и Manager автоматически наследуют все общедоступные члены базового класса Employee. Для иллюстрации обновите метод Main() следующим образом: // Создание объекта подкласса и доступ к функциональности базового класса. static void Main(string[] args) { Console.WriteLine("***** The Employee Class Hierarchy *****\n"); Salesperson danny = new Salesperson(); danny.Age =31; danny.Name = "Danny"; danny.SalesNumber = 50; Console.ReadLine(); } Управление созданием базового класса с помощью ключевого слова base Сейчас объекты Salesperson и Manager могут быть созданы только с использованием "бесплатного" конструктора по умолчанию (см. главу 5). Памятуя об этом, предположим, что к типу Manager добавлен новый конструктор, который принимает шесть аргументов и вызывается следующим образом: static void Main(string [] args) { // Предположим, что у Manager есть конструктор со следующей сигнатурой: // (string fullName, int age, int empID, // float currPay, string ssn, int numbOfOpts) Manager chucky = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000); Console.ReadLine (); }
Глава 6. Понятия наследования и полиморфизма 239 Если взглянуть на список параметров, то ясно видно, что большинство из них должно быть сохранено в переменных-членах, определенных в базовом классе Employee. В этом случае для класса Manager можно реализовать специальный конструктор следующего вида: public Manager(string fullName, int age, int empID, float currPay, string ssn, int numbOfOpts) { // Это свойство определено в классе Manager. StockOptions = numbOfOpts; // Присвоим входные параметры, используя // унаследованные свойства родительского класса. ID = empID; Age = age; Name = fullName; Pay = currPay; // Здесь возникнет ошибка компиляции, поскольку // свойство SSN доступно только для чтения! SocialSecurityNumber = ssn; } Первая проблема такого подхода состоит в том, что если определить какое-то свойство как доступное только для чтения (например, SocialSecurityNumber), то присвоить значение входного параметра string соответствующему полю не удастся, как можно видеть в финальном операторе специального конструктора. Вторая проблема состоит в том, что был неявно создан довольно неэффективный конструктор, учитывая тот факт, что в С#, если не указать иного, конструктор базового класса вызывается автоматически перед выполнением логики производного конструктора. После этого момента текущая реализация имеет доступ к многочисленным public-свойствам базового класса Employee для установки его состояния. Таким образом, в действительности во время создания объекта Manager выполняется семь действий (обращений к пяти унаследованным свойствам и двум конструкторам). Для оптимизации создания производного класса необходимо хорошо реализовать конструкторы подкласса, чтобы они явно вызывали специальный конструктор базового класса вместо конструктора по умолчанию. Поступая подобным образом, можно сократить количество вызовов инициализаций унаследованных членов (что уменьшит время обработки). Давайте модифицируем специальный конструктор класса Manager, применив ключевое слово base: public Manager(string fullName, int age, int empID, float currPay, string ssn, int numbOfOpts) : base (fullName, age, empID, currPay, ssn) { // Это свойство определено в классе Manager. StockOptions = numbOfOpts; } Здесь ключевое слово base ссылается на сигнатуру конструктора (подобно синтаксису, используемому для сцепления конструкторов на единственном классе с использованием ключевого слова this, как было показано в главе 5), что всегда указывает на то, что производный конструктор передает данные конструктору непосредственного родителя. В данной ситуации явно вызывается конструктор с пятью параметрами, определенный в Employee, что избавляет от излишних вызовов во время создания экземпляра базового класса. Специальный конструктор Salesperson выглядит в основном идентично:
240 Часть II. Главные конструкции программирования на С# //В качестве общего правила, все подклассы должны явно вызывать // соответствующий конструктор базового класса. public Salesperson(string fullName, int age, int empID, float currPay, string ssn, int numbOfSales) : base(fullName, age, empID, currPay, ssn) { // Это касается нас! SalesNumber = numbOfSales; } На заметку! Ключевое слово base можно использовать везде, где подкласс желает обратиться к общедоступному или защищенному члену, определенному в родительском классе. Применение этого ключевого слова не ограничивается логикой конструктора. Вы увидите примеры использования base в такой манере далее в главе, во время рассмотрения полиморфизма. И, наконец, вспомните, что как только в определении класса появляется специальный конструктор, конструктор по умолчанию из класса молча удаляется. Следовательно, не забудьте переопределить конструктор по умолчанию для типов Salesperson и Manager. Например: // Вернуть классу Manager конструктор по умолчанию. public Salesperson () {} Хранение фамильных тайн: ключевое слово protected Как уже должно быть известно, общедоступные (public) элементы непосредственно доступны отовсюду, в то время как приватные (private) могут быть доступны только в классе, где они определены. Вспомните из главы 5, что С# следует примеру многих других современных объектных языков и предлагает дополнительное ключевое слово для определения доступности членов, а именно — protected (защищенный). Когда базовый класс определяет защищенные данные или защищенные члены, он устанавливает набор элементов, которые могут быть доступны непосредственно любому наследнику. Например, чтобы позволить дочерним классам Salesperson и Manager непосредственно обращаться к разделу данных, определенному в Employee, можете изменить исходный класс Employee следующим образом: // Защищенные данные состояния. partial class Employee { // Теперь производные классы могут напрямую обращаться к этой информации. protected string empName; protected int empID; protected float currPay; protected int empAge; protected string empSSN; protected static string companyName; } Преимущество определения защищенных членов в базовом классе состоит в том, что производным типам больше не нужно обращаться к данным опосредованно, используя общедоступные методы и свойства. Возможным минусом такого подхода, конечно же, является то, что когда производный тип имеет прямой доступ к внутренним данным своего родителя, возникает вероятность непреднамеренного нарушения существующих бизнес- правил, которые реализованы в общедоступных свойствах. При определении защищенных членов создается уровень доверия между родительским и дочерним классами, поскольку компилятор не перехватит никаких нарушений существующих бизнес-правил.
Глава 6. Понятия наследования и полиморфизма 241 И, наконец, имейте в виду, что с точки зрения пользователя объекта защищенные данные трактуются как приватные (поскольку пользователь находится "вне" семейства). Потому следующий код некорректен: static void Main(string [ ] args) { // Ошибка! Доступ к защищенным данным через экземпляр объекта невозможен! Employee emp = new Employee(); emp.empName = "Fred"; } На заметку! Хотя protected-поля данных могут нарушить инкапсуляцию, объявлять protected- методы достаточно безопасно (и полезно). При построении иерархий классов очень часто приходится определять набор методов, которые используются только производными типами. Добавление запечатанного класса Вспомните, что запечатанный (sealed) класс не может быть расширен другими классами. Как уже упоминалось, эта техника чаще всего применяется при проектировании служебных классов. Тем не менее, при построении иерархий классов можно обнаружить, что некоторая ветвь в цепочке наследования нуждается в "отсечении", поскольку дальнейшее ее расширение не имеет смысла. Например, предположим, что в приложение добавлен еще один класс (PTSalesPerson), который расширяет существующий тип Salesperson. На рис. 6.5 показано текущее добавление. Manager Class •* Employee Salesperson Class ■♦ Employee PTSalesPerson Class -* Salesperson m ) 9\ _J Рис. 6.5. Класс PTSalesPerson Класс PTSalesPerson представляет продавца, который работает с частичной занятостью. Предположим, что необходимо гарантировать отсутствие возможности наследования от класса PTSalesPerson. (В конце концов, какой смысл в "частичной занятости от частичной занятости"?) Для предотвращения наследования от класса используется ключевое слово sealed: sealed class PTSalesPerson : Salesperson { public PTSalesPerson(string fullName, int age, int empID, float currPay, string ssn, int numbOfSales) :base (fullName, age, empID, currPay, ssn, numbOfSales) } // Остальные члены класса..,
242 Часть II. Главные конструкции программирования на С# Учитывая, что запечатанные классы не могут быть расширены, может возникнуть вопрос: каким образом повторно использовать функциональность, если класс помечен как sealed? Чтобы построить новый класс, использующий функциональность запечатанного класса, единственным вариантом является отказ от классического наследования в пользу модели включения/делегации (т.е. отношения "имеет"). Реализация модели включения/делегации Вспомните, что повторное использование кода возможно в двух вариантах. Только что было рассмотрено классическое отношение "является". Перед тем, как мы обратимся к третьему принципу ООП (полиморфизму), давайте поговорим об отношении "имеет" (еще известном под названием модели включения/делегации или агрегации). Предположим, что создан новый класс, который моделирует пакет льгот для сотрудников: // Этот новый тип будет работать как включаемый класс. class BenefitPackage { // Предположим, что есть другие члены, представляющие // медицинские/стоматологические программы и т.д. public double ComputePayDeduction() { return 125.0; } } Очевидно, что было бы довольно нелепо устанавливать отношение "является" между классом Benef itPackage и типами сотрудников. (Employee "является" Benef itPackage? Вряд ли). Однако должно быть ясно, что какие-то отношения между ними должны быть установлены. Короче говоря, понадобится выразить идею, что каждый сотрудник "имеет" BenefitPackage. Для этого можно модифицировать определение класса Employee следующим образом: // Сотрудники имеют льготы. partial class Employee { // Содержит объект BenefitPackage. protected BenefitPackage empBenefits = new BenefitPackage(); } Таким образом, один объект успешно содержит в себе другой объект. Однако чтобы представить функциональность включенного объекта внешнему миру, потребуется делегация. Делегация — это просто акт добавления общедоступных членов к включающему классу, которые используют функциональность включенного объекта. Например, можно было бы обновить класс Employee, чтобы он представлял включенный объект empBenefits с помощью специального свойства, а также пользоваться его функциональностью внутренне, через новый метод по имени GetBenefitCost(): public partial class Employee { // Содержит объект BenefitPackage. protected BenefitPackage empBenefits = new BenefitPackage(); // Представляет некоторое поведение, связанное с включенным объектом. public double GetBenefitCost () { return empBenefits.ComputePayDeduction(); }
Глава 6. Понятия наследования и полиморфизма 243 // Представляет объект через специальное свойство. public BenefitPackage Benefits { get { return empBenefits; } set { empBenefits = value; } } } В следующем обновленном методе Main() обратите внимание на взаимодействие с внутренним типом Benef itsPackage, который определен в типе Employee: static void Main(string [ ] args) { Console.WriteLine ("***** The Employee Class Hierarchy *****\nM); Manager chucky = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000); double cost = chucky.GetBenefitCost(); Console.ReadLine(); } Определения вложенных типов В предыдущей главе была кратко упомянута концепция вложенных типов, которая является разновидностью только что рассмотренного отношения "имеет". В С# (как и в других языках .NET) допускается определять тип (перечисление, класс, интерфейс, структуру или делегат) непосредственно внутри контекста класса или структуры. При этом вложенный (или "внутренний") тип считается членом охватывающего (или "внешнего") класса, и в глазах исполняющей системы им можно манипулировать как любым другим членом (полем, свойством, методом и событием). Синтаксис, используемый для вложения типа, достаточно прост: public class OuterClass { // Общедоступный вложенный тип может использоваться повсюду. public class PublicInnerClass {} // Приватный вложенный тип может использоваться // только членами включающего класса. private class PrivatelnnerClass {} } Хотя синтаксис ясен, понять, для чего это может потребоваться, не так-то просто. Чтобы разобраться с этой техникой, рассмотрим характерные особенности вложенных типов. • Вложенные типы позволяют получить полный контроль над уровнем доступа внутреннего типа, поскольку они могут быть объявлены как приватные (вспомните, что не вложенные классы не могут быть объявлены с использованием ключевого слова private). • Поскольку вложенный тип является членом включающего класса, он может иметь доступ к приватным членам включающего класса. • Часто вложенные типы удобны в качестве вспомогательных для внешнего класса и не предназначены для использования внешним миром. Когда тип включает в себя другой тип класса, он может создавать переменные-члены этого типа, как любой другой элемент данных. Однако если вложенный тип нужно использовать вне включающего типа, его понадобится квалифицировать именем включающего типа. Взгляните на следующий код:
244 Часть II. Главные конструкции программирования на С# static void Main(string [ ] args) { // Создать и использовать общедоступный вложенный класс. Верно! OuterClass.PublicInnerClass inner; inner = new OuterClass.PublicInnerClass (); // Ошибка компилятора! Доступ к приватному классу невозможен! OuterClass.PrivatelnnerClass inner2; inner2 = new OuterClass.PrivatelnnerClass (); } Чтобы использовать эту концепцию в рассматриваемом примере с сотрудниками, предположим, что теперь определение Bene fit Package вложено непосредственно в класс Employee: partial class Employee { public class BenefitPackage { // Предположим, что есть другие члены, представляющие // медицинские/стоматологические программы и т.д. public double ComputePayDeduction () { return 125.0; } Вложение может иметь произвольную глубину. Например, пусть требуется создать перечисление по имени BenefitPackageLevel, документирующее различные уровни льгот, которые могут быть предоставлены сотруднику. Чтобы программно установить тесную связь между Employee, BenefitPackage и BenefitPackageLevel, можно вложить перечисление следующим образом: // В Employee вложен BenefitPackage. public partial class Employee { // В BenefitPackage вложено BenefitPackageLevel. public class BenefitPackage { public enum BenefitPackageLevel { Standard, Gold, Platinum } public double ComputePayDeduction () { return 125.0; } Из-за отношений вложения обратите внимание на то, как приходится использовать это перечисление: static void Main(string [ ] args) { // Определить уровень льгот. Employee.BenefitPackage.BenefitPackageLevel myBenefitLevel = Employee.BenefitPackage.BenefitPackageLevel.Platinum; Console.ReadLine() }
Глава 6. Понятия наследования и полиморфизма 245 Блестяще! К этому моменту вы познакомились с множеством ключевых слов (и концепций), которые позволяют строить иерархии взаимосвязанных типов через классическое наследование, включение и вложенные типы. Если пока не все детали ясны, не переживайте. На протяжении оставшейся части книги вы построите еще много дополнительных иерархий. А теперь давайте перейдем к рассмотрению последнего принципа ООП: полиморфизма. Третий принцип ООП: поддержка полиморфизма в С# Вспомните, что в базовом классе Employee был определен метод по имени GiveBonusO со следующей первоначальной реализацией: public partial class Employee { public void GiveBonus(float amount) { currPay += amount; } } Поскольку этот метод был определен с ключевым словом public, теперь можно раздавать бонусы продавцам и менеджерам (а также продавцам с частичной занятостью): static void Main(string[] args) { Console.WriteLine ("***** The Employee Class Hierarchy *****\n"); // Дать каждому сотруднику бонус? Manager списку = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000); chucky.GiveBonusC00); chucky.DisplayStats(); Console.WriteLine(); Salesperson fran = new Salesperson ("Fran", 43, 93, 3000, "932-32-3232", 31); fran.GiveBonusB00); fran.DisplayStats(); Console.ReadLine(); } Проблема текущего кода состоит в том, что общедоступно унаследованный метод GiveBonusO работает идентично для всех подклассов. В идеале при подсчете бонуса для штатного продавца и частично занятого продавца должно приниматься во внимание количество продаж. Возможно, менеджеры должны получать дополнительные опционы на акции вместе с денежным вознаграждением. Учитывая это, вы однажды столкнетесь с интересным вопросом: "Как сделать так, чтобы связанные типы по-разному реагировали на один и тот же запрос?". Попробуем отыскать на него ответ. Ключевые слова virtual и override Полиморфизм предоставляет подклассу способ определения собственной версии метода, определенного в его базовом классе, с использованием процесса, который называется переопределением метода (method overriding). Чтобы пересмотреть текущий дизайн, нужно понять значение ключевых слов virtual и override. Если базовый класс желает определить метод, который может быть (но не обязательно) переопределен в подклассе, он должен пометить его ключевым словом virtual:
246 Часть II. Главные конструкции программирования на С# partial class Employee { // Этот метод теперь может быть переопределен производным классом. public virtual void GiveBonus(float amount) { currPay += amount; } } На заметку! Методы, помеченные ключевым словом virtual, называются виртуальными методами. Когда класс желает изменить реализацию деталей виртуального метода, он делает это с помощью ключевого слова override. Например, Salesperson и Manager могли бы переопределить GiveBonus (), как показано ниже (предполагая, что PTSalesPerson не будет переопределять GiveBonus (), а потому просто наследует версию, определенную Salesperson): class Salesperson : Employee { // Бонус продавца зависит от количества продаж. public override void GiveBonus(float amount) { int salesBonus = 0; if (numberOfSales >= 0 && numberOfSales <= 100) salesBonus = 10; else { if (numberOfSales >= 101 && numberOfSales <= 200) salesBonus = 15; else salesBonus = 20; } base.GiveBonus(amount * salesBonus); } } class Manager : Employee { public override void GiveBonus(float amount) { base.GiveBonus(amount); Random r = new Random () ; numberOfOptions += r.Next E00); } } Обратите внимание на использование каждым переопределенным методом поведения по умолчанию через ключевое слово base. Таким образом, полностью повторять реализацию логики GiveBonus () вовсе не обязательно, а вместо этого можно повторно использовать (и, возможно, расширять) поведение по умолчанию родительского класса. Также предположим, что текущий метод DisplayStatus () класса Employee объявлен виртуальным. При этом каждый подкласс может переопределять этот метод в расчете на отображение количества продаж (для продавцов) и текущих опционов на акции (для менеджеров). Например, рассмотрим версию метода DisplayStatus () в классе Manager (класс Salesperson должен реализовать DisplayStatus () аналогичным образом, чтобы вывести на консоль количество продаж):
Глава 6. Понятия наследования и полиморфизма 247 public override void DisplayStats () { base.DisplayStats (); Console.WriteLine("Number of Stock Options: {0}", numberOfOptions); } Теперь, когда каждый подкласс может интерпретировать, что именно эти виртуальные методы означают для него, каждый экземпляр объекта ведет себя как более независимая сущность: static void Main(string[] args) { Console.WriteLine ("***** The Employee Class Hierarchy *****\nM); // Лучшая система бонусов! Manager списку = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000); chucky.GiveBonusC00); chucky.DisplayStats(); Console.WriteLine(); Salesperson fran = new Salesperson("Fran", 43, 93, 3000, n932-32-3232n, 31); fran.GiveBonusB00); fran.DisplayStats(); Console.ReadLine(); } Ниже показан результат тестового запуска приложения в нынешнем виде: ***** The Employee Class Hierarchy ***** Name: Chucky ID: 92 Age: 50 Pay: 100300 SSN: 333-23-2322 Number of Stock Options: 9337 Name: Fran ID: 93 Age: 4 3 Pay: 5000 SSN: 932-32-3232 Number of Sales: 31 Переопределение виртуальных членов в Visual Studio 2010 Как вы уже, возможно, заметили, при переопределении члена класса необходимо помнить типы всех параметров, а также соглашения о передаче параметров (ref, out и params). В Visual Studio 2010 имеется очень полезное средство, которое можно использовать при переопределении виртуального члена. Если набрать слово override внутри контекста типа класса (и нажать клавишу пробела), то IntelliSense автоматически отобразит список всех переопределяемых членов родительского класса, как показано на рис. 6.6. После выбора члена и нажатия клавиши <Enter> среда IDE реагирует автоматическим заполнением шаблона метода вместо вас. Обратите внимание, что также добавляется оператор кода, который вызывает родительскую версию виртуального члена (эту строку можно удалить, если она не нужна). Например, при использовании этой техники во время переопределения метода DisplayStatusO добавится следующий автоматически сгенерированный код: public override void DisplayStats () { base.DisplayStats () ; }
248 Часть II. Главные конструкции программирования на С# $$Employees,SalesPerson ^DtsplayStatsQ else { if (nuwberOfSales >- 101 && numberOfSales <= 2вв) salesBonus = 15; else salesBonus ■ 20; } base.GiveBonus(amount * salesBonus); ) > public override void DisplayStats() { base.DisplayStats(); Console.WriteLine("Number of Sales: {0}", SalesNumber); ) I ♦|Equals(objectobj) j ♦ GetHashCodeO ; ♦ ToStringO Рис. 6.6. Быстрый просмотр переопределяемых методов в Visual Studio 2010 Запечатывание виртуальных членов Вспомните, что ключевое слово sealed применяется к типу класса для предотвращения расширения другими типами его поведения через наследование. Ранее класс PTSalesPerson был запечатан на основе предположения, что разработчикам не имеет смысла дальше расширять эту линию наследования. Иногда требуется не запечатывать класс целиком, а просто предотвратить переопределение некоторых виртуальных методов в производных типах. Например, предположим, что продавцы с частичной занятостью не должны получать определенные бонусы. Чтобы предотвратить переопределение виртуального метода GiveBonus() в классе PTSalesPerson, можно запечатать этот метод в классе Salesperson следующим образом: // Salesperson запечатал метод GiveBonus()■ class Salesperson : Employee { public override sealed void GiveBonus(float amount) { } } Здесь Salesperson действительно переопределяет виртуальный метод GiveBonus(), определенный в классе Employee, однако он явно помечен как sealed. Поэтому попытка переопределения этого метода в классе PTSalesPerson приведет к ошибке во время компиляции: sealed class PTSalesPerson : Salesperson { public PTSalesPerson(string fullName, int age, int empID, float currPay, string ssn, int numbOfSales) :base (fullName, age, empID, currPay, ssn, numbOfSales) }
Глава 6. Понятия наследования и полиморфизма 249 // Ошибка! Этот метод переопределять нельзя1 public override void GiveBonus(float amount) { } } Абстрактные классы В настоящее время базовый класс Employee спроектирован так, что поставляет различные данные-члены своим наследникам, а также предлагает два виртуальных метода (GiveBonus() и DisplayStatusO), которые могут быть переопределены наследниками. Хотя все это хорошо и замечательно, у данного дизайна есть один неприятный побочный эффект: можно непосредственно создавать экземпляры базового класса Employee: // Что это будет означать? Employee X = new Employee() ; В нашем примере базовый класс Employee имеет единственное назначение — определить общие члены для всех подклассов. По всем признакам вы не намерены позволять кому-либо создавать прямые экземпляры этого класса, поскольку тип Employee слишком общий по своей природе. Например, если кто-то скажет: "Я сотрудник!", то тут же возникнет вопрос: "Какой конкретно сотрудник?" (консультант, инструктор, административный работник, редактор, советник в правительстве и т.п.). Учитываяi что многие базовые классы склонны быть довольно неопределенными сущностями, намного лучший дизайн для рассматриваемого примера не должен разрешать непосредственное создание в коде нового объекта Employee. В С# можно добиться этого с использованием ключевого слова abstract в определении класса, создавая, таким образом, абстрактный базовый класс: II Превращение класса Employee в абстрактный // для предотвращения прямого создания экземпляров. abstract partial class Employee { } После этого попытка создать экземпляр класса Employee приведет к ошибке во время компиляции: // Ошибка! Нельзя создавать экземпляр абстрактного класса! Employee X = new Employee(); На первый взгляд может показаться очень странным, зачем определять класс, экземпляр которого нельзя создать непосредственно. Однако вспомните, что базовые классы (абстрактные или нет) очень полезны тем, что содержат общие данные и общую функциональность унаследованных типов. Используя эту форму абстракции, можно также моделировать общую "идею" сотрудника, а не обязательно конкретную сущность. Также следует понимать, что хотя непосредственно создать абстрактный класс нельзя, он все же присутствует в памяти, когда создан экземпляр его производного класса. Таким образом, совершенно нормально (и принято) для абстрактных классов определять любое количество конструкторов, вызываемых опосредованно при размещении в памяти экземпляров производных классов. Теперь получилась довольно интересная иерархия сотрудников. Позднее в этой главе, при рассмотрении правил приведения типов С#, мы добавим немного больше функциональности к этому приложению. А пока на рис. 6.7 показан основной дизайн типов на данный момент. Исходный код. Проект Employees доступен в подкаталоге Chapter 6.
250 Часть II. Главные конструкции программирования на С# Полиморфный интерфейс Когда класс определен как абстрактный базовый (с помощью ключевого слова abstract), в нем может определяться любое количество абстрактных членов. Абстрактные члены могут использоваться везде, где необходимо определить член, которые не предлагает реализации по умолчанию. За счет этого вы навязываете полиморфный интерфейс каждому наследнику, возлагая на них задачу реализации конкретных деталей абстрактных методов. Полиморфный интерфейс абстрактного базового класса просто ссылается на его набор виртуальных и абстрактных методов. На самом деле это интереснее, чем может показаться на первый взгляд, поскольку данная особенность ООП позволяет строить легко расширяемое и гибкое программное обеспечение. Для иллюстрации реализуем (и слегка модифицируем) иерархию фигур, кратко описанную в главе 5 при обзоре принципов ООП. Для начала создадим новый проект консольного приложения С# по имени Shapes. Обратите внимание на рис. 6.8, что типы Hexagon и Circle расширяют базовый класс Shape. Подобно любому базовому классу, в Shape определен набор членов (в данном случае свойство PetName и метод Draw()), общих для всех наследников. Подобно иерархии классов сотрудников, нужно запретить непосредственное создание экземпляров Shape, поскольку этот тип представляет слишком абстрактную концепцию. Чтобы предотвратить прямое создание экземпляров Shape, можно определить его как абстрактный класс. Также, учитывая, что производные типы должны уникальным образом реагировать на вызов метода Draw(), давайте пометим его как virtual и определим реализацию по умолчанию. Employee Abstract Class О Fields 5 Properties t Methods 6 Nested Types tft d Methods % ComputePayDeduction В Nested Types BenefitPackageLevel Erwm Standard Gold Platinum Г» Class •♦ Employee Я Fields * Properties a Methods Salesperson Class ■* Employee v.- . . .,.„,, 1 PTSafesFerson Sealed Class ■» Salesperson i „„,., ,J > Hexagon Class ■+ Shape \ Abstract Class | & Properties ^ PetName ! "Methods :Ф Draw ! ■'♦ Shape (♦ if IL .j 1 overload) i Circle Class ■♦Shape \ - T ThreeDCircle Class «•Circle J ®) Рис. 6.7. Иерархия классов Employee Рис. 6.8. Иерархия классов фигур
Глава 6. Понятия наследования и полиморфизма 251 // Абстрактный базовый класс иерархии. abstract class Shape { public Shape(string name = "NoName") { PetName - name; } public string PetName { get; set; } // Единственный виртуальный метод. public virtual void Draw() { Console.WriteLine ( "Inside Shape.Draw ()") ; } } Обратите внимание, что виртуальный метод Draw() предоставляет реализацию по умолчанию, которая просто выводит ан консоль сообщение, информирующее о том, что вызван метод Draw() базового класса Shape. Теперь вспомните, что когда метод помечен ключевым словом virtual, он предоставляет реализацию по умолчанию, которую автоматически наследуют все производные типы. Если дочерний класс так решит, он может переопределить такой метод, но он не обязан это делать. Учитывая это, рассмотрим следующую реализацию типов Circle и Hexagon: // Circle не переопределяет Draw(). class Circle : Shape { public Circle () {} public Circle(string name) : base(name) {} } // Hexagon переопределяет Draw(). class Hexagon : Shape { public Hexagon () {} public Hexagon(string name) : base(name){} public override void Draw() { Console.WriteLine("Drawing {0} the Hexagon", PetName); } } Польза от абстрактных методов становится совершенно ясной, как только вы запомните, что подклассы никогда не обязаны переопределять виртуальные методы (как в случае Circle). Поэтому если создать экземпляр типа Hexagon и Circle, обнаружится, что Hexagon знает, как правильно "рисовать" себя (или, по крайней мере, выводит на консоль соответствующее сообщение). Однако реакция Circle слегка приведет в замешательство: static void Main(string[] args) { Console.WriteLine("***** Fun with Polymorphism *****\n"); Hexagon hex = new Hexagon("Beth"); hex.Draw(); Circle cir = new Circle("Cindy"); // Вызывает реализацию базового класса1 cir.Draw () ; Console.ReadLine(); } Вывод этого метода Main() выглядит следующим образом: ***** pun W1th Polymorphism ***** Drawing Beth the Hexagon Inside Shape.Draw ()
252 Насть II. Главные конструкции программирования на С# Ясно, что это не особо интеллектуальный дизайн для текущей иерархии. Чтобы заставить каждый класс переопределить метод Draw(), можно определить Draw() как абстрактный метод класса Shape, а это означает отсутствие какой-либо реализации по умолчанию. Для пометки метода как абстрактного в С# служит ключевое слово abstract. He забывайте, что абстрактные методы не предусматривают вообще никакой реализации: abstract class Shape { // Вынудить все дочерние классы определить свою визуализацию. public abstract void Draw(); } На заметку! Абстрактные методы могут определяться только в абстрактных классах. Попытка поступить иначе приводит к ошибке во время компиляции. Методы, помеченные как abstract, являются чистым протоколом. Они просто определяют имя, возвращаемый тип (если есть) и набор параметров (при необходимости). Здесь абстрактный класс Shape информирует типы-наследники о том, что у него есть метод по имени Draw(), который не принимает аргументов и ничего не возвращает. О необходимых деталях должен позаботиться наследник. С учетом этого метод Draw() в классе Circle теперь должен быть обязательно переопределен. В противном случае Circle также должен быть абстрактным типом и оснащен ключевым словом abstract (что очевидно не подходит в данном примере). Ниже показаны необходимые изменения в коде: // Если не реализовать здесь абстрактный метод Draw() , то Circle также // должен считаться абстрактным, и тогда должен быть помечен как abstract! class Circle : Shape { public Circle () {} public Circle(string name) : base(name) {} public override void Draw() { Console.WriteLine("Drawing {0} the Circle", PetName); } } Выражаясь кратко, теперь делается предположение о том, что любой унаследованный от Shape класс должен иметь уникальную версию метода Draw(). Для демонстрации полной картины полиморфизма рассмотрим следующий код: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Polymorphism *****\n"); // Создать массив совместимых с Shape объектов. Shape [] myShapes = {new Hexagon(), new Circle (), new Hexagon("Mick"), new Circle("Beth"), new Hexagon("Linda")}; // Пройти циклом по всем элементам и взаимодействовать //с полиморфным интерфейсом. foreach (Shape s in myShapes) { s.Draw (); } Console.ReadLine (); }
Глава 6. Понятия наследования и полиморфизма 253 Ниже показан вывод этого метода Main(): ***** pun W1th Polymorphism ***** Drawing NoName the Hexagon Drawing NoName the Circle Drawing Mick the Hexagon Drawing Beth the Circle Drawing Linda the Hexagon Этот метод Main () иллюстрирует использование полиморфизма в чистом виде. Хотя невозможно напрямую создавать экземпляры абстрактного базового класса (Shape), можно свободно сохранять ссылки на объекты любого подкласса в абстрактной базовой переменной. Таким образом, созданный массив объектов Shape может хранить объекты, унаследованные от базового класса Shape (попытка поместить в массив объекты, несовместимые с Shape, приводит к ошибке во время компиляции). Учитывая, что все элементы в массиве my Shapes действительно наследуются от Shape, известно, что все они поддерживают один и тот же полиморфный интерфейс (или, говоря конкретно — все они имеют метод Draw()). Выполняя итерацию по массиву ссылок Shape, исполняющая система сама определяет, какой конкретный тип имеет каждый его элемент. И в этот момент вызывается корректная версия метода Draw(). Эта техника также делает очень простой и безопасной задачу расширения текущей иерархии. Например, предположим, что от абстрактного базового класса Shape унаследовано еще пять классов (Triangle, Square и т.д.). Благодаря полиморфному интерфейсу, код внутри цикла foreach не потребует никаких изменений, если компилятор увидит, что в массив myShapes помещены только Shape-совместимые типы. Сокрытие членов Язык С# предоставляет средство, логически противоположное переопределению методов, которое называется сокрытием (shadowing). Выражаясь формально, если производный класс определяет член, который идентичен члену, определенному в базовом классе, то производный класс скрывает родительскую версию. В реальном мире такая ситуация чаще всего возникает при наследовании от класса, который создавали не вы (и не ваша команда), например, в случае приобретения пакета программного обеспечения .NET у независимого поставщика. Для иллюстрации предположим, что вы получили от коллеги класс по имени ThreeDCitcle, в котором определен метод по имени Draw(), не принимающий аргументов: class ThreeDCircle { public void Draw () { Console.WriteLine("Drawing a 3D Circle"); } } Вы обнаруживаете, что ThreeDCircle "является" Circle, поэтому наследуете его от существующего типа Circle: class ThreeDCircle : Circle { public void Draw() { Console.WriteLine("Drawing a 3D Circle"); } }
254 Насть II. Главные конструкции программирования на С# После компиляции в окне ошибок Visual Studio 2010 появляется предупреждение (рис. 6.9). Рис. 6'.9. Мы только что скрыли член родительского класса Проблема в том, что в производном классе (ThreeDCircle) присутствует метод, идентичный унаследованному методу. Точное предупреждение компилятора в этом случае будет таким: 'phapes.ThreeDCircle.Draw() ' hides inherited member ' Shapes.Circle.Draw()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword. 'Shapes .ThreeDCircle. Draw () ' скрывает унаследованный член 'Shapes .Circle. Draw () '. Чтобы заставить текущий член переопределить эту реализацию, добавьте ключевое слово override. В противном случае добавьте ключевое слово new. Существует два способа решения этой проблемы. Можно просто обновить родительскую версию Draw(), используя ключевое слово override (как рекомендует компилятор). При таком подходе тип ThreeDCircle может расширять родительское поведение по умолчанию, как и требовалось. Однако если доступ к коду, определяющему базовый класс, отсутствует (как обычно случается с библиотеками от независимых поставщиков), то нет возможности модифицировать метод Draw(), сделав его виртуальным. В качестве альтернативы можно добавить ключевое слово new в определение члена Draw() производного типа (ThreeDCircle в данном случае). Делая это явно, вы устанавливаете, что реализация производного типа преднамеренно спроектирована так, чтобы игнорировать родительскую версию (в реальном проекте это может помочь, если внешнее программное обеспечение .NET каким-то образом конфликтует с вашим программным обеспечением). // Это класс расширяет Circle и скрывает унаследованный метод Draw(). class ThreeDCircle : Circle { // Скрыть любую реализацию Draw() , находящуюся выше в иерархии. public new void Draw() { Console.WriteLine("Drawing a 3D Circle"); } } Можно также применить ключевое слово new к любому члену типа, унаследованному от базового класса (полю, константе, статическому члену или свойству). В качестве еще одного примера предположим, что ThreeDCircle () желает скрыть унаследованное поле shapeName: // Этот класс расширяет Circle и скрывает унаследованный метод Draw() . class ThreeDCircle : Circle {
Глава 6. Понятия наследования и полиморфизма 255 // Скрыть поле shapeName, определенное выше в иерархии. protected new string shapeName; // Скрыть любую реализацию Draw() , находящуюся выше в иерархии. public new void Draw() { Console.WriteLine("Drawing a 3D Circle"); } } И, наконец, имейте в виду, что всегда можно обратиться к реализации базового класса скрытого члена, используя явное приведение (описанное в следующем разделе). Например, это демонстрируется в следующем коде: static void Main(string [ ] args) { // Здесь вызывается метод Draw() из класса ThreeDCircle. ThreeDCircle о = new ThreeDCircle(); о.Draw(); // Здесь вызывается метод Draw() родителя! ( (Circle)о) .Draw (); Console.ReadLine(); Исходный код. Проект Shapes доступен в подкаталоге Chapter 6. Правила приведения к базовому и производному классу Теперь, когда вы научились строить семейства взаимосвязанных типов классов, следует познакомиться с правилами, которым подчиняются операции приведения классов. Для этого вернемся к иерархии классов Employee, созданной ранее в главе. На платформе .NET конечным базовым классом служит System.Object. Поэтому все, что создается, "является" Object и может трактоваться как таковой. Учитывая этот факт, в объектной переменной можно хранить ссылку на экземпляр любого типа: void CastingExamples () { // Manager "является" System.Object, поэтому можно сохранять // ссылку на Manager в переменной типа object. object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5); } В примере Employees типы Manager, Salesperson и PTSalesPerson расширяют класс Employee, поэтому можно хранить любой из этих объектов в допустимой ссылке на базовый класс. Это значит, что следующий код также корректен: void CastingExamples () { // Manager "является" System.Object, поэтому можно сохранять // ссылку на Manager в переменной типа object. object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5); // Manager также "является" Employee. Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, 01-11-1321", 1) ; // PTSalesPerson "является" Salesperson. Salesperson ]ill = new PTSalesPerson ("Jill", 834, 3002, 100000, 11-12-1119", 90); }
256 Часть II. Главные конструкции программирования на С# Первое правило приведения между типами классов гласит, что когда два класса связаны отношением "является", всегда можно безопасно сохранить производный тип в ссылке базового класса. Формально это называется неявным приведением, поскольку оно "просто работает" в соответствии с законами наследования. Это делает возможным построение некоторых мощных программных конструкций. Например, предположим, что в текущем классе Program определен новый метод: static void GivePromotion(Employee emp) { // Повысить зарплату... // Предоставить место на парковке компании... Console.WriteLine ("{0 } was promoted!", emp.Name); } Поскольку этот метод принимает единственный параметр типа Employee, можно эффективно передавать этому методу любого наследника от класса Employee, учитывая отношение "является": static void CastingExamples () { // Manager "является" System.Object, поэтому можно сохранять // ссылку на Manager в переменной типа object. object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5); // Manager также "является" Employee. Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, 01-11-1321", 1) ; GivePromotion(moonUnit); // PTSalesPerson "является" Salesperson. Salesperson jill = new PTSalesPerson ("Jill", 834, 3002, 100000, 11-12-1119", 90); GivePromotion(jill); } Предыдущий код компилируется, благодаря неявному приведению от типа базового класса (Employee) к производному классу. Однако что если нужно также вызвать метод GivePromotion () для объекта frank (хранимого в данный момент в обобщенной ссылке System.Object)? Если вы передадите объект frank непосредственно в GivePromotion(), как показано ниже, то получите ошибку во время компиляции: // Ошибка! object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5) ; GivePromotion(frank); Проблема в том, что предпринимается попытка передать переменную, которая является не Employee, а более общим объектом System.Object. Поскольку он находится выше в цепочке наследования, чем Employee, компилятор не допустит неявного приведения, стараясь обеспечить максимально возможную безопасность типов. Несмотря на то что вы можете определить, что объектная ссылка указывает на Employee-совместимый класс в памяти, компилятор этого сделать не может, поскольку это не будет известно вплоть до времени выполнения. Чтобы удовлетворить компилятор, понадобится выполнить явное приведение. Второе правило приведения гласит: необходимо явно выполнять приведение "вниз", используя операцию приведения С#. Базовый шаблон, которому нужно следовать при выполнении явного приведения, выглядит примерно так: (Класс_к_которому_нужно_привести) существующаяСсылка Таким образом, чтобы передать переменную object методу GivePromotion(), потребуется написать следующий код: // Корректно! GivePromotion((Manager)frank);
Глава 6. Понятия наследования и полиморфизма 257 Ключевое слово as Помните, что явное приведение происходит во время выполнения, а не во время компиляции. Поэтому показанный ниже код: // Нет! Приводить frank к типу Hexagon нельзя, хотя код скомпилируете*! Hexagon hex = (Hexagon)frank; компилируется нормально, но вызывает ошибку времени выполнения, или, более формально — исключение времени выполнения. В главе 7 будут рассматриваться подробности структурированной обработки исключений, а пока следует лишь отметить, что при выполнении явного приведения можно перехватывать возможные ошибки приведения, применяя ключевые слова try и catch (см. главу 7): // Перехват возможной ошибки приведения. try { Hexagon hex = (Hexagon)frank; } catch (InvalidCastException ex) { Console.WriteLine(ex.Message); } Хотя это хороший пример защитного (defensive) программирования, С# предоставляет ключевое слово as для быстрого определения совместимости одного типа с другим во время выполнения. С помощью ключевого слова as можно определить совместимость, проверив возвращенное значение на равенство null. Взгляните на следующий код: // Использование as для проверки совместимости. Hexagon hex2 = frank as Hexagon; if (hex2 == null) Console.WriteLine("Sorry, frank is not a Hexagon..."); Ключевое слово is Учитывая, что метод GivePromotionO был спроектирован для приема любого возможного типа, производного от Employee, может возникнуть вопрос — как этот метод может определить, какой именно производный тип был ему передан? И, кстати, если входной параметр имеет тип Employee, как получить доступ к специализированным членам типов Salesperson и Manager? В дополнение к ключевому слову as, в С# предлагается ключевое слово is, которое позволяет определить совместимость двух типов. В отличие от ключевого слова as, если типы не совместимы, ключевое слово is возвращает false, а не null-ссылку. Рассмотрим следующую реализацию метода GivePromotionO: static void GivePromotion(Employee emp) { Console.WriteLine ("{0 } was promoted!", emp.Name); if (emp is Salesperson) { Console.WriteLine ("{0 } made {1} sale(s)!", emp.Name, ((Salesperson)emp).SalesNumber); Console.WriteLine (); } if (emp is Manager) { Console.WriteLine ("{0 } had {1} stock options ..." , emp.Name, ((Manager)emp).StockOptions);
258 Часть II. Главные конструкции программирования на С# Console.WriteLine (); } } Здесь во время выполнения производится проверка, на что именно в памяти указывает ссылка типа базового класса. Определив, что принят Salesperson или Manager, можно применить явное приведение и получить доступ к специализированным членам класса. Также обратите внимание, что окружать операции приведения конструкцией try/catch не обязательно! поскольку внутри контекста if, выполнившего проверку условия, уже известно, что приведение безопасно. Родительский главный класс System.Object В завершение этой главы исследуем детали устройства родительского главного класса всей платформы .NET — Object. Возможно, вы уже заметили в предыдущих разделах, что базовые классы всех иерархий (Car, Shape, Employee) никогда явно не указывали свои родительские классы: // Кто родитель Саг? class Car {...} В мире .NET каждый тип в конечном итоге наследуется от базового класса по имени System.Object (который в С# может быть представлен ключевым словом object). Класс Object определяет набор общих членов для каждого типа в каркасе. Фактически, при построении класса, который явно не указывает своего родителя, компилятор автоматически наследует его от Object. Если нужно очень четко прояснить свои намерения, можно определить класс, производный от Object, следующим образом: // Явное наследование класса от System.Object. class Car : object Как и в любом другом классе, в System.Object определен набор членов. В следующем формальном определении С# обратите внимание, что некоторые из этих членов определены как virtual, а это говорит о том, что данный член может быть переопределен в подклассе, в то время как другие помечены как static (и потому вызываются только на уровне класса): public class Object { // Виртуальные члены. public virtual bool Equals(object obj ) ; protected virtual void Finalize(); public virtual int GetHashCode() ; public virtual string ToStringO; // Уровень экземпляра, не виртуальные члены. public Type GetTypeO; protected object MemberwiseClone (); // Статические члены. public static bool Equals(object objA, object objB); public static bool ReferenceEquals(object objA, object objB); } В табл. 6.1 приведен перечень функциональности, предоставляемой некоторыми часто используемыми методами.
Глава 6. Понятия наследования и полиморфизма 259 Таблица 6.1. Основные методы System.Object Метод экземпляра Назначение Equals () FinalizeO GetHashCodeO ToStringO GetTypeO MemberwiseCloneO По умолчанию этот метод возвращает true, только если сравниваемые элементы ссылаются в точности на один и тот же объект в памяти. Таким образом, Equals () используется для сравнения объектных ссылок, а не состояния объекта. Обычно этот метод переопределяется, чтобы возвращать true, только если сравниваемые объекты имеют одинаковые значения внутреннего состояния. Следует отметить, что в случае переопределения Equals () потребуется также переопределить метод GetHashCodeO, потому что эти методы используются внутренне типами Hashtable для извлечения подобъектов из контейнера. Также вспомните из главы 4, что в классе ValueType этот метод переопределен для всех структур, чтобы он работал для сравнения на базе значений На данный момент можно считать, что этот метод (будучи переопределенным) вызывается для освобождения любых размещенных ресурсов перед удалением объекта. Сборка мусора CLR более подробно рассматривается в главе 8 Этот метод возвращает значение int, идентифицирующее конкретный экземпляр объекта Этот метод возвращает строковое представление объекта, используя формат Пространство имен>.<имя типа> (так называемое полностью квалифицированное имя). Этот метод часто переопределяется в подклассе для возврата строки, состоящей из пар "имя/значение", которая представляет внутреннее состояние объекта, вместо полностью квалифицированного имени Этот метод возвращает объект Туре, полностью описывающий объект, на который в данный момент производится ссылка. Коротко говоря, это метод идентификации типа во время выполнения (Runtime Type Identification — RTTI), доступный всем объектам (подробно обсуждается в главе 15) Этот метод возвращает полную (почленную) копию текущего объекта и часто используется для клонирования объектов (см. главу 9) Чтобы проиллюстрировать поведение по умолчанию, обеспечиваемое базовым классом Object, создадим новое консольное приложение С# по имени ObjectOverrides. Добавим в проект новый тип класса С#, содержащий следующее пустое определение типа по имени Person: // Помните: Person расширяет Object. class Person {} Теперь дополним метод Main() взаимодействием с унаследованными членами System.Object, как показано ниже: class Program static void Main(string[] args) { Console.WriteLine("***** Fun with System.Object ***' Person pi = new Person () ; // Использовать унаследованные члены System.Object. Console.WriteLine("ToString: {0}", pi.ToString ()); "An") ;
260 Часть II. Главные конструкции программирования на С# Console.WriteLine ("Hash code: {0}", pi.GetHashCode()); Console.WriteLine ("Type: {0}", pi.GetType()); // Создать другую ссылку на pi. Person p2 = pi; object о = p2; // Указывают ли ссылки на один и тот же объект в памяти? if (о.Equals(pi) && р2.Equals (о) ) { Console.WriteLine("Same instance1"); // один и тот же экземпляр } Console.ReadLine (); } } Вывод этого метода Main() выглядит следующим образом: ••••• pun W1th System.Object ***** ToString: ObjectOverrides. Person Hash code: 46104728 Type: ObjectOverrides. Person Same instance! Первым делом, обратите внимание, что реализация ToString() по умолчанию возвращает полностью квалифицированное имя текущего типа (ObjectOverrides.Person). Как будет показано позже, при рассмотрении построения специальных пространств имен в главе 14, каждый проект С# определяет "корневое пространство имен", название которого совпадает с именем проекта. Здесь мы создали проект под названием ObjectOverrides, поэтому тип Person (как и класс Program) помещен в пространство имен ObjectOverrides. Поведение Equals () по умолчанию заключается в проверке того, указывают ли две переменных на один и тот же объект в памяти. Здесь создается новая переменная Person по имени pi. В этот момент новый объект Person помещается в память управляемой кучи (managed heap). Переменная р2 также относится к типу Person. Однако вы не создаете новый экземпляр, а вместо этого присваиваете этой переменной ссылку pi. Таким образом, pi и р2 указывают на один и тот же объект в памяти, как и переменная о (типа object). Учитывая, что pi, р2 и о указывают на одно и то же местоположение в памяти, проверка эквивалентности дает положительный результат. Хотя готовое поведение System.Object во многих случаях может удовлетворять всем потребностям, довольно часто специальные типы переопределяют некоторые из этих унаследованных методов. Для иллюстрации модифицируем класс Person, добавив некоторые свойства, представляющие имя, фамилию и возраст лица; все они могут быть установлены с помощью специального конструктора: // Помните: Person расширяет Object. class Person { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public Person(string fName, string IName, int personAge) { FirstName = fName; LastName = IName; Age = personAge; } public Person () {} }
Глава 6. Понятия наследования и полиморфизма 261 Переопределение System.Object.ToStringO Многие создаваемые классы (и структуры) выигрывают от переопределения ToStringO для возврата строки с текстовым представлением текущего состояния экземпляра типа. Помимо прочего, это может быть довольно полезно при отладке. Как вы решите конструировать эту строку — дело персонального вкуса; однако рекомендованный подход состоит в разделении двоеточиями пар "имя/значение" и взятии всей строки в квадратные скобки (многие типы из библиотек базовых классов .NET следуют этому принципу). Рассмотрим следующую переопределенную версию ToStringO для нашего класса Person: public override string ToStringO { string myState; myState = string.Format("[First Name: {0}; Last Name: {1}; Age: {2}]", FirstName, LastName, Age); return myState; } Эта реализация ToStringO довольно прямолинейна, учитывая, что класс Person состоит всего их трех фрагментов данных состояния. Однако всегда нужно помнить, что правильное переопределение ToStringO должно также учитывать все данные, определенные выше в цепочке наследования. Когда вы переопределяете ToStringO для класса, расширяющего специальный базовый класс, первое, что следует сделать — получить возврат ToStringO от родительского класса, используя слово base. Получив строковые данные родителя, можно добавить к ним специальную информацию производного класса. Переопределение System.Object.Equals() Давайте также переопределим поведение Object. Equals () для работы с семантикой на основе значений. Вспомните, что по умолчанию Equals () возвращает true, только если два сравниваемых объекта ссылаются на один и тот же экземпляр объекта в памяти. Для класса Person может быть полезно реализовать Equals () для возврата true, когда две сравниваемых переменных содержат одинаковые значения (т.е. фамилию, имя и возраст). Прежде всего, обратите внимание, что входной аргумент метода Equals () — это общий System.Object. С учетом этого первое, что нужно сделать — удостовериться, что вызывающий код действительно передал тип Person, и для дополнительной подстраховки проверить, что входной параметр не является null-ссылкой. Установив, что передан размещенный Person, один подход состоит в реализации Equals () для выполнения сравнения поле за полем данных входного объекта с соответствующими данными текущего объекта: public override bool Eguals(object obj) { if (obj is Person && ob] != null) { Person temp; temp = (Person)obj; if (temp.FirstName == this.FirstName && temp.LastName == this.LastName && temp.Age == this.Age) { return true; }
262 Часть II. Главные конструкции программирования на С# else { return false; } } return false; } Здесь производится сравнение значения входного объекта с внутренними значениями текущего объекта (обратите внимание на применение ключевого слова this). Если имя, фамилия и возраст, записанные в двух объектах, идентичны, значит, есть два объекта с одинаковыми данными, и потому возвращается true. Любые другие возможные результаты возвратят false. Хотя этот подход действительно работает, представьте, насколько трудоемкой была бы реализация специального метода Equals () для нетривиальных типов, которые могут содержать десятки полей данных. Распространенным сокращением является использование собственной реализации ToStringO. Если у класса имеется правильная реализация ToStringO, которая учитывает все поля данных вверх по цепочке наследования, можно просто сравнить строковые данные объектов: public override bool Equals(object obj) { // Больше нет необходимости приводить obj к типу Person, // поскольку у всех имеется метод ToStringO . return obj .ToStringO == this .ToString () ; } Обратите внимание, что в этом случае нет необходимости проверять входной аргумент на принадлежность к корректному типу (в нашем примере — Person), поскольку все классы в .NET поддерживают метод ToStringO. Еще лучше то, что больше не нужно выполнять проверку равенства свойства за свойством, поскольку теперь просто проверяются значения, возвращенные методом ToStringO. Переопределение System.Object. GetHashCodeO Когда класс переопределяет метод Equals О, вы также обязаны переопределить реализацию по умолчанию GetHashCode(). Говоря упрощенно, хеш-код — это числовое значение, представляющее объект как определенное состояние. Например, если созданы две переменных string, хранящие значение Hello, они должны давать один и тот же хеш-код. Однако если одна переменная string хранит строку в нижнем регистре (hello), должны быть получены разные хеш-коды. По умолчанию System.Object.GetHashCodeO использует текущее местоположение объекта в памяти для порождения хеш-значения. Тем не менее, при построении специального типа, который нужно хранить в коллекции Hashtable (из пространства имен System.Collections), этот член должен быть всегда переопределен, поскольку Hashtable внутри вызывает Equals () HGetHashCodeO, чтобы извлечь правильный объект. На заметку! Точнее говоря, класс System.Collections.Hashtable внутренне вызывает метод GetHashCodeO для получения общего представления местоположения объекта, но последующий вызов Equals () определяет точное соответствие. Хотя мы не собираемся помещать Person в System.Collections.Hashtable, для полноты давайте переопределим GetHashCodeO. Существует немало алгоритмов, которые могут применяться для создания хеш-кода, одни из которых причудливы, а другие — не очень. В большинстве случаев можно сгенерировать значение хеш-кода, полагаясь на реализацию System.String.GetHashCode().
Глава 6. Понятия наследования и полиморфизма 263 Исходя из того, что класс String уже имеет солидный алгоритм хеширования, использующий символьные данные String для сравнения хеш-значений, если вы можете идентифицировать часть данных полей класса, которая должна быть уникальной для всех экземпляров (вроде номера карточки социального страхования), просто вызовите GetHashCodeO на этой части полей данных. Поскольку в классе Person определено свойство SSN, можно написать следующий код: // Вернуть хеш-код на основе уникальных строковых данных. public override int GetHashCodeO { return this.ToString () .GetHashCode(); } Если же выбрать уникальный строковый элемент данных затруднительно, но есть переопределенный метод ToString(), вызовите GetHashCodeO на собственном строковом представлении: // Возвращает хеш-код на основе значения ToString () персоны, public override int GetHashCodeO { return this.ToString () .GetHashCode(); } Тестирование модифицированного класса Person Теперь, когда виртуальные члены Object переопределены, давайте обновим Main О для добавления проверки внесенных изменений. static void Main(string [ ] args) { Console.WriteLine("***** Fun with System.Object *****\n"); // ПРИМЕЧАНИЕ: эти объекты идентичны для проверки // методов Equals () и GetHashCodeO . Person pi = new Person("Homer", "Simpson", 50); Person p2 = new Person("Homer", "Simpson", 50); // Получить строковые версии объектов. Console.WriteLine("pi.ToString () = {0}", pi.ToString ()); Console.WriteLine("p2.ToString () = {0}", p2.ToString ()); // Проверить переопределенный метод Equals (). Console.WriteLine ("pi = p2?: {0}", pi.Equals(p2)); // Проверить хеш-коды. Console.WriteLine ("Same hash codes?: {0}", pi.GetHashCode () == p2.GetHashCode ()) ; Console.WriteLine(); // Изменить возраст р2 и проверить снова. p2.Age =45; Console.WriteLine ("pi.ToStringO = {0}", pi. ToString ()) ; Console.WriteLine("p2.ToString () = {0}", p2.ToString()); Console.WriteLine ("pi = p2?: {0}", pi.Equals(p2)); Console.WriteLine("Same hash codes?: {0}", pi.GetHashCode() == p2.GetHashCode()); Console.ReadLine (); } Ниже показан вывод: ***** Fun with System.Object ***** pi.ToString () = [First Name: Homer; Last Name: Simpson; Age: 50] p2.ToString () = [First Name: Homer; Last Name: Simpson; Age: 50] pi = p2?: True Same hash codes?: True
264 Часть II. Главные конструкции программирования на С# pi.ToString () = [First Name: Homer; Last Name: Simpson; Age: 50] p2. ToString () = [First Name: Homer; Last Name: Simpson; Age: 45] pi = p2?: False Same hash codes?: False Статические члены System.Object В дополнение к только что рассмотренным членам уровня экземпляра, в System. Object также определены два очень полезных статических члена, которые проверяют эквивалентность на основе значений или на основе ссылок. Рассмотрим следующий код: static void StaticMembersOfObject () { // Статические члены System.Object. Person рЗ = new Person("Sally", "Jones", 4); Person p4 = new Person("Sally", "Jones", 4); Console.WriteLine ("P3 and P4 have same state: {0}", object.Equals(p3, p4)); Console.WriteLine ("P3 and P4 are pointing to same object: {0}", object.ReferenceEquals(p3, p4) ) ; } Здесь можно просто передать два объекта (любого типа) и позволить классу System. Object автоматически определить детали. Эти методы могут быть очень полезны при переопределении эквивалентности для специального типа, когда нужно сохранить возможность быстрого выяснения того, указывают ли две ссылочных переменных на одно и то же местоположение в памяти (через статический метод ReferenceEquals ()). Исходный код. Проект ObjectOverrides доступен в подкаталоге Chapter 6. Резюме В этой главе рассматривалась роль и подробности наследования и полиморфизма. Были представлены многочисленные новые ключевые слова и лексемы для поддержки каждой этой техники. Например, вспомните, что двоеточие применяется для установки родительского класса для заданного типа. Родительские типы могут определять любое количество виртуальных и/или абстрактных членов для установки полиморфного интерфейса. Производные типы переопределяют эти члены, используя ключевое слово override. В дополнение к построению многочисленных иерархий классов, в главе также рассматривалось явное приведение между базовым и производным типом. Кроме того, было дано описание главного класса среди всех родительских типов библиотеки базовых классов .NET— System.Object.
ГЛАВА 7 Структурированная обработка исключений В настоящей главе речь пойдет о способах обработки аномалий, возникающих во время выполнения, в коде на С# с применением методики так называемой структурированной обработки исключений (structured exception handling — SEH). Здесь будут описаны не только ключевые слова в С#, которые предназначены для этого (try, catch, throw, finally), но и отличия исключений уровня приложения и системы, а также роль базового класса System. Exception. Вдобавок будет показано, как создавать специальные исключения, и рассмотрены инструменты, доступные в Visual Studio 2010 для выполнения отладки. Ода ошибкам и исключениям Чтобы не нашептывало наше (порой раздутое) эго, ни один программист не идеален. Написание кода программного обеспечения является сложным делом, и из-за этой сложности довольно часто даже самые лучшие программы поставляются с различными, так сказать, проблемами. В одних случаях причиной этих проблем служит "плохо написанный" код (например, в нем происходит выход за пределы массива), а в других — ввод пользователями неправильных данных, которые не были предусмотрены в коде приложения (например, приводящий к присваиванию полю для ввода телефонного номера значения вроде "Списку"). Что бы ни служило причиной проблем, в конечном итоге приложение начинает работать не так, как ожидается. Прежде чем переходить к рассмотрению структурированной обработки исключений, давайте сначала ознакомимся с тремя наиболее часто применяемыми для описания аномалий терминами. • Программные ошибки (bugs). Так обычно называются ошибки, которые допускает программист. Например, предположим, что приложение создается с помощью неуправляемого языка C++. Если динамически выделяемая память не освобождается, что чревато утечкой памяти, появляется программная ошибка. • Пользовательские ошибки (user errors). В отличие от программных ошибок, пользовательские ошибки обычно возникают из-за тех, кто запускает приложение, а не тех, кто его создает. Например, ввод конечным пользователем в текстовом поле неправильно оформленной строки может привести к генерации ошибки подобного рода, если в коде не была предусмотрена возможность обработки некорректного ввода.
266 Часть II. Главные конструкции программирования на С# • Исключения (exceptions). Исключениями, или исключительными ситуациями, обычно называются аномалии, которые могут возникать во время выполнения 1 и которые трудно, а порой и вообще невозможно, предусмотреть во время программирования приложения. К числу таких возможных исключений относятся попытки подключения к базе данных, которой больше не существует, попытки открытия поврежденного файла или попытки установки связи с машиной, которая в текущий момент находится в автономном режиме. В каждом из этих случаев программист (и конечный пользователь) мало что может сделать с подобными "исключительными" обстоятельствами. По приведенным выше описаниям должно стать понятно, что структурированная обработка исключений в .NET представляет собой методику, предназначенную для работы с исключениями, которые могут возникать на этапе выполнения. Даже в случае программных и пользовательских ошибок, которые ускользнули от глаз программиста, однако, CLR будет часто автоматически генерировать соответствующее исключение с описанием текущей проблемы. В библиотеках базовых классов .NET определено множество различных исключений, таких как FormatException, IndexOutOfRangeException, FileNotFoundException, ArgumentOutOfRangeException и т.д. В терминологии .NET под "исключением" подразумеваются программные ошибки, пользовательские ошибки и ошибки времени выполнения, несмотря на то, что мы, программисты, можем считать каждый из этих видов ошибок совершенно отдельным типом проблем. Прежде чем погружаться в детали, давайте посмотрим, какую роль играет структурированная обработка исключений, и чем она отличается от традиционных методик обработки ошибок. На заметку! Чтобы упростить примеры кода, абсолютно все исключения, которые может выдавать тот или иной метод из библиотеки базовых классов, перехватываться не будут. В реальных проектах следует поступать согласно существующим требованиям. Роль обработки исключений в .NET До появления .NET обработка ошибок в среде операционной системы Windows представляла собой весьма запутанную смесь технологий. Многие программисты включали собственную логику обработки ошибок в контекст интересующего приложения. Например, команда разработчиков могла определять набор числовых констант для представления известных сбойных ситуаций и затем применять эти константы в качестве возвращаемых значений методов. Для примера рассмотрим следующий фрагмент кода на языке С. /* Типичный механизм отлавливания ошибок в С. */ #define E_FILENOTFOUND 1000 int SomeFunction () { // Предполагаем, что в этой функции происходит нечто // такое, что приводит к возврату следующего значения. return E_FILENOTFOUND/ } void main () { int retVal = SomeFunction (); if(retVal == E_FILENOTFOUND) printf ("Cannot find file..."); // He удается найти файл.. . }
Глава 7. Структурированная обработка исключений 267 Такой подход далеко не идеален из-за того факта, что константа EFILENOTFOUND представляет собой не более чем просто числовое значение, но уж точно не агента, способного помочь в решении проблемы. В идеале хотелось бы, чтобы название ошибки, сообщение с ее описанием и другой полезной информацией подавалось в одном удобном пакете (что как раз и происходит в случае применения структурированной обработки исключений). Помимо приемов, изобретаемых самими разработчиками, в API-интерфейсе Windows определены сотни кодов ошибок с помощью #define и HRESULT, а также множество вариаций простых булевских значений (bool, BOOL, VARIANTBOOL и т.д.). Более того, многие разработчики СОМ-приложений на языке C++ (а также VB 6) явно или неявно применяют небольшой набор стандартных СОМ-интерфейсов (наподобие ISupportErrorlnfo, IErrorlnfo или ICreateErrorlnfо) для возврата СОМ-клиенту понятной информации об ошибках. Очевидная проблема со всеми этими более старыми методиками — отсутствие симметрии. Каждая из них более-менее вписывается в рамки какой-то одной технологии, одного языка и, пожалуй, даже одного проекта. Чтобы положить конец всему этому безумству, в .NET была предложена стандартная методика для генерации и выявления ошибок в исполняющей среде, называемая структурированной обработкой исключений (SEH). Прелесть этой методики состоит в том, что она позволяет разработчикам использовать в области обработки ошибок унифицированный подход, который является общим для всех языков, ориентированных на платформу .NET. Благодаря этому, программист на С# может обрабатывать ошибки почти таким же с синтаксической точки зрения образом, как и программист на VB и программист на C++, использующий C++/CLI. Дополнительное преимущество состоит в том, что синтаксис, который требуется применять для генерации и перехвата исключений за пределами сборок и машин, тоже выглядит идентично. Например, при написании на С# службы Windows Communication Foundation (WCF) генерировать исключение SOAP для удаленного вызывающего кода можно с использованием тех же ключевых слов, которые применяются для генерации исключения внутри методов в одном и том же приложении. Еще одно преимущество механизма исключений .NET состоит в том, что в отличие от запутанных числовых значений, просто обозначающих текущую проблему, они представляют собой объекты, в которых содержится читабельное описание проблемы, а также детальный снимок стека вызовов на момент, когда изначально возникло исключение. Более того, конечному пользователю можно предоставлять справочную ссылку, которая указывает на определенный URL-адрес с описанием деталей ошибки, а также специальные данные, определенные программистом. Составляющие процесса обработки исключений в .NET Программирование со структурированной обработкой исключений подразумевает использование четырех следующих связанных между собой сущностей: • тип класса, который представляет детали исключения; • член, способный генерировать (throw) в вызывающем коде экземпляр класса исключения при соответствующих обстоятельствах; • блок кода на вызывающей стороне, ответственный за обращение к члену, в котором может произойти исключение; • блок кода на вызывающей стороне, который будет обрабатывать (или перехватывать (catch)) исключение в случае его возникновения.
268 Часть II. Главные конструкции программирования на С# При генерации и обработке исключений в С# используются четыре ключевых слова (try, catch, throw и finally). Любой объект, отражающий обрабатываемую проблему, должен обязательно представлять собой класс, унаследованный от базового класса System. Exception (или от какого-то его потомка). По этой причине давайте сначала рассмотрим роль этого базового класса в обработке исключений. Базовый класс System.Exception Все определяемые на уровне пользователя и системы исключения в конечном итоге всегда наследуются от базового класса System.Exception, который, в свою очередь, наследуется от класса System. Object. Ниже показано, как в целом выглядит этот класс (обратите внимание, что некоторые его члены являются виртуальными и, следовательно, могут переопределяться в производных классах): public class Exception : ISenalizable, _Exception { // Общедоступные конструкторы. public Exception(string message, Exception innerException); public Exception(string message); public Exception (); // Методы. public virtual Exception GetBaseException (); public virtual void GetObjectData(Serializationlnfо info, StreamingContext context); // Свойства. public virtual IDictionary Data { get; } public virtual string HelpLink { get; set; } public Exception InnerException { get; } public virtual string Message { get; } public virtual string Source { get; set; } public virtual string StackTrace { get; } public MethodBase TargetSite { get; } } Нетрудно заметить, что многие из содержащихся в System.Exception свойств являются по своей природе доступными только для чтения. Это объясняется тем, что для каждого из них значения, используемые по умолчанию, обычно поставляются в производных классах. Например, в производном классе IndexOutOfRangeException поставляется сообщение по умолчанию "Index was outside the bounds of the array" ("Индекс вышел за границы массива"). На заметку! В классе Exception реализованы два интерфейса .NET. Хотя интерфейсы подробно рассматриваются в главе 9, сейчас главное понять, что интерфейс _Exception позволяет сделать так, чтобы исключение .NET обрабатывалось неуправляемым кодом (таким как приложение СОМ), а интерфейс ISerializable — чтобы объект исключения сохранялся за пределами границ (например, границ машины). В табл. 7.1 приведено краткое описание некоторых наиболее важных.свойств класса System.Exception.
Глава 7. Структурированная обработка исключений 269 Таблица 7.1. Ключевые свойства System.Exception Свойство Описание Data HelpLink InnerException Message Source StackTrace TargetSite Это свойство, доступное только для чтения, позволяет извлекать коллекцию пар "ключ/значение" (представленную объектом, реализующим интерфейс iDictionary), которая предоставляет дополнительную определяемую программистом информацию об исключении. По умолчанию эта коллекция является пустой Это свойство позволяет получать или устанавливать URL-адрес, по которому доступен справочный файл или веб-сайт с детальным описанием ошибки Это свойство, доступное только для чтения, может применяться для получения информации о предыдущем исключении или исключениях, которые послужили причиной возникновения текущего исключения. Запись предыдущих исключений осуществляется путем их передачи конструктору самого последнего исключения Это свойство, доступное только для чтения, возвращает текстовое описание соответствующей ошибки. Само сообщение об ошибке задается в передаваемом конструктору параметре Это свойство позволяет получать или устанавливать имя сборки или объекта, который привел к выдаче исключения Это свойство, доступное только для чтения, содержит строку с описанием последовательности вызовов, которая привела к возникновению исключения. Как нетрудно догадаться, это свойство очень полезно во время отладки или для сохранения информации об ошибке во внешнем журнале ошибок Это свойство, доступное только для чтения, возвращает объект MethodBase с описанием многочисленных деталей метода, который привел к выдаче исключения (вызов вместе с ним ToString () позволяет идентифицировать этот метод по имени) Простейший пример Для иллюстрации пользы от структурированной обработки исключений необходимо создать класс, который будет выдавать исключение при надлежащих (или, можно сказать, исключительных) обстоятельствах. Создадим новый проект типа Console Application (Консольное приложение) на С# по имени SimpleException и определим в нем два класса (Саг (автомобиль) и Radio (радиоприемник)), связав их между собой отношением принадлежности ("has-a"). В классе Radio определим единственный метод, отвечающий за включение и выключение радиоприемника: class Radio public void TurnOn(bool on) { if(on) Console .WriteLme ("Jamming. ..") ; // работает else Console.WriteLine("Quiet time..."); // отключен } } В классе Car (показанном ниже) помимо использования класса Radio через принадлежность/делегирование, сделаем так, чтобы в случае превышения объектом Саг пре-
270 Часть II. Главные конструкции программирования на С# допределенной максимальной скорости (отражаемой с помощью константы экземпляра MaxSpeed) двигатель выходил из строя, приводя его в нерабочее состояние (отражаемое приватной переменной экземпляра bool по имени carlsDead). Кроме того, включим в Саг свойства для представления текущей скорости и указанного пользователем "дружественного названия" автомобиля и различные конструкторы для установки состояния нового объекта Саг. Ниже приведено полное определение Саг вместе с поясняющими комментариями. public class Car { / / Константа, отражающая допустимую максимальную скорость. public const int MaxSpeed = 100; // Свойства автомобиля. public int CurrentSpeed {get; set;} public string PetName {get; set;} //He вышел ли автомобиль из строя? private bool carlsDead; //В автомобиле есть радиоприемник. private Radio theMusicBox = new Radio(); // Конструкторы. public Car () { } public Car(string name, int speed) { CurrentSpeed = speed; PetName = name; } public void CrankTunes(bool state) { // Запрос делегата к внутреннему объекту. theMusicBox.TurnOn(state); } // Проверка, не перегрелся ли автомобиль. public void Accelerate (int delta) { if (carlsDead) Console.WriteLine ("{0} is out of order...", PetName); // вышел из строя else { CurrentSpeed += delta; if (CurrentSpeed > MaxSpeed) { Console.WriteLine ("{0 } has overheated1", PetName); // перегрелся CurrentSpeed = 0; carlsDead = true; } else // Вывод текущей скорости. Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed); } } } Теперь реализуем метод Main (), в котором объект Car будет превышать заданную максимальную скорость (установленную равной 100 в классе Саг), как показано ниже:
Глава 7. Структурированная обработка исключений 271 static void Main(string [ ] args) { Console.WriteLine("***** simple Exception Example ****+"); Console.WriteLine("=> Creating a car and stepping on it1"); Car myCar = new Car ( "Zippy" , 20); myCar.CrankTunes(true); for (int i = 0; i < 10; i++) myCar.AccelerateA0); Console.ReadLine(); } Вьгоод будет выглядеть следующим образом: ***** Simple Exception Example ***** => Creating a car and stepping on it! Jamming... => CurrentSpeed = 30 => CurrentSpeed = 40 => CurrentSpeed = 50 => CurrentSpeed = 60 => CurrentSpeed = 70 => CurrentSpeed = 80 => CurrentSpeed = 90 => CurrentSpeed = 100 Zippy has overheated! Zippy is out of order. . . Генерация общего исключения Имея функционирующий класс Саг, давайте рассмотрим простейший способ генерации исключения. Текущая реализация Accelerate () предусматривает просто отображение сообщения об ошибке, когда предпринимается попытка разогнать автомобиль (объект Саг) до скорости, превышающей максимальный предел. Для изменения этого метода так, чтобы при попытке разогнать автомобиль до скорости, превышающий установленный в классе Саг предел, генерировалось исключение, потребуется создать и сконфигурировать новый экземпляр класса System.Exception и установить значение доступного только для чтения свойства Message через конструктор класса. Чтобы объект ошибки отправлялся обратно вызывающей стороне, в С# используется ключевое слово throw. Ниже показан код модифицированного метода Accelerate (). //На этот раз, в случае превышения пользователем указанного //в MaxSpeed предела должно генерироваться исключение. public void Accelerate(int delta) { if (carlsDead) Console.WriteLine ("{0 } is out of order...", PetName); else { CurrentSpeed += delta; if (CurrentSpeed >= MaxSpeed) { carlsDead = true; CurrentSpeed = 0; // Использование ключевого слова throw для генерации исключения. throw new Exception(string.Format("{0} has overheated!", PetName)); } else Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed); } }
272 Часть II. Главные конструкции программирования на С# Прежде чем переходить к рассмотрению перехвата данного исключения в вызывающем коде, необходимо отметить несколько интересных моментов. При генерации исключения то, как будет выглядеть ошибка и когда она должна выдаваться, решает программист. В рассматриваемом примере предполагается, что при попытке увеличить скорость автомобиля (объекта Саг), который уже вышел из строя, должен генерироваться объект System. Exception для уведомления о том, что метод Accelerate () не может быть продолжен (это предположение может оказаться как подходящим, так и нет, в зависимости от создаваемого приложения). В качестве альтернативы метод Accelerate () можно было бы реализовать и так, чтобы он производил автоматическое восстановление, не выдавая перед этим никакого исключения. По большому счету, исключения должны генерироваться только в случае возникновения более критичных условий (например, отсутствии нужного файла, невозможности подключиться к базе данных и т.п.). Принятие решения о том, что должно служить причиной генерации исключения, требует серьезного продумывания и поиска веских оснований на стадии проектирования. Для преследуемых сейчас целей давайте считать, что попытка увеличить скорость неисправного автомобиля является вполне оправданной причиной для выдачи исключения. Перехват исключений Поскольку теперь метод Accelerate () способен генерировать исключение, вызывающий код должен быть готов обработать его, если оно вдруг возникнет. При вызове метода, который может генерировать исключение, должен использоваться блок try/catch. После перехвата объекта исключения можно вызывать различные его члены и извлекать детальную информацию о проблеме. Что делать с этими деталями дальше по большей части нужно решать самостоятельно. Может возникнуть желание занести их в специальный файл отчета, записать в журнал событий Windows, отправить по электронной почте системному администратору или отобразить конечному пользователю. Давайте для простоты выведем их в окне консоли. // Обработка сгенерированного исключения. static void Main(string[] args) { Console.WriteLine ("***** Simple Exception Example *****"); Console.WriteLine("=> Creating a car and stepping on it!"); Car myCar = new Car ("Zippy", 20); myCar.CrankTunes(true); // Разгон до скорости, превышающей максимальный // предел автомобиля, для выдачи исключения. try { for(int 1 = 0; i < 10; i++) myCar.AccelerateA0); } catch (Exception e) { Console.WriteLine ("\n*** Error! ***"); // ошибка Console.WriteLine("Method: {0}", e.TargetSite); // метод Console.WriteLine("Message: {0}", e.Message); // сообщение Console.WriteLine("Source: {0}", e.Source); // источник } // Ошибка была обработана, продолжается выполнение следующего оператора. Console.WriteLine("\n***** Out of exception logic *****"); Console . ReadLme () ; }
Глава 7. Структурированная обработка исключений 273 По сути, блок try представляет собой раздел операторов, которые в ходе выполнения могут выдавать исключение. Если обнаруживается исключение, управление переходит к соответствующему блоку catch. С другой стороны, в случае, если код внутри блока try не приводит к генерации исключения, блок catch полностью пропускается, и все проходит "гладко". Ниже показано, как будет выглядеть вывод в результате тестового выполнения данной программы. •ж*** Simple Exception Example ***** => Creating a car and stepping on it! Jamming... => CurrentSpeed = 30 => CurrentSpeed = 40 => CurrentSpeed = 50 => CurrentSpeed = 60 => CurrentSpeed = 70 => CurrentSpeed = 80 => CurrentSpeed = 90 *** Error! *** Method: Void Accelerate(Int32) Message: Zippy has overheated! Source: SimpleException ***** out of exception logic ***** Как здесь видно, после обработки исключения приложение может продолжать свою работу с того оператора, который идет сразу после блока catch. В некоторых случаях исключение может оказаться достаточно серьезным и стать причиной для завершения работы приложения. Чаще всего, однако, логика внутри обработчика исключений позволяет приложению спокойно продолжать работу (хотя, возможно, и менее функциональным образом, например, без возможности устанавливать соединение с каким-нибудь удаленным источником данных). Конфигурирование состояния исключения В настоящий момент объект System. Exception, сконфигурированный в методе Accelerate (), просто устанавливает значение, предоставляемое свойству Message (через параметр конструктора). Как показывалось ранее в табл. 7.1, в классе Exception доступно множество дополнительных членов (TargetSite, StackTrace, HelpLink и Data), которые могут помочь еще больше уточнить природу проблемы. Чтобы усовершенствовать текущий пример, давайте рассмотрим возможности каждого из этих членов более подробно. Свойство TargetSite Свойство System.Exception .TargetSite позволяет получать различные детали о методе, в котором было сгенерировано данное исключение. Как было показано в предыдущем методе Main () , вывод значения свойства TargetSite приводит к отображению возвращаемого значения, имени и параметров выдавшего исключение метода. Вместо простой строки свойство TargetSite возвращает строго типизированный объект System.Reflection.MethodBase. Объект такого типа может применяться для сбора многочисленных деталей, связанных с проблемным методом, а также классом, в котором он содержится. Для примера изменим предыдущую логику в блоке catch следующим образом: static void Main(string[] args) {
274 Часть II. Главные конструкции программирования на С# // Свойство TargetSite на самом деле // возвращает объект MethodBase. catch (Exception e) { Console. WriteLme ("\n*** Error1 ***"); Console .WriteLme ("Member name: {0}", e . TargetSite) ; // имя члена Console .WriteLme ("Class defining member: {0}", e.TargetSite.DeclaringType); // класс, определяющий член Console.WriteLme ("Member type: {0}", e.TargetSite.MemberType); // тип члена Console .WriteLme ("Message: {0}", e.Message); // сообщение Console .WriteLme ("Source : {0}", e.Source); // источник } Console .WriteLme (" \n***** Out of exception logic •** + **"); Console.ReadLine(); } На этот раз в коде с помощью свойства MethodBase . DeclaringType получается полностью определенное имя выдавшего ошибку класса (в данном случае SimpleException. Car), а с помощью свойства Member Type объекта MethodBase выясняется тип члена (свойство или метод), в котором возникло исключение. Ниже показано, как теперь будет выглядеть вывод в результате выполнения логики в блоке catch. *** Error! *** Member name: Void Accelerate(Int32) Class defining member: SimpleException.Car Member type: Method Message: Zippy has overheated! Source: SimpleException Свойство StackTrace Свойство System. Exception. StackTrace позволяет определить последовательность вызовов, которая привела к возникновению исключения. Значение этого свойства никогда самостоятельно не устанавливается — это делается автоматически во время создания исключения. Чтобы проиллюстрировать это, модифицируем логику в блоке catch следующим образом: catch(Exception e) { Console .WriteLme ("Stack: {0}", e . StackTrace) ; // вывод стека } Если теперь снова запустить программу, можно будет увидеть в окне консоли следующие данные трассировки стека (номера строк и пути к файлам, конечно же, на разных машинах выглядят по-разному): Stack: at SimpleException.Car.Accelerate (Int32 delta) in c:\MyApps\SimpleException\car.cs:line 65 at SimpleException.Program.Main() in с:\MyApps\SimpleException\Program.cs:line 21 Строка, возвращаемая из StackTrace, отражает последовательность вызовов, которая привела к выдаче данного исключения. Обратите внимание, что самый нижний номер в этой строке указывает на место возникновения первого вызова в последовательности, а самый верхний — на место, где точно находится породивший проблему член. Очевидно, что такая информация очень полезна при выполнении отладки или просмотре журналов в конкретном приложении, поскольку позволяет прослеживать весь путь, приведший к возникновению ошибки.
Глава 7. Структурированная обработка исключений 275 СВОЙСТВО HelpLink Хотя свойства Targetsite и StackTrace позволяют программистам понять, почему возникло то или иное исключение, пользователям выдаваемая ими информация мало что дает. Как уже показывалось ранее, для получения удобной для человеческого восприятия и потому пригодной для отображения конечному пользователю информации может применяться свойство System.Exception .Message. Кроме него, также может использоваться свойство HelpLink, которое позволяет направить пользователя на конкретный URL-адрес или стандартный справочный файл Windows, где содержатся более детальные сведения о возникшей проблеме. По умолчанию значением свойства HelpLink является пустая строка. Присваивание этому свойству какого-то более интересного значения должно делаться перед генерацией исключения типа System.Exception. Чтобы посмотреть, как это делается, изменим метод Car .Accelerate () следующим образом: public void Accelerate (int delta) { if (carlsDead) Console.WriteLine ("{0} is out of order...", PetName); else { CurrentSpeed += delta; if (CurrentSpeed >= MaxSpeed) { carlsDead = true; CurrentSpeed = 0; // Создание локальной переменной перед // выдачей объекта Exception для получения // возможности обращения к свойству HelpLink. Exception ex = new Exception(string.Format("{0} has overheated!", PetName)); ex.HelpLink = "http://www.CarsRUs.com"; throw ex; } else // Вывод текущей скорости Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed); } } Теперь можно модифицировать логику в блоке catch так, чтобы информация из данного свойства HelpLink выводилась в окне консоли: catch(Exception e) { // Ссылка для справки Console.WriteLine("Help Link: {0}", e.HelpLink); } Свойство Data Доступное в классе System.Exception свойство Data позволяет заполнять объект исключения соответствующей вспомогательной информацией (например, датой и временем возникновения исключения). Оно возвращает объект, реализующий интерфейс по имени IDictionary, который определен в пространстве имен System.Collections. В главе 9 более подробно рассматривается программирование с использованием интер-
276 Часть II. Главные конструкции программирования на С# фейсов, а также пространство имен System.Collections. На данный момент важно понять лишь то, что коллекции типа словарей позволяют создавать наборы значений, извлекаемых по ключу. Модифицируем метод Car .Accelerate (), как показано ниже: public void Accelerate(int delta) { if (carlsDead) Console.WriteLine ("{0} is out of order...", PetName); else { CurrentSpeed += delta; if (CurrentSpeed >= MaxSpeed) { carlsDead = true; CurrentSpeed = 0; // Создание локальной переменной перед выдачей // объекта Exception для обращения к свойству HelpLink. Exception ex = new Exception(string.Format("{0} has overheated!", PetName)); ex.HelpLink = "http://www.CarsRUs.com"; // Вставка специальных дополнительных данных, // имеющих отношение к ошибке. ex.Data.Add("TimeStamp", string.Format("The car exploded at {0}", DateTime.Now)); // дата и время ex.Data.Add("Cause", "You have a lead foot."); // причина throw ex; } else Console.WriteLine ("=> CurrentSpeed = {0}", CurrentSpeed); } } Для успешного перечисления пар "ключ/значение" необходимо не забыть сослаться на пространство имен System.Collections с помощью директивы using, поскольку будет использоваться тип DictionaryEntry в файле с классом, реализующем метод Main (): using System.Collections; Затем потребуется обновить логику catch так, чтобы в ней выполнялась проверка на предмет того, не равно ли null значение свойства Data (null является значением по умолчанию). После этого остается только воспользоваться свойствами Key и Value типа DictionaryEntry для вывода специальных данных в окне консоли. catch (Exception e) { //По умолчанию поле данных является пустым, поэтому // выполняется проверка на предмет равенства null. Console.WriteLine ("\n-> Custom Data:"); if (e.Data != null) { foreach (DictionaryEntry de in e.Data) Console.WriteLine ("-> {0}: {1}", de.Key, de.Value); } } Ниже показано, как теперь будет выглядеть вывод программы:
Глава 7. Структурированная обработка исключений 277 ***** Simple Exception Example ***** => Creating a car and stepping on it! Jamming... => CurrentSpeed = 30 => CurrentSpeed = 40 => CurrentSpeed = 50 => CurrentSpeed = 60 => CurrentSpeed = 70 => CurrentSpeed = 80 ■=> CurrentSpeed = 90 *** Error! *** Member name: Void Accelerate(Int32) Class defining member: SimpleException.Car Member type: Method Message: Zippy has overheated! Source: SimpleException Stack: at SimpleException.Car.Accelerate (Int32 delta) at SimpleException.Program.Main(String[] args) Help Link: http://www.CarsRUs.com -> Custom Data: -> TimeStamp: The car exploded at 1/12/2010 8:02:12 PM -> Cause: You have a lead foot. ***** out of exception logic ***** Свойство Data очень полезно, так как позволяет формировать специальную информацию об ошибке, не прибегая к созданию совершенно нового класса, который расширяет базовый класс Exception (до выхода версии .NET 2.0 это было единственным возможным вариантом). Однако каким бы полезным ни было свойство Data, разработчики .NET-приложений все равно довольно часто предпочитают создавать строго типизированные классы исключений, в которых специальные данные обрабатываются с помощью строго типизированных свойств. При таком подходе вызывающий код получает возможность перехватывать конкретный производный от Exception тип, а не углубляться в коллекцию данных в поиске дополнительных деталей. Чтобы понять, как это работает, сначала необходимо разобраться с отличиями между исключениями уровня системы и уровня приложений. Исходный код. Проект SimpleException доступен в подкаталоге Chapter 7. Исключения уровня системы (System. SystemException) В библиотеке базовых классов .NET содержится много классов, которые в конечном итоге наследуются от System.Exception. Например, в пространстве имен System определены ключевые классы исключений, такие как ArgumentOutOfRangeException, IndexOutOfRangeException, StackOverflowException и т.д. В других пространствах имен есть исключения, отражающие их поведение (например, в пространстве имен System. Drawing. Printing содержатся исключения, возникающие при печати, в System. 10 — исключения, возникающие во время ввода-вывода, в System. Data — исключения, связанные с базами данных, и т.д.). Исключения, которые генерируются самой платформой .NET, называются исключениями уровня системы. Эти исключения считаются неустранимыми фатальными ошибками. Они наследуются прямо от базового класса System. SystemException, который, в свою очередь, наследуется от System.Exception (а тот — от класса System.Object):
278 Часть II. Главные конструкции программирования на С# public class SystemException : Exception { // Различные конструкторы. } Из-за того, что в System.SystemException никакой дополнительной функциональности помимо набора специальных конструкторов больше не предлагается, может возникнуть вопрос о том, а зачем он тогда вообще существует. Попросту говоря, когда тип исключения наследуется от System. SystemException, это дает возможность понять, что сущностью, которая сгенерировала исключение, является исполняющая среда .NET, а не кодовая база функционирующего приложения. В этом можно довольно легко удостовериться с помощью ключевого слова is: // Действительно1 Исключение NullReferenceException // является исключением типа SystemException. NullReferenceException nullRefEx = new NullReferenceException(); Console.WriteLine("NullReferenceException is-a SystemException? : {0}", nullRefEx is SystemException); Исключения уровня приложения (System.ApplicationException) Поскольку все исключения .NET представляют собой типы классов, вполне допускается создавать собственные исключения, предназначенные для конкретного приложения. Из-за того, что базовый класс System. SystemException представляет исключения, генерируемые CLR-средой, может сложиться впечатление о том, что специальные исключения тоже должны наследоваться от System.Exception. Поступать подобным образом действительно допускается, однако рекомендуется наследовать их не от System.Exception, а от System.ApplicationException: public class ApplicationException : Exception { // Различные конструкторы. } Как и в SystemException, в классе ApplicationException никаких дополнительных членов кроме набора конструкторов, не предлагается. С точки зрения функциональности единственной целью System. ApplicationException является указание на источник ошибки. То есть при обработке исключения, унаследованного от System. ApplicationException, программист может смело полагать, что исключение было вызвано кодом функционирующего приложения, а не библиотекой базовых классов .NET или механизмом исполняющей среды .NET. Создание специальных исключений, способ первый Хотя для уведомления о возникновении ошибки во время выполнения можно всегда генерировать экземпляры System.Exception (как было показано в первом примере), иногда гораздо выгоднее создавать строго типизированное исключение, способное предоставлять уникальные детали по текущей проблеме. Например, предположим, что понадобилось создать специальное исключение (по имени CarlsDeadException) для предоставления деталей об ошибке, возникающей из-за увеличения скорости неисправного автомобиля. Для получения любого специального исключения в первую очередь необходимо создать новый класс, унаследованный от класса System.Exception или System. ApplicationException (по соглашению, имена всех классов исключений оканчиваются суффиксом Exception; в действительности это является рекомендуемым практическим приемом в .NET).
Глава 7. Структурированная обработка исключений 279 На заметку! Как правило, классы всех специальных исключений должны быть сделаны общедоступными, т.е. public (вспомните, что по умолчанию для не вложенных типов используется модификатор доступа internal, а не public). Объясняется это тем, что исключения часто передаются за пределы сборки, следовательно, они должны быть доступны вызывающему коду. Чтобы увидеть все на конкретном примере, давайте создадим новый проект типа Console Application (Консольное приложение) по имени CustomException и скопируем в него приведенные ранее файлы Car.cs и Radio, cs, выбрав в меню Project (Проект) пункт Add Existing Item (Добавить существующий элемент) и изменив для ясности название пространства имен, в котором определяются типы Саг и Radio, с SimpleException на CustomException. После этого добавим в него следующее определение класса: // Это специальное исключение описывает детали условия // выхода автомобиля из строя. public class CarlsDeadException : ApplicationException {} Как и в любой другой класс, в этот класс можно включать любое количество специальных членов, которые могли бы вызываться в блоке catch, а также переопределять в нем любые виртуальные члены, которые поставляются в родительских классах. Например, реализовать CarlsDeadException можно было бы за счет переопределения виртуального свойства Message. Вместо заполнения словаря данных (через свойство Data) при выдаче исключения, конструктор позволяет отправителю передавать данные о дате и времени и причине возникновения ошибки, которые могут быть получены с помощью строго типизированных свойств: public class CarlsDeadException : ApplicationException { private string messageDetails = String.Empty; public DateTime ErrorTimeStamp {get; set;} public string CauseOfError {get; set;} public CarlsDeadException(){} public CarlsDeadException(string message, string cause, DateTime time) { messageDetails = message; CauseOfError = cause; ErrorTimeStamp = time; } // Переопределение свойства Exception.Message. public override string Message { get { return string.Format("Car Error Message: {0}", messageDetails); } } } Здесь класс CarlsDeadException включает в себя приватное поле (message Details), которое предоставляет данные о текущем исключении, которые могут устанавливаться с помощью специального конструктора. Генерация этого исключения из метода Accelerate () производится довольно легко и заключается просто в выделении, настройке и выдаче исключения типа CarlsDeadException, а не общего типа System. Exception (обратите внимание, что в таком случае заполнять коллекцию данных вручную не понадобится):
280 Часть II. Главные конструкции программирования на С# // Выдача специального исключения CarlsDeadException. public void Accelerate (int delta) { CarlsDeadException ex = new CarlsDeadException (string.Format("{0} has overheated!", PetName), "You have a lead foot", DateTime.Now); ex.HelpLink = "http://www.CarsRUs.com"; throw ex; } Для перехвата такого поступающего исключения теперь можно модифицировать блок catch, чтобы в нем перехватывалось именно исключение типа CarlsDeadException (хотя из-за того, что System. CarlsDeadException является потомком System. Exception, перехват в нем исключения типа System.Exception также допустим). static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Custom Exceptions *****\n"); Car myCar = new Car ("Rusty", 90); try { // Отслеживание исключения. myCar.AccelerateE0); } catch (CarlsDeadException e) { Console.WriteLine(e.Message) ; Console.WriteLine(e.ErrorTimeStamp); Console.WriteLine(e.CauseOfError) ; } Console.ReadLine (); } Теперь, когда известно, как в общем выглядит процесс создания специального исключения, может возникнуть вопрос о том, когда к нему следует прибегать. Обычно необходимость в создании специальных исключений возникает, только если ошибка тесно связана с генерирующим ее классом (например, специальный файловый класс может выдавать набор специальных ошибок, связанных с файлами, класс Саг — ошибки, связанные с автомобилем, объект доступа к данным — ошибки, связанные с отдельной таблицей в базе данных, и т.д.). Их создание позволяет обеспечить вызывающий код возможностью обрабатывать многочисленные исключения за счет описания каждой ошибки по отдельности. Создание специальных исключений, способ второй В предыдущем примере в специальном типе CarlsDeadException переопределялось свойство System.Exception.Message для настройки специального сообщения об ошибке и поставлялось два специальных свойства для предоставления дополнительных фрагментов данных. В реальности, однако, переопределять виртуальное свойство Message вовсе не требуется, поскольку можно также просто передавать поступающее сообщение конструктору родителя, как показано ниже: public class CarlsDeadException : ApplicationException { public DateTime ErrorTimeStamp { get; set; } public string CauseOfError { get; set; } public CarlsDeadException () { }
Глава 7. Структурированная обработка исключений 281 // Передача сообщения конструктору родителя. public CarlsDeadException(string message, string cause, DateTime time) :base(message) { CauseOfError = cause; ErrorTimeStamp = time; } } Обратите внимание, что на этот раз никакая строковая переменная для представления сообщения не определяется и никакое свойство Message не переопределяется. Вместо этого производится передача соответствующего параметра конструктору базового класса. С таким дизайном специальный класс исключения представляет собой уже нечто большее, чем просто класс с уникальным именем, унаследованный от System.ApplicationException (и, при необходимости, имеющий дополнительные свойства), поскольку не содержит никаких переопределений базового класса. Не стоит удивляться, если многие (а то и все) специальные классы исключений придется создавать именно по такой простой схеме. Во многих случаях роль специального исключения состоит не в предоставлении дополнительной функциональности помимо той, что унаследована от базовых классов, а в обеспечении строго именованного типа, четко описывающего природу ошибки и тем самым позволяющего клиенту использовать разную логику обработки для разных типов исключений. Создание специальных исключений, способ третий Если планируется создать действительно заслуживающий внимания специальный класс исключения, необходимо позаботиться о том, чтобы он соответствовал наилучшим рекомендациям .NET. В частности это означает, что он должен: • наследоваться от ApplicationException; • сопровождаться атрибутом [System. Serializable]; • иметь конструктор по умолчанию; • иметь конструктор, который устанавливает значение унаследованного свойства Message; • иметь конструктор для обработки "внутренних исключений"; • иметь конструктор для обработки сериализации типа. Исходя из рассмотренного на текущий момент базового материла по .NET, роль атрибутов и сериализации объектов может быть совершенно не понятна, в чем ничего страшного нет, потому что эти темы будут подробно раскрываться далее в книге (в главе 15, которая посвящена атрибутам, и в главе 20, в которой рассматриваются службы сериализации). В завершение изучения специальных исключений ниже приведена последняя версия класса CarlsDeadException, в которой поддерживается каждый из упомянутых выше специальных конструкторов: [Serializable] public class CarlsDeadException : ApplicationException { public CarlsDeadException () { } public CarlsDeadException(string message) : base ( message ) { } public CarlsDeadException(string message, System.Exception inner) : base ( message, inner ) { } protected CarlsDeadException ( System.Runtime.Serialization.SerializationInfo info,
282 Часть II. Главные конструкции программирования на С# System.Runtime.Serialization.StreamingContext context) : base ( info, context ) { } // Далее могут идти любые дополнительные специальные // свойства, конструкторы и члены данных. } Поскольку специальные исключения, создаваемые в соответствии с наилучшими практическими рекомендациям .NET, отличаются только именами, не может не радовать тот факт, что в Visual Studio 2010 поставляется специальный шаблон фрагмента кода под названием Exception (рис. 7.1), который позволяет автоматически генерировать новый класс исключения, отвечающий требованиям наилучших практических рекомендаций .NET. (Как рассказывалось в главе 2, для активизации фрагмента кода необходимо ввести его имя, которым в данном случае является exception, и два раза нажать клавишу <ТаЬ>.) C*rIsDeadExcept«on.cs* X Q "TjCustomtxceptioaCadsDeadExceptbo *I ♦CarlsDeadEKception(stringmessage, stnngcause, 0 'I ex I- > <Ctri+Att*Space> exception j Code snippet for exception I 1 Рис. 7.1. Шаблон фрагмента кода под названием exception Исходный код. Проект CustomException находится в подкаталоге Chapter 7. Обработка многочисленных исключений В простейшем варианте блок try сопровождается только одним блоком catch. В реальности, однако, часто требуется, чтобы операторы в блоке try могли приводить к срабатыванию нескольких возможных исключений. Чтобы рассмотреть пример, создадим новый проект типа Console Application (Консольное приложение) на С# по имени ProcessMultipleExpceptions. Добавим в него файлы Car.cs, Radio, cs и CarIsDeadException.es из предыдущего примера CustomException (выбрав в меню Project пункт Add Existing Item) и соответствующим образом модифицируем названия пространств имен. Далее изменим в классе Саг метод Accelerate () так, чтобы он выдавал и такое готовое исключение из библиотеки базовых классов, как ArgumentOutOf RangeException, в случае передачи недействительного параметра (которым будет считаться любое значение меньше нуля). Обратите внимание, что конструктор этого класса исключения принимает имя проблемного аргумента в качестве первого параметра string, следом за которым идет сообщение с описанием ошибки: // Выполнение проверки аргумента на предмет действительности перед продолжением. public void Accelerate (int delta) { if(delta < 0) // Скорость должна быть больше нуля! throw new ArgumentOutOfRangeException("delta", "Speed must be greater than zero1"); IK extern
Глава 7. Структурированная обработка исключений 283 Теперь можно модифицировать логику в блоке catch так, чтобы в ней предусматривалось специфическая реакция на исключение каждого типа: static void Main(string[] args) { Console.WriteLine("***** Handling Multiple Exceptions *****\n"); Car myCar = new Car ("Rusty", 90) ; try // Отслеживание исключения ArgumentOutOfRangeException. myCar.Accelerate(-10); catch (CarlsDeadException e) Console.WriteLine(e.Message); catch (ArgumentOutOfRangeException e) Console.WriteLine (e.Message); Console.ReadLine (); } При создании множества блоков catch следует иметь в виду, что в случае выдачи исключения оно будет обрабатываться "первым доступным" блоком catch. Чтобы рассмотреть пример, изменим предыдущую логику, добавив еще один блок catch, пытающийся обрабатывать все остальные исключения помимо CarlsDeadException и ArgumentOutOfRangeException за счет перехвата исключения обобщенного типа System.Exception, как показано ниже: // Этот код компилироваться не будет! static void Main(string[] args) { Console.WriteLine ("***** Handling Multiple Exceptions *****\n"); Car myCar = new Car ("Rusty", 90) ; try // Приведение в действие исключения ArgumentOutOfRangeException. myCar.Accelerate(-10); catch(Exception e) // Обработка всех остальных исключений? Console.WriteLine(e.Message); catch (CarlsDeadException e) Console.WriteLine(e.Message); catch (ArgumentOutOfRangeException e) Console.WriteLine(e.Message); Console.ReadLine(); } Такая логика по обработке исключений будет приводить к ошибкам на этапе компиляции. Проблема в том, что первый блок catch может обрабатывать любые исключения, унаследованные от System.Exception, в том числе, следовательно, исключения
284 Часть II. Главные конструкции программирования на С# типа CarlsDeadException и ArgumentOutOfRangeException. Из-за этого два последних блока catch получаются недостижимыми. При структурировании блоков catch необходимо помнить о том, что в первом блоке должно обрабатываться наиболее конкретное исключение (т.е. исключение максимально производного типа в цепочке наследования типов исключений), а в последнем блоке — наиболее общее (т.е. исключение базового типа в текущей цепочке наследования, каковым в данном случае является System.Exception). Таким образом, если необходимо определить блок catch, способный обрабатывать любые ошибки помимо CarlsDeadException и ArgumentOutOfRangeException, можно написать следующий код: // Этот код скомпилируется без проблем. static void Main(string[] args) { Console.WriteLine ("***** Handling Multiple Exceptions *****\n"); Car myCar = new Car ("Rusty", 90); try { // Приведение в действие исключения ArgumentOutOfRangeException. myCar.Accelerate (-10); } catch (CarlsDeadException e) { Console.WriteLine(e.Message); } catch (ArgumentOutOfRangeException e) { Console.WriteLine(e.Message); } // В этом блоке будут перехватываться любые другие исключения // помимо CarlsDeadException и ArgumentOutOfRangeException. catch (Exception e) { Console.WriteLine(e.Message); } Console.ReadLine(); } На заметку! Везде, где только возможно, следует отдавать предпочтение перехвату конкретных классов исключений, а не общих исключений типа System.Exception. Хотя поначалу может казаться, что это упрощает жизнь (поскольку охватывает все вещи, с которыми не хочется возиться), со временем из-за того, что обработка более серьезной ошибки не была напрямую предусмотрена в коде, могут возникать очень странные сбои во время выполнения. Не следует забывать о том, что последний блок catch, который отвечает за обработку исключений System.Exception, имеет тенденцию оказываться чрезвычайно общим. Общие операторы catch В С# поддерживается так называемый "общий" (универсальный) блок catch, в котором объект исключения, генерируемый тем или иным членом, явным образом не получается. // Общий блок catch. static void Main(string[] args) { Console.WriteLine("***** Handling Multiple Exceptions *****\n");
Глава 7. Структурированная обработка исключений 285 Car myCar = new Car ( "Rusty" , 90) ; try { myCar.Accelerate(90); } catch { Console.WriteLine("Something bad happened..."); } Console.ReadLine(); } Очевидно, что это не самый информативный способ обработки исключений, поскольку нет никакой возможности для получения более детальных сведений о возникшей ошибке (таких как имя метода, стек вызовов или специальное сообщение). Тем не менее, в С# все-таки можно применять конструкцию подобного рода, так как она может оказаться полезной, когда требуется обработать все ошибки в чрезвычайно общей манере. Передача исключений При перехвате исключения внутри блока try допускается передавать (rethrow) исключение вверх по стеку вызовов предшествующему вызывающему коду. Для этого достаточно воспользоваться в блоке catch ключевым словом throw. Это позволит передать исключение вверх по цепочке логики вызовов, что может оказаться полезным, если блок catch способен обрабатывать текущую ошибку только частично. // Передача ответственности. static void Main(string[] args) { try { // Логика, касающаяся увеличения скорости // автомобиля... } catch(CarlsDeadException e) { // Выполнение любой частичной обработки данной ошибки //и передача дальнейшей ответственности. throw; } } Следует иметь в виду, что в приведенном примере кода конечным получателем исключения CarlsDeadException будет CLR-среда из-за его передачи в методе Main (). Следовательно, конечному пользователю будет отображаться системное диалоговое окно с информацией об ошибке. Обычно передача частичного обработанного исключения вызывающему коду осуществляется только в случае, если он способен обрабатывать поступающее исключение более элегантно. Обратите внимание на неявную передачу объекта CarlsDeadException и на применение ключевого слова throw без аргументов. Никакого нового объекта исключения не создается, а производится просто передача самого исходного объекта исключения (со всей его исходной информацией). Это позволяет сохранить контекст первоначального целевого объекта.
286 Часть II. Главные конструкции программирования на С# Внутренние исключения Нетрудно догадаться, что вполне возможно генерировать исключение во время обработки какого-то другого исключения. Например, предположим, что производится обработка исключения CarlsDeadException в определенном блоке catch, и в ходе этого процесса обработки предпринимается попытка записать данные трассировки стека в файл carErrors.txt на диске С: (для получения доступа к таким ориентированным на работу с вводом-выводом типам в директиве using должно быть указано пространство имен System. 10). catch(CarlsDeadException e) { // Попытка открыть файл carErrors.txt на диске С: FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open); } Теперь, если указанный файл на диске С: отсутствует, вызов File. Open () приведет к генерации исключения FileNotFoundException. Позже в книге будет более подробно рассказываться о пространстве имен System. 10 и о том, как программно определить, существует ли файл на жестком диске, перед тем как пытаться открыть его (это позволит вообще избежать генерации исключения). Тем не менее, чтобы не отходить от темы исключений, давайте считать, что такое исключение все-таки генерируется. Если во время обработки исключения возникает какое-то другое исключение, согласно наилучшим практическим рекомендациям, необходимо сохранить новый объект исключения как "внутреннее исключение" в новом объекте того же типа, что у исходного исключения. Причина, по которой необходимо выделять новый объект для обрабатываемого исключения, связана с тем, что документировать внутреннее исключение допускается только через параметр конструктора. Рассмотрим следующий код: catch (CarlsDeadException e) { try { FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open); } catch (Exception e2) { // Генерация исключения, записывающего новое // исключение, а также сообщение первого исключения. throw new CarlsDeadException(e.Message, e2) ; } } В данном случае важно обратить внимание, что конструктору CarlsDeadException в качестве второго параметра передается объект FileNotFoundException. После настройки этоп объект передается вверх по стеку вызовов следующему вызывающему коду, которым будет метод Main (). Поскольку после Main () никакого "следующего вызывающего кода", который мог бы перехватить исключение, не существует, пользователю будет отображаться системное диалоговое окно с сообщением об ошибке. Во многом подобно передаче исключения, запись внутренних исключений обычно осуществляется только тогда, когда вызывающий код способен обрабатывать данное исключение более элегантно. В этом случае в вызывающем коде внутри catch может использоваться свойство InnerException для извлечения деталей объекта внутреннего исключения.
Глава 7. Структурированная обработка исключений 287 Блок finally В контексте try/catch можно также определять необязательный блок finally. Это гарантирует, что некоторый набор операторов будет выполняться всегда, независимо от того, возникло исключение (любого типа) или нет. Для целей иллюстрации предположим, что перед выходом из метода Main () должно всегда производиться выключение радиоприемника в автомобиле, невзирая ни на какие обрабатываемые исключения. static void Main(string[] args) { Console.WriteLine ("***** Handling Multiple Exceptions *****\n"); Car myCar = new Car ("Rusty", 90); try // Логика, связанная с увеличением скорости автомобиля. catch(CarlsDeadException e) // Обработка исключения CarlsDeadException. catch(ArgumentOutOfRangeException e) // Обработка исключения ArgumentOutOfRangeException. catch(Exception e) // Обработка любых других исключений. finally // Это код будет выполняться всегда, независимо //от того, будет возникать и обрабатываться // какое-нибудь исключение или нет. myCar.CrankTunes(false); } Console.ReadLine(); } Если бы не был добавлен блок finally, тогда в случае возникновения любого исключения радиоприемник не выключался бы (что может как быть, так и не быть проблемой). В более реалистичном сценарии, когда необходимо удалить объекты, закрыть файл, отключиться от базы данных (или чего-то подобного), блок finally представляет собой идеальное место для выполнения надлежащей очистки. Какие исключения могут выдавать методы Из-за того, что каждый метод в .NET Framework может генерировать любое количество исключений (в зависимости от обстоятельств), возникает вполне логичный вопрос о том, как узнать, какие исключения может выдавать тот или иной метод из библиотеки базовых классов? Ответ прост: нужно заглянуть в документацию .NET Framework 4.0 SDK. В этой документации вместе с описанием каждого метода перечислены и все исключения, которые он может генерировать. В качестве более быстрой альтернативы, для просмотра списка всех исключений, которые способен выдавать тот или иной член библиотеки базовых классов, можно воспользоваться Visual Studio 2010, просто наведя курсор мыши на имя интересующего члена в окне кода, как показано на рис. 7.2.
288 Насть II. Главные конструкции программирования на С# ^Ш Program.cs X В 1 J$ProcessMultiple£xceptions.Program ^*Mam(stringQ args) • j finally { // This will always occur. Exception or not. myCar.CrankTunes(false); } т Console. ReadL.'.ne (); } } Л * 1 string Console.ReadLineQ Reads the next tine of characters from the standard input stream. Exceptions: SystemJOJOException System.OutOfMemoryException 5ystem.ArgurnentOutOfRange£xceptiori ► - Рис. 7.2. Просмотр списка исключений, которые может генерировать определенный метод Тем, кто перешел на .NET с Java, важно понять, что в .NET члены типов не про- тотипируются с набором исключений, которые они могут выдавать (другими словами, контролируемые исключения в С# не поддерживаются). Хорошо это или плохо, но обрабатывать каждое исключение, генерируемое определенным членом, не требуется. К чему приводят необрабатываемые исключения К этому моменту наверняка возник вопрос о том, что произойдет, если выданное исключение обработано не будет? Для примера предположим, что в логике внутри Main () объект Саг разгоняется до скорости, превышающей допустимый максимальный предел, и что логика try/catch отсутствует: static void Main(string [ ] args) { Console.WriteLine ("***** Handling Multiple Exceptions *****\n»); Car myCar = new Car ("Rusty", 90) ; myCar.Accelerate E00); Console.ReadLine(); } Игнорирование исключения в таком случае станет серьезным препятствием для конечного пользователя приложения, поскольку ему будет отображаться диалоговое окно с сообщением о необработанном исключении, показанное на рис. 7.3. ProcessMultipleExceptions has stopped working A problem caused the program to stop working correctly. Windows will close the program and notify you if a solution is available. Debug Close program Рис. 7.З. Последствия не обрабатывания исключений
Глава 7. Структурированная обработка исключений 289 Отладка необработанных исключений с помощью Visual Studio Среда Visual-Studio 2010 предлагает набор инструментов, которые помогают отлаживать необработанные специальные исключения. Для примера предположим, что скорость объекта Саг была увеличена до предела, превышающего допустимый максимум. После запуска сеанса отладки в Visual Studio 2010 (выбором в меню Debug (Отладка) пункта Start (Начать)), выполнение программы на месте выдачи не перехватываемого исключения будет автоматически прерываться. Кроме того, откроется окно (рис. 7.4), в котором будет отображаться значение свойства Message. Cw.cs X I JfProcessMultipleExcepbons.Car * Accelerator* delta) else // We need to call the HelpLink property, thus we need // to create a local variable before throwing the Exception object. л ' I CarhOeadExceptlonwasunhandled "You have а 15^^°°*'М1^^^^ ex. Helpline" "http://J ** throw ex- у has overheated! Console.Wr ffcuaty has overheated!"} "You have a lead foot* {10/11/2009 9:i6r00PM} й! Search for more Help Online... View Detail... Copy exception detail to the clipboard Рис. 7.4. Отладка необработанных специальных исключений в Visual Studio 210 На заметку! Если обработать исключение, сгенерированное каким-то методом из библиотеки базовых классов .NET не удается, отладчик Visual Studio 2010 прерывает выполнение программы на операторе, который вызвал этот проблемный метод. Щелкнув в этом окне на ссылке View Detail (Показать подробности), можно увидеть дополнительные сведения о состоянии объекта (рис. 7.5). Exception snapshot л ProcessMurtipleExceptions.CarkDeadExceptU {"Rusty has overheated!"} [ProcessMukipleExceptionsXarlsDeadExc {"Rusty has overheated!"} CauseOfError You have a lead foot Data {System.Collections.ListDictionarylnternal} ErrorTimeStamp {10/11/2009 9:48:25 PM} BNH H http://www.CarsRUs.com :▼] InnerException null Message Rusty has overheated! Source ProceuMurtipleExceptions StackTrace at ProcessMultipleExceptions.Car.Accelerate(Int32 • TergetSite {Void Accelerate(lnt32)} Рис. 7.5. Просмотр детальной информации об исключении Исходный код. Проект ProcessMultipleExceptions доступен в подкаталоге Chapter 7.
290 Часть II. Главные конструкции программирования на С# Несколько слов об исключениях, связанных с поврежденным состоянием (Corrupted State Exceptions) В завершении изучения предлагаемой в С# поддержки для структурированной обработки исключений следует упомянуть о появлении в .NET 4.0 совершенно нового пространства имен под названием System.Runtime.ExceptionServices (которое поставляется в составе сборки mscorlib.dll). Это довольно небольшое пространство имен включает в себя всего два типа класса, которые могут применяться, когда необходимо снабдить различные методы в приложении (вроде Main ()) возможностью перехвата и обработки "исключений, связанных с поврежденным состоянием" (Corrupted State Exceptions — CSEZ). Как говорилось в главе 1, платформа .NET всегда размещается в среде обслуживающей операционной системы (такой как Microsoft Windows). Имея опыт программирования приложений для Windows, можно вспомнить, что низкоуровневый API-интерфейс Windows обладает очень уникальным набором правил по обработке ошибок времени выполнения, которые немного похожи на предлагаемые в .NET приемы структурированной обработки исключений. В API-интерфейсе Windows можно перехватывать ошибки чрезвычайно низкого уровня, которые как раз и представляют собой ошибки, связанные с "поврежденным состоянием". Попросту говоря, если ОС Windows передает такую ошибку, это означает, что с программой что-то серьезно не так, причем настолько, что нет никакой надежды на восстановление, и единственно возможной мерой является завершение ее работы. На заметку! При работе с .NET ошибка CSE может появиться только в случае использования в коде С# служб вызова платформы (для непосредственного взаимодействия с API-интерфейсом Windows) или применения поддерживаемого в С# синтаксиса указателей (см. главу 12). До выхода версии .NET 4.0 подобные низкоуровневые ошибки, специфичные для операционной системы, можно было перехватывать только с помощью блока catch, предусматривающего перехват общих исключений System.Exception. Однако с этим подходом была связана проблема: если каким-то образом возникало исключение CSE, которое перехватывалось в таком блоке catch, в .NET не предлагалось (и до сих пор не предлагается) никакого элегантного кода для восстановления. Теперь, с выходом версии .NET 4.0, среда CLR больше не разрешает автоматический перехват исключений CSE в приложениях .NET. В большинстве случаев это именно то поведение, которое нужно. Если же необходимо получать уведомления о таких ошибках уровня ОС (обычно при использовании унаследованного кода, нуждающегося в таких уведомлениях), понадобится применять атрибут [HandledProcessCorruptedState Exceptions]. Хотя роль атрибутов в .NET рассматривается позже в книге (в главе 15), сейчас важно понять, что данный атрибут может применяться к любому методу в приложении, и в результате его применения соответствующий метод получит возможность иметь дело с подобными низкоуровневыми ошибками, специфическими для операционной системы. Чтобы увидеть хотя бы простой пример, давайте создадим следующий метод Main (), не забыв перед этим импортировать в файл кода С# пространство имен System.Runtime. ExceptionServices: [HandledProcessCorruptedStateExceptions] static int Main(string[ ] args) {
Глава 7. Структурированная обработка исключений 291 try { // Предполагаем, что в Ма±п() вызывается метод, // который отвечает за выполнение всей программы. RunMyApplication(); } catch (Exception ex) { // Если мы добрались сюда, значит, что-то произошло. // Поэтому просто отображаем сообщение //и выходим из программы. Console.WriteLine("Ack! Huge problem: {0}", ex.Message); return -1; } return 0; } Задача приведенного выше метода Main () практически сводится только к вызову второго метода, отвечающего за выполнение всего приложения. В данном примере будем полагать, что в этом втором методе RunMyApplication () интенсивно используется логика try/catch для обработки любой ожидаемой ошибки. Поскольку метод Main () был помечен атрибутом [HandledProcessCorruptedStateExceptions], в случае возникновения ошибки CSE перехват System.Exception получается последним шансом сделать хоть что-то перед завершением работы программы. Метод Main () здесь возвращает значение int, а не void. Как объяснялось в главе 3, по соглашению возврат операционной системе нулевого значения свидетельствует о завершении работы приложения без ошибок, в то время как возврат любого другого значения (обычно отрицательного числа) — о том, что в ходе его выполнения возникла какая-то ошибка. В настоящей книге обработка подобных низкоуровневых ошибок, специфических для операционной системы Windows, рассматриваться не будет, а потому и о роли System. Runtime .ExceptionServices тоже подробно рассказываться не будет. Исчерпывающие сведения по этому поводу можно найти в документации .NET 4.0 Framework SDK. Резюме В этой главе была показана роль, которую играет структурированная обработка исключений. Если необходимо, чтобы из метода вызывающему коду отправлялся объект, описывающий ошибку, в методе нужно выделить, сконфигурировать и выдать конкретное исключение производного от System.Exception типа с применением такого поддерживаемого в С# ключевого слова, как throw. В вызывающем коде любые поступающие исключения обрабатываются с помощью ключевого слова catch и необязательного блока finally. Для получения собственных специальных исключений, по сути, требуется создать класс, унаследованный от класса System.ApplicationException. Этот новый класс будет представлять исключения, генерируемые приложением, которое выполняется в настоящий момент. Объекты ошибок, унаследованные от System. SystemException, в свою очередь, позволяют представлять критические (и фатальные) ошибки, которые выдает CLR. Напоследок в главе были продемонстрированы различные инструменты внутри Visual Studio 2010, которые можно применять для создания специальных исключений (в соответствии с наилучшими практическими рекомендациями .NET), а также для их отладки.
ГЛАВА 8 Время жизни объектов К этому моменту было предоставлено немало сведений о создании специальных типов классов в С#. Теперь речь пойдет о том, как CLR-среда управляет размещенными экземплярами классов (т.е. объектами) с помощью процесса сборки мусора (garbage collection). Программистам на С# никогда не приходится непосредственно удалять управляемый объект из памяти (в языке С# нет даже ключевого слова вроде delete). Вместо этого объекты .NET размещаются в области памяти, которая называется управляемой кучей (managed heap), откуда они автоматически удаляются сборщиком мусора, когда наступает "определенный момент в будущем". После рассмотрения ключевых деталей процесса сборки мусора в настоящей главе будет показано, как программно взаимодействовать со сборщиком мусора с помощью класса System.GC, и как с применением виртуального метода System.Object. Finalize () и интерфейса IDisposable создавать классы, способные освобождать внутренние неуправляемые ресурсы в определенное время. Кроме того, будут описаны некоторые новые функциональные возможности сборщика мусора, появившиеся в версии .NET 4.0, включая фоновую сборку мусора и отложенную (ленивую) инициализацию с использованием обобщенного класса System.Lazyo. После изучения материалов настоящей главы должно появиться вполне четкое представление об управлении объектами .NET в среде CLR. Классы, объекты и ссылки Перед изучением тем, излагаемых в настоящей главе, сначала необходимо немного больше прояснить различие между классами, объектами и ссылками. Вспомните, что класс представляет собой ни что иное, как схему, которая описывает то, каким образом экземпляр данного типа должен выглядеть и вести себя в памяти. Определяются классы в файлах кода (которым по соглашению назначается расширение *. cs). Для примера создадим новый проект типа Console Application (Консольное приложение) на С# по имени SimpleGC и определим в нем следующий простой класс Саг: // Содержимое файла Car.cs public class Car { public int CurrentSpeed {get; set;} public string Petllame {get; set; } public Car () {} public Car(string name, int speed) { PetName = name; CurrentSpeed = speed; }
Глава 8. Время жизни объектов 293 public override string ToStringO { return string.Format ( "{0 } is going {1} MPH", petName, currSp); PetName, CurrentSpeed); } } Как только класс определен, с использованием ключевого слова new, поддерживаемого в С#, можно размещать в памяти любое количество его объектов. Однако при этом следует помнить, что ключевое слово new возвращает ссылку на объект в куче, а не фактический объект. Если ссылочная переменная объявляется как локальная переменная в контексте метода, она сохраняется в стеке для дальнейшего использования в приложении. Для вызова членов объекта к сохраненной ссылке должна применяться операция точки С#. class Program { static void Main(string[] args) { Console.WriteLine ("***** GC Basics *****"); // Создание нового объекта Car в управляемой куче. // Возвращается ссылка на этот объект (refToMyCar). Car refToMyCar = new Car ( "Zippy", 50); // Применение к переменной с этой ссылкой С#-операции // точки (.)для вызова членов данного объекта. Console.WriteLine(refToMyCar.ToString ()); Console.ReadLine(); На рис. 8.1 схематично показаны отношения между классами, объектами и ссылками на них. Стек refToMyCar Управляемая куча „ (Объект} V Car J Рис. 8.1. Ссылки на объекты в управляемой куче На заметку! Вспомните из главы 4, что структуры представляют собой типы значения, которые всегда размещаются прямо в стеке и никогда не попадают в управляемую кучу .NET. Размещение в куче происходит только при создании экземпляров классов. Базовые сведения о времени жизни объектов При создании приложений на С# можно смело полагать, что исполняющая среда .NET будет сама заботиться об управляемой куче без непосредственного вмешательства со стороны программиста. На самом деле "золотое правило" по управлению памятью в .NET звучит просто.
294 Часть II. Главные конструкции программирования на С# Правило. Размещайте объект в управляемой куче с использованием ключевого слова new и забывайте об этом. После создания объект будет автоматически удален сборщиком мусора тогда, когда в нем отпадет необходимость. Разумеется, возникает вопрос о том, каким образом сборщик мусора определяет момент, когда в объекте отпадает необходимость? В двух словах на этот вопрос можно ответить так: сборщик мусора удаляет объект из кучи тогда, когда тот становится недостижимым ни в одной части программного кода. Например, добавим в класс Program метод, который размещает в памяти объект Саг: public static void MakeACarO { // Если myCar является единственной ссыпкой на объект // Саг, тогда при возврате результата данным // методом объект Саг *может* быть уничтожен. Car myCar = new Car(); } Обратите внимание, что ссылка на объект Car (myCar) была создана непосредственно внутри метода MakeACar () и не передавалась за пределы определяющей области действия (ни в виде возвращаемого значения, ни в виде параметров ref /out). Поэтому после завершения вызова данного метода ссылка myCar окажется недостижимой, а объект Саг, соответственно — кандидатом на удаление сборщиком мусора. Следует, однако, понять, что иметь полную уверенность в немедленном удалении этого объекта из памяти сразу же после выполнения метода MakeACar () нельзя. Все, что в данный момент можно предполагать, так это то, что когда в CLR-среде будет в следующий раз производиться сборка мусора, объект myCar может подпасть под процесс уничтожения. Как станет ясно со временем, программирование в среде с автоматической сборкой мусора значительно облегчает разработку приложений. Программистам на C++ хорошо известно, что если они специально не позаботятся об удалении размещаемых в куче объектов, вскоре обязательно начнут возникать "утечки памяти". На самом деле отслеживание проблем, связанных с утечкой памяти, является одним из самых длительных (и утомительных) аспектов программирования в неуправляемых средах. Благодаря назначению ответственным за уничтожение объектов сборщика мусора, обязанности по управлению памятью, по сути, сняты с плеч программиста и возложены на CLR-среду. На заметку! Тем, кто ранее применял для разработки приложений технологию СОМ, следует знать, что в .NET объекты не снабжаются никаким внутренним счетчиком ссылок и потому не поддерживают использования методов вроде AddRef () или Release (). CIL-код, генерируемый для ключевого слова new При обнаружении ключевого слова new компилятор С# вставляет в реализацию метода CIL-инструкцию newob j. Если скомпилировать текущий пример кода и заглянуть в полученную сборку с помощью утилиты ildasm.exe, то можно обнаружить внутри метода MakeACar () следующие CIL-операторы: .method private hidebysig static void MakeACarO cil managed { // Code size 8 @x8) // Размер кода 8 @x8) .maxstack 1
Глава 8. Время жизни объектов 295 .locals init ([0] class SimpleGC.Car) IL_0000: nop IL_0001: newobj instance void SimpleGC.Car::.ctor () IL_0006: stloc.O IL_0007: ret } // end of method Program::MakeACar // конец метода Program::MakeACar Прежде чем ознакомиться с точными правилами, которые определяют момент, когда объект должен удаляться из управляемой кучи, давайте более подробно рассмотрим роль CIL-инструкции newobj. Дна начала важно понять, что управляемая куча представляет собой нечто большее, чем просто случайный фрагмент памяти, к которому CLR получает доступ. Сборщик мусора .NET "убирает" кучу довольно тщательно, причем (при необходимости) даже сжимает пустые блоки памяти с целью оптимизации. Чтобы ему было легче это делать, в управляемой куче поддерживается указатель (обычно называемый указателем на следующий объект или указателем на новый объект), который показывает, где точно будет размещаться следующий объект. Таким образом, инструкция newobj заставляет CLR-среду выполнить перечисленные ниже ключевые операции. • Вычислить, сколько всего памяти требуется для размещения объекта (в том числе памяти, необходимой для членов данных и базовых классов). • Проверить, действительно ли в управляемой куче хватает места для обслуживания размещаемого объекта. Если хватает, вызвать указанный конструктор и вернуть вызывающему коду ссылку на новый объект в памяти, адрес которого совпадает с последней позицией указателя на следующий объект. • И, наконец, перед возвратом ссылки вызывающему коду переместить указатель на следующий объект, чтобы он указывал на следующую доступную позицию в управляемой куче. Весь описанный процесс схематично изображен на рис. 8.2. static void Main(string[] args) { Car cl = new Car(); Car c2 = new Car(); Управляемая куча Cl C2 Указатель на следующий объект Рис. 8.2. Детали размещения объектов в управляемой куче Из-за постоянного размещения объектов приложением пространство в управляемой куче может со временем заполниться. В случае если при обработке следующей инструкции newob] среда CLR обнаруживает, что в управляемой куче не хватает пространства для размещения запрашиваемого типа, она приступает к сборке мусора и тем самым пытается освободить хоть сколько-то памяти. Поэтому следующее правило, касающееся сборки мусора, тоже звучит довольно просто. Правило. В случае нехватки в управляемой куче пространства для размещения запрашиваемого объекта начинает выполняться сборка мусора.
296 Часть II. Главные конструкции программирования на С# Однако то, каким именно образом начнет выполняться сборка мусора, зависит от версии .NET, под управлением которой функционирует приложение. Различия будут описаны позже в настоящей главе. Установка объектных ссылок в null Если ранее приходилось создавать СОМ-объекты в Visual Basic 6.0, то должно быть известно, что по завершении их использования предпочтительнее устанавливать эти ссылки в Nothing. На внутреннем уровне счетчик ссылок на объект СОМ уменьшалось на единицу, и когда он становился равным нулю, объект можно было удалять из памяти. Аналогичным образом программисты на C/C++ часто предпочитают устанавливать для переменных указателей значение null, гарантируя, что они больше не будут ссылаться на неуправляемую память. Из-за упомянутых фактов, вполне естественно, может возникнуть вопрос о том, что же происходит в С# после установки объектных ссылок в null. Для примера изменим метод MakeACar () следующим образом: static void MakeACar () { Car myCar = new Car () ; myCar = null; } Когда объектные ссылки устанавливаются в null, компилятор С# генерирует CIL- код, который заботится о том, чтобы ссылка (в рассматриваемом примере myCar) больше не ссылалась ни на какой объект. Если теперь снова воспользоваться утилитой ildasm. ехе и заглянуть с ее помощью в CIL-код измененного метода MakeACar (), можно обнаружить в нем код операции ldnull (который заталкивает значение null в виртуальный стек выполнения) со следующим за ним кодом операции stloc.O (который присваивает переменной ссылку null): .method private hidebysig static void MakeACar () cil managed { // Code size 10 (Oxa) // Размер кода 10 (Oxa) .maxstack 1 .locals mit ([0] class SimpleGC.Car myCar) IL_0000: nop IL_0001: newob] instance void SimpleGC.Car::.ctor() IL_0006: stloc.O IL_0007: ldnull IL_0008: stloc.O IL_0009: ret } // end of method Program::MakeACar // конец метода Program::MakeACar Однако обязательно следует понять, что установка ссылки в null никоим образом не вынуждает сборщик мусора немедленно приступить к делу и удалить объект из кучи, а просто позволяет явно разорвать связь между ссылкой и объектом, на который она ранее указывала. Благодаря этому, присваивание ссылкам значения null в С# имеет гораздо меньше последствий, чем в других языках на базе С (или VB 6.0), и совершенно точно не будет причинять никакого вреда.
Глава 8. Время жизни объектов 297 Роль корневых элементов приложения Теперь снова вернемся к вопросу о том, каким образом сборщик мусора определяет момент, когда объект уже более не нужен. Чтобы разобраться в стоящих за этим деталях, необходимо знать, что собой представляет корневые элементы приложения (application roots). Попросту говоря, корневым элементом (root) называется ячейка в памяти, в которой содержится ссылка на размещающийся в куче объект. Строго говоря, корневыми могут называться элементы любой из перечисленных ниже категорий. • Ссылки на глобальные объекты (хотя в С# они не разрешены, CIL-код позволяет размещать глобальные объекты). • Ссылки на любые статические объекты или статические поля. • Ссылки на локальные объекты в пределах кодовой базы приложения. • Ссылки на передаваемые методу параметры объектов. • Ссылки на объекты, ожидающие финализации (об этом подробно рассказываться далее в главе). • Любые регистры центрального процессора, которые ссылаются на объект. Во время процесса сборки мусора исполняющая среда будет исследовать объекты в управляемой куче, чтобы определить, являются ли они по-прежнему достижимыми (т.е. корневыми) для приложения. Для этого среда CLR будет создавать графы объектов, представляющие все достижимые для приложения объекты в куче. Более подробно объектные графы будут описаны при рассмотрении процесса сериализации объектов в главе 20. Пока главное усвоить то, что графы применяются для документирования всех достижимых объектов. Кроме того, следует иметь в виду, что сборщик мусора никогда не будет создавать граф для одного и того же объекта дважды, избегая необходимости выполнения подсчета циклических ссылок, который характерен для программирования в среде СОМ. Чтобы увидеть все это на примере, предположим, что в управляемой куче содержится набор объектов с именами A, B,C,D,E,FhG. Во время сборки мусора эти объекты (а также любые внутренние объектные ссылки, которые они могут содержать) будут исследованы на предмет наличия у них активных корневых элементов. После построения графа все недостижимые объекты (которыми в примере пусть будут объекты С и F) помечаются как являющиеся мусором. На рис. 8.3 показано, как примерно выглядит граф объектов в только что описанном сценарии (линии со стрелками следует воспринимать как "зависит от" или "требует"; например, "Е зависит от G и В", 4А не зависит ни от чего" и т.д.). После того как объект помечен для уничтожения (в данном случае это объекты С и F, поскольку в графе объектов они во внимание не принимаются), они будут удалены из памяти. Оставшееся пространство в куче будет после этого сжиматься до компактного состояния, что, в свою очередь, вынудит CLR изменить набор активных корневых элементов приложения (и лежащих в их основе указателей) так, чтобы они ссылались на правильное место в памяти (это делается автоматически и прозрачно). И, наконец, указатель на следующий объект тоже будет подстраиваться так, чтобы указывать на следующий доступный участок памяти. На рис. 8.4 показано, как выглядит конечный результат этих изменений в рассматриваемом сценарии. На заметку! Собственно говоря, сборщик мусора использует две отдельных кучи, одна из которых предназначена специально для хранения очень больших объектов. Доступ к этой куче во время сборки мусора получается реже из-за возможных последствий в плане производительности, в которые может выливаться изменение места размещения больших объектов. Невзирая на этот факт, управляемая куча все равно может спокойно считаться единой областью памяти.
298 Часть II. Главные конструкции программирования на С# Управляемая куча А В С D Е F G Указатель на следующий объект 0 (№ Рис. 8.3. Графы объектов создаются для определения объектов, достижимых для корневых элементов приложения Управляемая куча А В D Е G Указатель на следующий объект Рис. 8.4. Очищенная и сжатая до компактного состояния куча Поколения объектов При попытке обнаружить недостижимые объекты CLR-среда не проверяет буквально каждый находящийся в куче объект. Очевидно, что на это уходила бы масса времени, особенно в более крупных (реальных) приложениях. Для оптимизации процесса каждый объект в куче относится к определенному "поколению". Смысл в применении поколений выглядит довольно просто: чем дольше объект находится в куче, тем выше вероятность того, что он там и будет оставаться. Например, класс, определенный в главном окне настольного приложения, будет оставаться в памяти вплоть до завершения выполнения программы. С другой стороны, объекты, которые были размещены в куче лишь недавно (как, например, те, что находятся в пределах области действия метода), вероятнее всего будут становиться недостижимым довольно быстро. Исходя из этих предположений, каждый объект в куче относится к одному из перечисленных ниже поколений. • Поколение О. Идентифицирует новый только что размещенный объект, который еще никогда не помечался как подлежащий удалению в процессе сборки мусора. • Поколение 1. Идентифицирует объект, который уже "пережил" один процесс сборки мусора (был помечен как подлежащий удалению в процессе сборки мусора, но не был удален из-за наличия достаточного места в куче). • Поколение 2. Идентифицирует объект, которому удалось пережить более одного прогона сборщика мусора.
Глава 8. Время жизни объектов 299 На заметку! Поколения 0 и 1 называются эфемерными (недолговечными). В следующем разделе будет показано, что в ходе процесса сборки мусора эфемерные поколения действительно обрабатываются по-другому. Сборщик мусора сначала анализирует все объекты, которые относятся к поколению 0. Если после их удаления остается достаточное количество памяти, статус всех остальных (уцелевших) объектов повышается до поколения 1. Чтобы увидеть, как поколение, к которому относится объект, влияет на процесс сборки мусора, обратите внимание на рис. 8.5, где схематически показано, как набору уцелевших объектов поколения О (А, В и Е) назначается статус объектов следующего поколения после освобождения требуемого объема памяти. Поколение О А В С D Е F G Поколение 1 U №- А В Е Рис. 8.5. Объектам поколения 0, которые уцелели после сборки мусора, назначается статус объектов поколения 1 Если все объекты поколения 0 уже были проверены, но все равно требуется дополнительное пространство, проверяться на предмет достижимости и подвергаться процессу сборки мусора начинают объекты поколения 1. Объектам поколения 1, которым удалось уцелеть после этого процесса, затем назначается статус объектов поколения 2. Если же сборщику мусора все равно требуется дополнительная память, тогда на предмет достижимости начинают проверяться и объекты поколения 2. Объектам, которым удается пережить сборку мусора на этом этапе, оставляется статус объектов поколения 2, поскольку более высокие поколения просто не поддерживаются. Из всего вышесказанного важно сделать следующий вывод: из-за отнесения объектов в куче к определенному поколению, более новые объекты (вроде локальных переменных) будут удаляться быстрее, а более старые (такие как объекты приложений) — реже. Параллельная сборка мусора в версиях .NET 1.0 - .NET 3.5 До выхода версии .NET 4.0 очистка неиспользуемых объектов в исполняющей среде производилась с применением техники параллельной сборки мусора. В этой модели при выполнении сборки мусора для любых объектов поколения 0 или 1 (т.е. эфемерных поколений) сборщик мусора временно приостанавливал все активные потоки внутри текущего процесса, чтобы приложение не могло получить доступ к управляемой куче вплоть до завершения процесса сборки мусора. Потоки более подробно рассматриваются в главе 19, а пока можно считать поток просто одним из путей выполнения внутри функционирующей программы. По заверше-
300 Часть II. Главные конструкции программирования на С# нии цикла сборки мусора приостановленным потокам разрешалось снова продолжать работу. К счастью, в .NET 3.5 (и предшествующих версиях) сборщик мусора был хорошо оптимизирован и потому связанные с этим короткие перерывы в работе приложения редко становились заметными (а то и вообще никогда). Как и оптимизация, параллельная сборка мусора позволяла производить очистку объектов, которые не были обнаружены ни в одном из эфемерных поколений, в отдельном потоке. Это сокращало (но не устраняло) необходимость в приостановке активных потоков исполняющей средой .NET. Более того, параллельная сборка мусора позволяла программам продолжать размещать объекты в куче во время сборки объектов не эфемерных поколений. Фоновая сборка мусора в версии .NET 4.0 В .NET 4.0 сборщик мусора по-другому решает вопрос с приостановкой потоков при очистке объектов в управляемой куче, используя при этом технику фоновой сборки мусора. Несмотря на ее название, это вовсе не означает, что вся сборка мусора теперь происходит в дополнительных фоновых потоках выполнения. На самом деле в случае фоновой сборки мусора для объектов, относящихся к не эфемерному поколению, исполняющая среда .NET теперь может производить сборку объектов эфемерных поколений в отдельном фоновом потоке. Механизм сборки мусора в .NET 4.0 был улучшен так, чтобы на приостановку потока, связанного с деталями сборки мусора, требовалось меньше времени. Благодаря этим изменениям, процесс очистки неиспользуемых объектов поколения 0 или 1 стал оптимальным. Он позволяет получать более высокие показатели по производительности приложений (что действительно важно для систем, работающих в реальном времени и нуждающихся в небольших и предсказуемых перерывах на сборку мусора). Однако следует понимать, что ввод такой новой модели сборки мусора никоим образом не отражается на способе построения приложений .NET Теперь практически всегда можно просто позволять сборщику мусора .NET выполнять работу без непосредственного вмешательства со своей стороны (и радоваться тому, что разработчики в Microsoft продолжают улучшать процесс сборки мусора прозрачным образом). Тип System.GC В библиотеках базовых классов доступен класс по имени System. GC, который позволяет программно взаимодействовать со сборщиком мусора за счет обращения к его статическим членам. Необходимость в непосредственном использовании этого класса в разрабатываемом коде возникает крайне редко (а то и вообще никогда). Обычно единственным случаем, когда нужно применять члены System. GC, является создание классов, предусматривающих использование на внутреннем уровне неуправляемых ресурсов. Это может быть, например, класс, работающий с основанным на С интерфейсом Windows API за счет применения протокола вызовов платформы .NET, или какая-то низкоуровневая и сложная логика взаимодействия с СОМ. В табл. 8.1 приведено краткое описание некоторых наиболее интересных членов класса System.GC (полные сведения можно найти в документации .NET Framework 4.0 SDK). На заметку! В .NET 3.5 с пакетом обновлений Service Pack 1 появилась дополнительная возможность получать уведомления перед началом процесса сборки мусора за счет применения ряда новых членов. И хотя данная возможность может оказаться полезной в некоторых сценариях, в большинстве приложений она не нужна, поэтому здесь подробно не рассматривается. Всю необходимую информацию об уведомлениях подобного рода можно найти в разделе "Garbage Collection Notifications" ("Уведомления о сборке мусора") документации .NET Framework 4.0 SDK.
Глава 8. Время жизни объектов 301 Таблица 8.1. Некоторые члены класса System.gc Член Описание AddMemorуPressure(), RemoveMemoryPressure() Collect() CollectionCount() GetGeneration() GetTotalMemory() MaxGeneration SuppressFinalize() WaitForPendingFinalizers () Позволяют указывать числовое значение, отражающее "уровень срочности", который вызывающий объект применяет в отношении к сборке мусора. Следует иметь в виду, что эти методы должны изменять уровень давления в тандеме и, следовательно, никогда не устранять больше давления, чем было добавлено Заставляет сборщик мусора провести сборку мусора. Должен быть перегружен так, чтобы указывать, объекты какого поколения подлежат сборке, а также какой режим сборки использовать (с помощью перечисления GCCollectionMode) Возвращает числовое значение, показывающее, сколько раз объектам данного поколения удалось переживать процесс сборки мусора Возвращает информацию о том, к какому поколению в настоящий момент относится объект Возвращает информацию о том, какой объем памяти (в байтах) в настоящий момент занят в управляемой куче. Булевский параметр указывает, должен ли вызов сначала дождаться выполнения сборки мусора, прежде чем возвращать результат Возвращает информацию о том, сколько максимум поколений поддерживается в целевой системе. В .NET 4.0 поддерживается всего три поколения: 0, 1 и 2 Позволяет устанавливать флаг, указывающий, что для данного объекта не должен вызываться его метод Finalize () Позволяет приостанавливать выполнение текущего потока до тех пор, пока не будут финализированы все объекты, предусматривающие финализацию. Обычно вызывается сразу же после вызова метода GC. Collect () Рассмотрим применение System. GC для получения касающихся сборки мусора деталей на примере следующего метода Main (), в котором используются сразу несколько членов System. GC: static void Main(string [ ] args) { Console.WriteLine("***** Fun with System.GC *****"); // Вывод подсчитанного количества байтов в куче. Console.WriteLine("Estimates bytes on heap: {0}", GC.GetTotalMemory(false)); // Отсчет для MaxGeneration начинается с нуля, // поэтому для удобства добавляется 1. Console.WriteLine("This OS has @} object generations.\n", (GC.MaxGeneration + 1)); Car refToMyCar = new Car("Zippy", 100); Console.WriteLine(refToMyCar.ToString()); // Вывод информации о поколении объекта refToMyCar. Console.WriteLine("Generation of refToMyCar is: @ 1", GC.GetGeneration(refToMyCar)); Console.PeadLine ();
302 Часть II. Главные конструкции программирования на С# Принудительная активизация сборки мусора Сборщик мусора .NET предназначен в основном для того, чтобы управлять памятью вместо разработчиков. Однако в очень редких случаях требуется принудительно запустить сборку мусора с помощью метода GC.Collect(). Примеры таких ситуаций приведены ниже. • Приложение приступает к выполнению блока кода, прерывание которого возможным процессом сборки мусора является недопустимым. • Приложение только что закончило размещать чрезвычайно большое количество объектов и нуждается в как можно скорейшем освобождении большого объема памяти. Если выяснилось, что выполнение сборщиком мусора проверки на предмет наличия недостижимых объектов может быть выгодным, можно инициировать процесс сборки мусора явным образом, как показано ниже: static void Main(string [ ] args) { // Принудительная активизация процесса сборки мусора и // ожидание завершения финализации каждого из объектов. GC.Collect (); GC.WaitForPendingFinalizers (); } В случае принудительной активизации сборки мусора не забывайте вызвать метод GC. WaitForPendingFinalizers (). Это дает возможность всем финализируемым объектам (рассматриваются в следующем разделе) произвести любую необходимую очистку перед продолжением работы программы. Метод GC.WaitForPendingFinalizers () незаметно приостанавливает выполнение вызывающего "потока" во время процесса сборки мусора, что очень хорошо, поскольку исключает вероятность вызова в коде каких- либо методов на объекте, который в текущий момент уничтожается. Методу GC . Collect () можно передать числовое значение, отражающее старейшее поколение объектов, в отношении которого должен проводиться процесс сборки мусора. Например, чтобы CLR-среда анализировала только объекты поколения 0, необходимо использовать следующий код: static void Main(string [ ] args) { // Исследование только объектов поколения 0. GC.Collect @); GC.WaitForPendingFinalizers (); } Вдобавок методу Collect () во втором параметре может передаваться значение перечисления GCCollectionMode, которое позволяет более точно указать, каким образом исполняющая среда должна принудительно инициировать сборку мусора. Ниже показаны значения, доступные в этом перечислении: public enum GCCollectionMode { Default, // Текущим значением по умолчанию является Forced. Forced, // Указывает исполняющей среде начать сборку мусора немедленно1 Optimized // Позволяет исполняющей среде выяснить, оптимален //ли настоящий момент для удаления объектов. }
Глава 8. Время жизни объектов 303 Как и при любой сборке мусора, в случае вызова GC.CollectO уцелевшим объектам назначается статус объектов более высокого поколения. Чтобы удостовериться в этом, модифицируем метод Main () следующим образом: static void Main(string[] args) { Console.WriteLine ("***** Fun with System.GC *****"); // Отображение примерного количества байтов в куче. Console.WriteLine("Estimated bytes on heap: {0}", GC.GetTotalMemory(false)); // Отсчет значения MaxGeneration начинается с нуля. Console.WriteLine("This OS has {0} object generations.\n", (GC.MaxGeneration + 1) ) ; Car refToMyCar = new Car("Zippy", 100); Console.WriteLine(refToMyCar.ToString()); // Вывод информации о поколении, к которому // относится refToMyCar. Console.WriteLine ("\nGeneration of refToMyCar is: {0}", GC.GetGeneration(refToMyCar)); // Создание большого количества объектов для целей тестирования. object[] tonsOfObjects = new object[50000]; for (int i = 0; i < 50000; i++) tonsOfObjects [i] = new object (); // Выполнение сборки мусора в отношении только // объектов, относящихся к поколению 0. GC.Collect @, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // Вывод информации о поколении, к которому // относится refToMyCar. Console.WriteLine("\nGeneration of refToMyCar is: {0}", GC.GetGeneration(refToMyCar)); // Выполнение проверки, удалось ли // tonsOfObjects[9000] уцелеть // после сборки мусора. if (tonsOfObjects[9000] !=null) { // Вывод поколения tonsOfObjects [ 9000] . Console.WriteLine("Generation of tonsOfObjects[9000] is: {0}", GC.GetGeneration(tonsOfObjects [ 9000])); } else // tonsOfObjects[9000] больше не существует. Console.WriteLine("tonsOfObjects[9000] is no longer alive."); // Вывод информации о том, сколько раз в отношении // объектов каждого поколения выполнялась сборка мусора. Console.WriteLine("\nGen 0 has been swept {0} times", GC.CollectionCount@)); Console.WriteLine("Gen 1 has been swept {0} times", GC.CollectionCountA)); Console.WriteLine("Gen 2 has been swept {0} times", GC.CollectionCountB)); Console.ReadLine();
304 Часть II. Главные конструкции программирования на С# Дя целей тестирования был создан очень большой массив типа object (состоящий из 50 000 элементов). Как можно увидеть по приведенному ниже выводу, хотя в данном методе Main () и был сделан только один явный запрос на выполнение сборки мусора (с помощью метода GC.CollectO), среда CLR в фоновом режиме провела несколько таких сборок. ***** Fun with System.GC ***** Estimated bytes on heap: 70240 This OS has 3 object generations. Zippy is going 100 MPH Generation of refToMyCar is: 0 Generation of refToMyCar is: 1 Generation of tonsOfObjects [ 9000] is: 1 Gen 0 has been swept 1 times Gen 1 has been swept 0 times Gen 2 has been swept 0 times К этому моменту детали жизненного цикла объектов должны выглядеть более понятно. В следующем разделе исследование процесса сборки мусора продолжается, и будет показано, как создавать финализируемые (finalizable) и высвобождаемые (disposable) объекты. Очень важно отметить, что описанные далее приемы подходят только в случае построения управляемых классов, внутри которых используются неуправляемые ресурсы. Исходный код. Проект SimpleGC доступен в подкаталоге Chapter 8. Создание финализируемых объектов В главе 6 уже рассказывалось о том, что в самом главном базовом классе .NET — System.Object — имеется виртуальный метод по имени Finalize (). В предлагаемой по умолчанию реализации он ничего особенного не делает: // Класс System.Object public class Object { protected virtual void Finalize () {} } За счет его переопределения в специальных классах устанавливается специфическое место для выполнения любой необходимой данному типу логики по очистке. Из-за того, что метод Finalize () по определению является защищенным (protected), вызывать его напрямую из класса экземпляра с помощью операции точки не допускается. Вместо этого метод Finalize () (если он поддерживается) будет автоматически вызываться сборщиком мусора перед удалением соответствующего объекта из памяти. На заметку! Переопределять метод Finalize О в типах структур нельзя. Это вполне логичное ограничение, поскольку структуры представляют собой типы значения, которые изначально никогда не размещаются в управляемой памяти и, следовательно, никогда не подвергаются процессу сборки мусора. Однако в случае создания структуры, которая содержит ресурсы, нуждающиеся в очистке, вместо этого метода можно реализовать интерфейс iDisposable (рассматривается далее в главе). Разумеется, вызов метода Finalize () будет происходить (в конечном итоге) либо во время естественной активизации процесса сборки мусора, либо во время его принудительной активизации программным образом с помощью GC. Collect (). Помимо
Глава 8. Время жизни объектов 305 этого, финализатор типа будет автоматически вызываться и при выгрузке из памяти домена, который отвечает за обслуживание приложения. Некоторым по опыту работы с .NET уже может быть известно, что домены приложений (AppDomaln) применяются для обслуживания исполняемой сборки и любых необходимых внешних библиотек кода. Те, кто еще не знаком с этим понятием .NET, узнают все необходимое после прочтения главы 16. Пока что необходимо обратить внимание лишь на то, что при выгрузке домена приложения из памяти CLR-среда будет автоматически вызывать финализаторы для каждого финализируемого объекта, который был создан во время существования AppDomaln. Что бы не подсказывали инстинкты разработчика, в подавляющем большинстве классов С# необходимость в создании явной логики по очистке или специального фи- нализатора возникать не будет. Объясняется это очень просто: если в классах используются лишь другие управляемые объекты, все они рано или поздно все равно будут подвергаться сборке мусора. Единственным случаем, когда может возникать потребность в создании класса, способного выполнять после себя процедуру очистки, является работа с неуправляемыми ресурсами (такими как низкоуровневые файловые дескрипторы, низкоуровневые неуправляемые соединения с базами данных, фрагменты неуправляемой памяти и т.п.). Внутри .NET неуправляемые ресурсы появляются в результате непосредственного вызова API-интерфейса операционной системы с помощью служб PInvoke (Platform Invocation Services — службы вызова платформы) или применения очень сложных сценариев взаимодействия с СОМ. Ознакомьтесь со следующим правилом сборки мусора. Правило. Единственная причина переопределения Finalize () связана с использованием в классе С# каких-то неуправляемых ресурсов через PInvoke или сложных процедур взаимодействия с СОМ (обычно посредством членов типа System. Runtime . InteropServices . Marshal). Переопределение System.Object. Finalize () В тех редких случаях, когда создается класс С#, в котором используются неуправляемые ресурсы, очевидно, понадобится обеспечить предсказуемое освобождение соответствующей памяти. Для примера создадим новый проект типа Console Application на С# по имени SimpleFinalize, вставим в него класс MyResourceWrapper, в котором будет использоваться какой-то неуправляемый ресурс. Теперь необходимо переопределить метод Finalize(). Как ни странно, применять для этого ключевое слово override в С# не допускается: class MyResourceWrapper { // Ошибка на этапе компиляции' protected override void Finalize () { } } Вместо этого для достижения того же эффекта должен применяться синтаксис деструктора (подобно C++). Объясняется это тем, что при обработке синтаксиса финализато- ра компилятор автоматически добавляет в неявно переопределяемый метод Finalize () приличное количество требуемых элементов инфраструктуры. Финализаторы в С# очень похожи на конструкторы тем, что именуются идентично классу, внутри которого определены. Помимо этого, они сопровождаются префиксом в виде тильды (~). В отличие от конструкторов, однако, они никогда не снабжаются модификатором доступа (поскольку всегда являются неявно защищенными), не принимают никаких параметров и не могут быть перегружены (в каждом классе может присутствовать только один финализатор).
306 Часть II. Главные конструкции программирования на С# Ниже приведен пример написания специального финализатора для класса MyResourceWrapper, который при вызове заставляет систему выдавать звуковой сигнал. Очевидно, что данный пример предназначен только для демонстрационных целей. В реальном приложении финализатор будет только освобождать любые неуправляемые ресурсы и не будет взаимодействовать ни с какими другими управляемыми объектами, даже теми, на которые ссылается текущий объект, поскольку рассчитывать на то, что они все еще существуют на момент вызова данного метода Finalize () сборщиком мусора нельзя. // Переопределение System.Object.Finalize() //с использованием синтаксиса финализатора. class MyResourceWrapper { -MyResourceWrapper() { // Здесь производится очистка неуправляемых ресурсов. // Обеспечение подачи звукового сигнала при // уничтожении объекта (только в целях тестирования). Console.Beep(); } } Если теперь просмотреть CIL-код данного деструктора с помощью утилиты ildasm. ехе, обнаружится, что компилятор добавляет некоторый код для проверки ошибок. Во- первых, он помещает операторы из области действия метода Finalize () в блок try (см. главу 7), и, во-вторых, добавляет соответствующий блок finally, чтобы гарантировать выполнение метода FinalizeO в базовых классах, какие бы исключения не возникали в контексте try. .method family hidebysig virtual instance void Finalize() cil managed // Code size 13 (Oxd) // Размер кода 13(Oxd) axstack 1 ■try I IL_0000 IL_0005 IL_000a void IL_000f IL_0010 IL 0011 ldc.i4 0x4e20 ldc.i4 0x3e8 call [mscorlib]System.Co nop nop leave.s IL 001b } // end .try finally { IL_0013: ldarg.O IL_0014: call instance void [mscorlib]System.Object::Finalize() IL_0 019: nop IL_001a: endfinally } // end handler IL 001b: nop IL_001c: ret } // end of method MyResourceWrapper::Finalize // конец метода MyResourceWrapper::Finalize
Глава 8. Время жизни объектов 307 При тестировании класса MyResourceWrapper система выдает звуковой сигнал во время завершения работы приложения, так как среда CLR автоматически вызывает фи- нализаторы при выгрузке AppDomain. static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Finalizers *****\n"); Console.WriteLine("Hit the return key to shut down this app"); Console.WriteLine("and force the GC to invoke Finalize ()"); Console.WriteLine("for finalizable objects created in this AppDomain."); // Нажмите клавишу <Enter>, чтобы завершить приложение // и заставить сборщик мусора вызвать метод Finalize () // для всех финализируемых объектов, которые // были созданы в домене этого приложения. Console.ReadLine (); MyResourceWrapper rw = new MyResourceWrapper (); } Исходный код. Проект SimpleFinalize доступен в подкаталоге Chapter 8. Описание процесса финализации Чтобы не делать лишнюю работу, следует всегда помнить, что задачей метода Finalize () является забота о том, чтобы объект .NET мог освобождать неуправляемые ресурсы во время сборки мусора. Следовательно, при создании типа, в котором никакие неуправляемые сущности не используются (так бывает чаще всего), от финализации оказывается мало толку. На самом деле, всегда, когда возможно, следует стараться проектировать типы так, чтобы в них не поддерживался метод FinalizeO по той очень простой причине, что выполнение финализации отнимает время. При размещении объекта в управляемой куче исполняющая среда автоматически определяет, поддерживается ли в нем какой-нибудь специальный метод Finalize () . Если да, тогда она помечает его как финализируемый (finalizable) и сохраняет указатель на него во внутренней очереди, называемой очередью финализации (finalizatlon queue). Эта очередь финализации представляет собой просматриваемую сборщиком мусора таблицу, где перечислены объекты, которые перед удалением из кучи должны быть обязательно финализированы. Когда сборщик мусора определяет, что наступило время удалить объект из памяти, он проверяет каждую запись в очереди финализации и копирует объект из кучи в еще одну управляемую структуру, называемую таблицей объектов, доступных для финализации (finalization reachable table). После этого он создает отдельный поток для вызова метода FinalizeO в отношении каждого из упоминаемых в этой таблице объектов при следующей сборке мусора. В результате получается, что для окончательной финализации объекта требуется как минимум два процесса сборки мусора. Из всего вышесказанного следует, что хотя финализация объекта действительно позволяет гарантировать способность объекта освобождать неуправляемые ресурсы, она все равно остается недетерминированной по своей природе, и по причине дополнительной выполняемой незаметным образом обработки протекает гораздо медленнее. Создание высвобождаемых объектов Как было показано, методы финализации могут применяться для освобождения неуправляемых ресурсов при активизации процесса сборки мусора. Однако многие неуправляемые объекты являются "ценными элементами" (например, низкоуровневые
308 Часть II. Главные конструкции программирования на С# соединения с базой данных или файловые дескрипторы) и часто выгоднее освобождать их как можно раньше, еще до наступления момента сборки мусора. Поэтому вместо переопределения FinalizeO в качестве альтернативного варианта также можно реализовать в классе интерфейс I Disposable, который имеет единственный метод по имени Dispose() : public interface IDisposable { void Dispose (); } Принципы программирования с использованием интерфейсов детально рассматриваются в главе 9. Если объяснять вкратце, то интерфейс представляет собой коллекцию абстрактных членов, которые может поддерживать класс или структура. Когда действительно реализуется поддержка интерфейса IDisposable, то предполагается, что после завершения работы с объектом метод Dispose () должен вручную вызываться пользователем этого объекта, прежде чем объектной ссылке будет позволено покинуть область действия. Благодаря этому объект может выполнять любую необходимую очистку неуправляемых ресурсов без попадания в очередь финализации и без ожидания того, когда сборщик мусора запустит содержащуюся в классе логику финализации. На заметку! Интерфейс IDisposable может быть реализован как в классах, так и в структурах (в отличие от метода Finalize (), который допускается переопределять только в классах), потому что метод Dispose () вызывается пользователем объекта (а не сборщиком мусора). Рассмотрим пример использования этого интерфейса. Создадим новый проект типа Console Application по имени SimpleDispose и добавим в него следующую модифицированную версию класса MyResourceWrapper, в которой теперь предусмотрена реализация интерфейса IDisposable, а не переопределение метода System.Object. Finalize (): // Реализация интерфейса IDisposable. public class MyResourceWrapper : IDisposable { // После окончания работы с объектом пользователь // объекта должен вызывать этот метод. public void Dispose () { // Освобождение неуправляемых ресурсов. . . // Избавление от других содержащихся внутри //и пригодных для очистки объектов. // Только для целей тестирования. Console.WriteLine ("***** In Dispose1 *****"); } } Обратите внимание, что метод Dispose () отвечает не только за освобождение неуправляемых ресурсов типа, но и за вызов аналогичного метода в отношении любых других содержащихся в нем высвобождаемых объектов. В отличие от Finalize (), в нем вполне допустимо взаимодействовать с другими управляемыми объектами. Объясняется это очень просто: сборщик мусора не имеет понятия об интерфейсе IDisposable и потому никогда не будет вызывать метод Dispose (). Следовательно, при вызове данного метода пользователем объект будет все еще существовать в управляемой куче и иметь доступ ко всем остальным находящимся там объектам. Логика вызова этого метода выглядит довольно просто:
Глава 8. Время жизни объектов 309 class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Dispose *****\n"); // Создание высвобождаемого объекта и вызов метода // Dispose () для освобождения любых внутренних ресурсов. MyResourceWrapper rw = new MyResourceWrapper (); rw.Dispose(); Console.ReadLine (); } } Разумеется, перед тем как пытаться вызывать метод Dispose () на объекте, нужно проверить, поддерживает ли соответствующий тип интерфейс I Disposable. И хотя в документации .NET Framework 4.0 SDK всегда доступна информация о том, какие типы в библиотеке базовых классов реализуют I Disposable, такую проверку удобнее выполнять программно с применением ключевого слова is или as (см. главу 6). class Program { static void Main(string[] args) { Console.WriteLine ("***** Fun with Dispose *****\n"); MyResourceWrapper rw = new MyResourceWrapper (); if (rw is IDisposable) rw.Dispose(); Console.ReadLine(); } } Этот пример раскрывает еще одно правило относительно работы с подвергаемыми сборке мусора типами. Правило. Для любого создаваемого напрямую объекта, если он поддерживает интерфейс IDisposable, следует всегда вызывать метод Dispose (). Необходимо исходить из того, что в случае, если разработчик класса решил реализовать метод Dispose (), значит, классу надлежит выполнять какую-то очистку. К приведенному выше правилу прилагается одно важное пояснение. Некоторые из типов, которые поставляются в библиотеках базовых классов и реализуют интерфейс IDisposable, предусматривают использование для метода Dispose () (несколько сбивающего с толку) псевдонима, чтобы заставить отвечающий за очистку метод звучать более естественно для типа, в котором он определяется. Для примера можно взять класс System. 10. FileStream, в котором реализуется интерфейс IDisposable (и, следовательно, поддерживается метод Dispose ()), но при этом также определяется и метод Close (), каковой применяется для той же цели. // Предполагается, что было импортировано пространство имен System.10... static void DisposeFileStream() { FileStream fs = new FileStream("myFile.txt", FileMode.OpenOrCreate); // Мягко говоря, сбивает с толку! // Вызовы этих методов делают одно и то же! fs.Close () ; f s . Dispose () ; }
310 Часть II. Главные конструкции программирования на С# И хотя "закрытие" (close) файла действительно звучит более естественно, чем его "освобождение" (dispose), нельзя не согласиться с тем, что подобное дублирование отвечающих за одно и то же методов вносит путаницу. Поэтому при использовании этих нескольких типов, в которых применяются псевдонимы, просто помните о том, что если тип реализует интерфейс IDisposable, то вызов метода Dispose () всегда является правильным образом действия. Повторное использование ключевого слова using в С# При работе с управляемым объектом, который реализует интерфейс IDisposable, довольно часто требуется применять структурированную обработку исключений, гарантируя, что метод Dispose () типа будет вызываться даже в случае возникновения какого-то исключения: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Dispose *****\n"); MyResourceWrapper rw = new MyResourceWrapper(); try { // Использование членов rw. } finally { // Обеспечение вызова метод Dispose() в любом случае, //в том числе при возникновении ошибки. rw.Dispose (); } } Хотя это является замечательными примером "безопасного программирования", истина состоит в том, что очень немногих разработчиков прельщает перспектива заключать каждый очищаемый тип в блок try/finally лишь для того, чтобы гарантировать вызов метода Dispose (). Для достижения аналогичного результата, но гораздо менее громоздким образом, в С# поддерживается специальный фрагмент синтаксиса, который выглядит следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Dispose *****\n"); // Метод Dispose() вызывается автоматически // при выходе за пределы области действия using. using(MyResourceWrapper rw = new MyResourceWrapper()) { . // Использование объекта rw. } } Если теперь просмотреть CIL-код этого метода Main () с помощью утилиты ildasm. ехе, то обнаружится, что синтаксис using в таких случаях на самом деле расширяется до логики try/finally, которая включает в себя и ожидаемый вызов Dispose (): .method private hidebysig static void Main(string [ ] args) cil managed { . try { } // end .try
Глава 8. Время жизни объектов 311 finally { IL_0012: callvirt instance void SimpleFinalize.MyResourceWrapper::Dispose() } // end handler } // end of method Program::Main На заметку! При попытке применить using к объекту, который не реализует интерфейс IDisposable, на этапе компиляции возникнет ошибка. Хотя применение такого синтаксиса действительно избавляет от необходимости вручную помещать высвобождаемые объекты в рамки try/ finally, в настоящее время, к сожалению, ключевое слово using в С# имеет двойное значение (поскольку служит и для добавления ссылки на пространства имен, и для вызова метода Dispose ()). Тем не менее, при работе с типами.NET, которые поддерживают интерфейс IDisposable, данная синтаксическая конструкция будет гарантировать автоматический вызов метода Dispose () в отношении соответствующего объекта при выходе из блока using. Кроме того, в контексте using допускается объявлять несколько объектов одного и того же типа. Как не трудно догадаться, в таком случае компилятор будет вставлять код с вызовом Dispose () для каждого объявляемого объекта. static void Main(string[] args) { Console.WriteLine ("***** Fun with Dispose *****\nM); // Использование разделенного запятыми списка для // объявления нескольких подлежащих освобождению объектов. using(MyResourceWrapper rw = new MyResourceWrapper(), rw2 = new MyResourceWrapper()) { // Использование объектов rw и rw2. } } Исходный код. Проект SimpleDispose доступен в подкаталоге Chapter 8. Создание финализируемых и высвобождаемых типов К этому моменту были рассмотрены два различных подхода, которые можно применять для создания класса, способного производить очистку и освобождать внутренние неуправляемые ресурсы. Первый подход заключается в переопределении метода System. Ob ject.Finalize() и позволяет гарантировать то, что объект будет очищать себя сам во время процесса сборки мусора (когда бы тот не запускался) без вмешательства со стороны пользователя. Второй подход предусматривает реализацию интерфейса IDisposable и позволяет обеспечить пользователя объекта возможностью очищать объект сразу же по окончании работы с ним. Однако если пользователь забудет вызвать метод Dispose (), неуправляемые ресурсы могут оставаться в памяти на неопределенный срок. Как не трудно догадаться, оба подхода можно комбинировать и применять вместе в определении одного класса, получая преимущества от обеих моделей. Если пользо-
312 Часть II. Главные конструкции программирования на С# ватель объекта не забыл вызвать метод Dispose (), можно проинформировать сборщик мусора о пропуске финализации, вызвав метод GC.SuppressFinalize (). Если же пользователь забыл вызвать этот метод, объект рано или поздно будет подвергнут финализации и получит возможность освободить внутренние ресурсы. Преимущество такого подхода в том, что при этом внутренние ресурсы будут так или иначе, но всегда освобождаться. Ниже приведена очередная версия класса MyResourceWrapper, которая теперь предусматривает выполнение и финализации и освобождения и содержится внутри проекта типа Console Application по имени FinalizableDisposableClass. // Сложный упаковщик ресурсов. public class MyResourceWrapper : IDisposable { // Сборщик мусора будет вызывать этот метод, если // пользователь объекта забыл вызвать метод Dispose () . ~MyResourceWrapper() { // Освобождение любых внутренних неуправляемых // ресурсов. Метод Dispose () HE должен вызываться //ни для каких управляемых объектов. } // Пользователь объекта будет вызывать этот метод для // того, чтобы освободить ресурсы как можно быстрее. public void Dispose () { // Здесь осуществляется освобождение неуправляемых ресурсов //и вызов Dispose()для остальных высвобождаемых объектов. // Если пользователь вызвал Dispose (), то финализация не нужна, // поэтому далее она подавляется. GC.SuppressFinalize(this); } } Здесь важно обратить внимание на то, что метод Dispose () был модифицирован так, чтобы вызывать метод GC . SuppressFinalize (). Этот метод информирует CLR- среду о том, что вызывать деструктор при подвергании данного объекта сборке мусора больше не требуется, поскольку неуправляемые ресурсы уже были освобождены посредством логики Dispose (). Формализованный шаблон очистки В текущей реализации MyResourceWrapper работает довольно хорошо, но все равно еще остается несколько небольших недочетов. Во-первых, методам Finalize () и Dispose () требуется освобождать одни и те же неуправляемые ресурсы, а это чревато дублированием кода, которое может существенно усложнить его сопровождение. Поэтому в идеале не помешало бы определить приватную вспомогательную функцию, которая могла бы вызываться в любом из этих методов. Во-вторых, нелишне позаботиться о том, чтобы метод Finalize () не пытался избавиться от любых управляемых объектов, а метод Dispose () — наоборот, обязательно это делал. И, наконец, в-третьих, не помешало бы позаботиться о том, чтобы пользователь объекта мог спокойно вызывать метод Dispose () множество раз без получения ошибки. В настоящий момент в методе Dispose () никаких подобных мер предосторожностей пока не предусмотрено. Для решения подобных вопросов с дизайном в Microsoft создали формальный шаблон очистки, который позволяет достичь оптимального баланса между надежностью,
Глава 8. Время жизни объектов 313 удобством в обслуживании и производительностью. Ниже приведена окончательная версия MyResourceWrapper, в которой применяется упомянутый формальный шаблон. public class MyResourceWrapper : IDisposable { // Используется для выяснения того, вызывался ли уже метод Dispose () . private bool disposed = false; public void Dispose () { // Вызов вспомогательного метода. // Значение true указывает на то, что очистка // была инициирована пользователем объекта. Cleanup(true); // Подавление финализации. GC.SuppressFinalize (this); } private void Cleanup(bool disposing) { // Проверка, выполнялась ли очистка. if (! this.disposed) { // Если disposing равно true, должно осуществляться // освобождение всех управляемых ресурсов, if (disposing) { // Здесь осуществляется освобождение управляемых ресурсов. } // Очистка неуправляемых ресурсов. } disposed = true; } -MyResourceWrapper() { // Вызов вспомогательного метода. // Значение false указывает на то, что // очистка была инициирована сборщиком мусора. Cleanup(false); } } Обратите внимание, что в MyResourceWrapper теперь определяется приватный вспомогательный метод по имени Cleanup (). Передача ему в качестве аргумента значения true свидетельствует о том, что очистку инициировал пользователь объекта, следовательно, требуется освободить все управляемые и неуправляемые ресурсы. Когда очистка инициируется сборщиком мусора, при вызове Cleanup () передается значение false, чтобы освобождения внутренних высвобождаемых объектов не происходило (поскольку рассчитывать на то, что они по-прежнему находятся в памяти, нельзя). И, наконец, перед выходом из Cleanup () для переменной экземпляра типа bool (по имени disposed) устанавливается значение true, что дает возможность вызывать метод Dispose () много раз без появления ошибки. На заметку! После "освобождения" (dispose) объекта клиент по-прежнему может вызывать на нем какие-нибудь члены, поскольку объект пока еще находится в памяти. Следовательно, в показанном сложном классе-упаковщике ресурсов не помешало бы снабдить каждый член дополнительной логикой, которая бы, по сути, гласила: "если объект освобожден, ничего не делать, а просто вернуть управление".
314 Часть II. Главные конструкции программирования на С# Чтобы протестировать последнюю версию класса MyResourceWrapper, добавим в метод финализации вызов Console. Веер (): ~MyResourceWrapper() { Console.Веер(); // Вызов вспомогательного метода. // Указание значения false свидетельствует о том, // что очистка была инициирована сборщиком мусора. Cleanup(false); } Давайте обновим метод Main () следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Dispose() / Destructor Combo Platter *****"); // Вызов метода Dispose () вручную; метод финализации //в таком случае вызываться не будет. MyResourceWrapper rw = new MyResourceWrapper(); rw.Dispose (); // Пропуск вызова метода Dispose () ; в таком случае будет // вызываться метод финализации и выдаваться звуковой сигнал. MyResourceWrapper rw2 = new MyResourceWrapper() ; } Обратите внимание, что в первом случае производится явный вызов метода Dispose () на объекте rw и потому вызов деструктора подавляется. Однако во втором случае мы "забываем" вызвать метод Dispose () на объекте rw2 и потому по окончании выполнения приложения услышим однократный звуковой сигнал. Если закомментировать вызов метода Dispose () на объекте rw, звуковых сигналов будет два. Исходный код. Проект FinalizableDisposableClass доступен в подкаталоге Chapter 8. На этом тема управления объектами CLR-средой посредством сборки мусора завершена. И хотя некоторые дополнительные (и довольно экзотические) детали, касающиеся сборки мусора (вроде слабых ссылок и восстановления объектов), остались не рассмотренными, полученных базовых сведений должно оказаться достаточно, чтобы продолжить изучение самостоятельно. Напоследок в главе предлагается исследовать совершенно новую функциональную возможность, появившуюся в .NET 4.0, которая называется отложенной (ленивой) инициализацией. Отложенная инициализация объектов На заметку! В настоящем разделе предполагается наличие у читателя знаний о том, что собой представляют обобщения и делегаты в .NET. Исчерпывающие сведения о делегатах и обобщениях можно найти в главах 10 и 11. При создании классов иногда возникает необходимость предусмотреть в коде определенную переменную-член, которая на самом деле может никогда не понадобиться из-за того, что пользователь объекта не будет обращаться к методу (или свойству), в котором она используется. Вполне разумное решение, однако, на практике его реализация может оказываться очень проблематичной в случае, если инициализация интересующей переменной экземпляра требует большого объема памяти.
Глава 8. Время жизни объектов 315 Для примера представим, что требуется создать класс, инкапсулирующий операции цифрового музыкального проигрывателя, и помимо ожидаемых методов вроде Play (), Pause () и Stop () его нужно также обеспечить способностью возврата коллекции объектов Song (через класс по имени All Tracks), которые представляют каждый из имеющихся в устройстве цифровых музыкальных файлов. Чтобы получить такой класс, создадим новый проект типа Console Application no имени La z у Object Instantiation и добавим в него следующие определения типов классов: // Представляет одну композицию. class Song { public string Artist { get; set; } public string TrackName { get; set; } public double TrackLength { get; set; } } // Представляет все композиции в проигрывателе. class AllTracks { //В нашем проигрывателе может содержаться // максимум 10 000 композиций. public AllTracks () { // Предполагаем, что здесь производится заполнение // массива объектов Song. Console. WriteLine ("Filling up the songs!11); } } // Класс MediaPlayer включает объект AllTracks. class MediaPlayer { // Предполагаем, что эти методы делают нечто полезное. public void Play() { /* Воспроизведение композиции */ } public void Pause() { /* Приостановка воспроизведения композиции */ } public void Stop() { /* Останов воспроизведения композиции */ } private AllTracks allSongs = new AllTracks(); public AllTracks GetAllTracks() { // Возвращаем все композиции. return allSongs; } } В текущей реализации MediaPlayer делается предположение о том, что пользователю объекта понадобится получать список объектов с помощью метода GetAllTracks (). А что если пользователю объекта не нужен этот список? Так или иначе, но переменная экземпляра AllTracks будет приводить к созданию 10 000 объектов Song в памяти: static void Main(string [ ] args) { //В этом вызывающем коде получение всех композиций не // производится, но косвенно все равно создаются // 10 000 объектов! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play(); Console.ReadLine ();
316 Часть II. Главные конструкции программирования на С# Понятно, что создания 10 000 объектов, которыми никто не будет пользоваться, лучше избежать, так как это изрядно прибавит работы сборщику мусора .NET. Хотя можно вручную добавить код, который обеспечит создание объекта all Songs только в случае его использования (например, за счет применения шаблона с методом фабрики), существует и более простой путь. С выходом .NET 4.0 в библиотеках базовых классов появился очень интересный обобщенный класс по имени Lazy о, который находится в пространстве имен System внутри сборки mscorlib.dll. Этот класс позволяет определять данные, которые не должны создаваться до тех пор, пока они на самом деле не начнут использоваться в кодовой базе. Поскольку он является обобщенным, при первом использовании в нем должен быть явно указан тип элемента, который должен создаваться. Этим типом может быть как любой из типов, определенных в библиотеках базовых классов .NET, так и специальный тип, самостоятельно созданный разработчиком. Для обеспечения отложенной инициализации переменной экземпляра AllTracks можно просто заменить следующий фрагмент кода: // Класс MediaPlayer включает объект AllTracks class MediaPlayer { private AllTracks allSongs = new AllTracks (); public AllTracks GetAllTracks() { // Возврат всех композиций. return allSongs; } } таким кодом: // Класс MediaPlayer включает объект Lazy<AllTracks>. class MediaPlayer { private Lazy<AllTracks> allSongs = new Lazy<AllTracks>(); public AllTracks GetAllTracks() { // Возврат всех композиций. return allSongs.Value; } } Помимо того, что переменная экземпляра AllTrack теперь имеет тип Lazyo, важно отметить, что реализация предыдущего метода GetAllTracks () тоже изменилась. В частности, теперь требуется использовать доступное только для чтения свойство Value класса Lazy о для получения фактических хранимых данных (в этом случае — объект AllTracks, обслуживающий 10 000 объектов Song). Кроме того, обратите внимание, как благодаря этому простому изменению, показанный ниже модифицированный метод Main() будет незаметно размещать объекты Song в памяти только в случае, когда был действительно выполнен вызов метода GetAllTracks(). static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Lazy Instantiation *****\n"); // Никакого размещения объекта AllTracks! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play();
Глава 8. Время жизни объектов 317 // Размещение объекта AllTracks происходит // только в случае вызова метода GetAllTracks(). MediaPlayer yourPlayer = new MediaPlayer(); AllTracks yourMusic = yourPlayer.GetAllTracks(); Console.ReadLine(); } Настройка процесса создания данных Lazyo При объявлении переменной Lazyo фактический внутренний тип данных создается с помощью конструктора по умолчанию: // Конструктор по умолчанию для AllTracks вызывается // при использовании переменной LazyO. private Lazy<AllTracks> allSongs = new Lazy<AllTracks>(); В некоторых случаях подобное поведение может быть вполне подходящим, но что если класс AllTracks имеет дополнительные конструкторы, и нужно позаботиться о том, чтобы вызывался подходящий из них? Более того, а что если необходимо, чтобы при создании переменной Lazy () выполнялась какая-то дополнительная работа (помимо просто создания объекта AllTracks)? К счастью, класс Lazy () позволяет предоставлять в качестве необязательного параметра обобщенный делегат, который указывает, какой метод должен вызываться во время создания находящегося внутри него типа. Этим обобщенным делегатом является тип System. Funco, который может указывать на метод, возвращающий тот же тип данных, что создается соответствующей переменной Lazyo, и способный принимать вплоть до 16 аргументов (которые типизируются с помощью параметров обобщенного типа). В большинстве случаев необходимость указывать параметры для передачи методу, на который ссылается Fun со, возникать не будет Более того, чтобы значительно упростить применение Fun со , лучше использовать лямбда-выражения (отношения между делегатами и лямбда-выражениями подробно рассматриваются в главе 11). С учетом всего сказанного, ниже приведена окончательная версия MediaPlayer, в которой теперь при создании внутреннего объекта AllTracks добавляется небольшой специальный код. Следует запомнить, что данный метод должен обязательно возвращать новый экземпляр указанного в Lazyo типа перед выходом, а использовать можно любой конструктор (в коде для AllTracks по-прежнему вызывается конструктор по умолчанию). class MediaPlayer { // Использование лямбда-выражения для добавления // дополнительного кода при создании объекта AllTracks. private Lazy<AllTracks> allSongs = new Lazy<AllTracks> ( () => { Console.WriteLine ("Creating AllTracks object!11); return new AllTracks (); } ) ; public AllTracks GetAllTracks () { // Возврат всех композиций. return allSongs.Value; } }
318 Часть II. Главные конструкции программирования на С# Хочется надеяться, что удалось продемонстрировать выгоду, которую способен обеспечить класс Lazyo. В сущности, этот новый обобщенный класс позволяет делать так, чтобы дорогостоящие объекты размещались в памяти только тогда, когда они будут действительно нужны пользователю объекта. Если эта тема заинтересовала, можно заглянуть в посвященный классу System. Lazyo раздел в документации .NET Framework 4.0 SDK и найти там дополнительные примеры программирования отложенной инициализации. Исходный код. Проект LazyObjectInstantiation доступен в подкаталоге Chapter 8. Резюме Цель настоящей главы состояла в разъяснении, что собой представляет процесс сборки мусора. Было показано, что сборщик мусора активизируется только тогда, когда ему не удается получить необходимый объем памяти из управляемой кучи (или когда происходит выгрузка домена соответствующего приложения из памяти). После его активизации поводов для волнения возникать не должно, поскольку разработанный в Microsoft алгоритм сборки мусора хорошо оптимизирован и предусматривает использование поколений объектов, дополнительных потоков для финализации объектов и управляемой кучи для обслуживания больших объектов. В главе также было показано, каким образом программно взаимодействовать со сборщиком мусора, применяя класс System.GC. Как отмечалось, единственным случаем, когда в подобном может возникнуть необходимость, является создание финализи- руемых или высвобождаемых типов классов, в которых используются неуправляемые ресурсы. Вспомните, что под финализируемыми типами подразумеваются классы, в которых переопределен виртуальный метод System.Object.FinalizeO для обеспечения очистки неуправляемых ресурсов на этапе сборки мусора, а под высвобождаемыми — классы (или структуры), в которых реализован интерфейс IDisposable, вызываемый пользователем объекта по окончании работы с ним. Кроме того, в главе был продемонстрирован формальный шаблон "очистки", позволяющий совмещать оба эти подхода. И, наконец, в главе был описан появившийся в .NET 4.0 обобщенный класс по имени Lazyo. Как здесь было показано, данный класс позволяет отложить создание дорогостоящих (в плане потребления памяти) объектов до тех пор, пока у вызывающей стороны действительно не возникнет потребность в их использовании. Это помогает сократить количество объектов, сохраняемых в управляемой куче, и облегчает нагрузку на сборщик мусора.
ЧАСТЬ III Дополнительные конструкции программирования наС# В этой части... Глава 9. Работа с интерфейсами Глава 10. Обобщения Глава 11. Делегаты, события и лямбда-выражения Глава 12. Расширенные средства языка С# Глава 13. LINQ to Objects
ГЛАВА 9 Работа с интерфейсами Материал этой главы основан на начальных знаниях объектно-ориентированной разработки и посвящен концепциям программирования с использованием интерфейсов. В главе будет показано, как определять и реализовать интерфейсы, а также описаны преимущества, которые дает построение типов, поддерживающих несколько видов поведения. Также будут рассмотрены и другие связанные с этим темы, наподобие того, как получать ссылки на интерфейсы, как реализовать интерфейсы явным образом и как создавать иерархии интерфейсов. Помимо этого, конечно же, будут описаны стандартные интерфейсы, которые предлагаются в библиотеках базовых классов .NET. Как будет показано, специальные классы и структуры могут реализовать эти готовые интерфейсы, что позволяет поддерживать несколько дополнительных поведений, таких как клонирование, перечисление и сортировка объектов. Что собой представляют типы интерфейсов Для начала ознакомимся с формальным определением типа интерфейса. Интерфейс (interface) представляет собой не более чем просто именованный набор абстрактных членов. Как упоминалось в главе 6, абстрактные методы являются чистым протоколом, поскольку не имеют никакой стандартной реализации. Конкретные члены, определяемые интерфейсом, зависят от того, какое поведение моделируется с его помощью. Это действительно так. Интерфейс выражает поведение, которое данный класс или структура может избрать для поддержки. Более того, как будет показано далее в главе, каждый класс (или структура) может поддерживать столько интерфейсов, сколько необходимо, и, следовательно, тем самым поддерживать множество поведений. Нетрудно догадаться, что в библиотеках базовых классов .NET поставляются сотни предопределенных типов интерфейсов, которые реализуются в различных классах и структурах. Например, как будет показано в главе 21, в состав ADO.NET входит множество поставщиков данных, которые позволяют взаимодействовать с определенной системой управления базами данных. Это означает, что в ADO.NET на выбор доступно множество объектов соединения (SqlConnection, OracleConnection, OdbcConnection и т.д.). Несмотря на то что каждый из этих объектов соединения имеет уникальное имя, определяется в отдельном пространстве имен и (в некоторых случаях) упаковывается в отдельную сборку, все они реализуют один общий интерфейс IDbConnection: // Интерфейс IDbConnection имеет типичный ряд членов, // которые поддерживают все объекты соединения. public interface IDbConnection : IDisposable {
Глава 9. Работа с интерфейсами 321 // Методы IDbTransaction BeginTransaction (); IDbTransaction BeginTransaction(IsolationLevel ll); void ChangeDatabase(string databaseName); void Close (_) ; IDbCommand CreateCommand(); void Open (); // Свойства string ConnectionString { get; set;} int ConnectionTimeout { get; } string Database { get; } ConnectionState State { get; } } На заметку! По соглашению имена всех интерфейсов в .NET сопровождаются префиксом в виде заглавной буквы "I". При создании собственных специальных интерфейсов рекомендуется тоже следовать этому соглашению. Вдумываться, что именно делают все эти члены, пока не нужно. Сейчас главное просто понять, что в интерфейсе IDbConnection предлагается набор членов, которые являются общими для всех объектов соединений ADO.NET. Исходя из этого, можно точно знать, что каждый объект соединения поддерживает такие члены, как Open(),Close(), CreateCommand () и т.д. Более того, поскольку методы этого интерфейса всегда являются абстрактными, в каждом объекте соединения они могут быть реализованы собственным уникальным образом. Другим примером может служить пространство имен System. Windows . Forms. В этом пространстве имени определен класс по имени Control, который является базовым для целого ряда интерфейсных элементов управления Windows Forms (DataGridView, Label, StatusBar, TreeView и т.д.). Этот класс реализует интерфейс IDropTarget, определяющий базовую функциональность перетаскивания: public interface IDropTarget { // Методы void OnDragDrop(DragEventArgs e); void OnDragEnter(DragEventArgs e) ; void OnDragLeave(EventArgs e); void OnDragOver(DragEventArgs e) ; } С учетом этого интерфейса можно верно предполагать, что любой класс, который расширяет System. Windows . Forms . Control, будет поддерживать четыре метода с именами OnDragDrop(), OnDragEnter(), OnDragLeave() и OnDragOver(). В остальной части книги будут встречаться десятки таких интерфейсов, поставляемых в библиотеках базовых классов .NET. Эти интерфейсы могут быть реализованы в собственных классах и структурах, чтобы получать типы, тесно интегрированные с .NET. Сравнение интерфейсов и абстрактных базовых классов Из-за материала, приведенного в главе 6, тип интерфейса может показаться очень похожим на абстрактный базовый класс. Вспомните, что когда класс помечается как абстрактный, в нем может определяться любое количество абстрактных членов для предоставления полиморфного интерфейса для всех производных типов. Даже если в типе класса действительно определяется набор абстрактных членов, в нем также может
322 Часть III. Дополнительные конструкции программирования на С# спокойно определяться любое количество конструкторов, полей, неабстрактных членов (с реализацией) и т.п. Интерфейсы, с другой стороны, могут содержать только определения абстрактных членов. Полиморфный интерфейс, предоставляемый абстрактным родительским классом, обладает одним серьезным ограничением: определяемые в абстрактном родительском классе члены поддерживаются только в производных типах. В более крупных программных системах, однако, довольно часто разрабатываются многочисленные иерархии классов, не имеющие никаких общих родительских классов кроме System.Object. Из-за того, что абстрактные члены в абстрактном базовом классе подходят только для производных типов, получается, что настраивать типы в разных иерархиях так, чтобы они поддерживали один и тот же полиморфный интерфейс, невозможно. Для примера предположим, что определен следующий абстрактный класс: abstract class CloneableType { // Только производные типы могут поддерживать этот // "полиморфный интерфейс". Классы в других иерархиях //не будут иметь доступа к этому абстрактному члену. public abstract object Clone (); } Из-за такого определения поддерживать метод Clone () могут только члены, которые расширяют CloneableType. В случае создания нового набора классов, которые не расширяют CloneableType, использовать в них этот полиморфный интерфейс, соответственно, не получится. Кроме того, как уже неоднократно упоминалось, в С# не поддерживается возможность наследования от нескольких классов. Следовательно, создать класс MiniVan, унаследованный и от Саг, и от CLoneableType, тоже не получится: // В С# наследование от нескольких классов не допускается. public class MiniVan : Car, CloneableType { } Нетрудно догадаться, что здесь на помощь приходят типы интерфейсов. После определения интерфейс может быть реализован в любом типе или структуре, в любой иерархии и внутри любого пространства имен или сборки (написанной на любом языке программирования .NET). Очевидно, что это делает интерфейсы чрезвычайно полиморфными. Для примера рассмотрим стандартный интерфейс .NET по имени ICloneable, определенный в пространстве имен System. Этот интерфейс имеет единственный метод Clone (): public interface ICloneable { object Clone (); } Если заглянуть в документацию .NET Framework 4.0 SDK, можно обнаружить, что очень многие по виду несвязанные типы (System. Array, System. Data . SqlClient. SqlConnection, System.OperatingSystem, System. String и т.д.) реализуют этот интерфейс. В результате, хотя у этих типов нет общего родителя (кроме System. Object), с ними все равно можно работать полиморфным образом через интерфейс ICloneable. Например, если есть метод по имени С1опеМе(), принимающий интерфейс ICloneable в качестве параметра, этому методу можно передавать любой объект, который реализует упомянутый интерфейс. Рассмотрим следующий простой класс Program, определенный в проекте типа Console Application (Консольное приложение) по имени ICloneableExample:
Глава 9. Работа с интерфейсами 323 class Program { static void Main(string [ ] args) { Console,WriteLine("***** A First Look at Interfaces *****\n"); // Все эти классы поддерживают интерфейс ICloneable. string myStr = "Hello"; OperatingSystem unixOS = new OperatingSystem(PlatformID.Unix, new Version ()); System.Data.SqlClient.SqlConnection sqlCnn = new System.Data.SqlClient.SqlConnection (); // Поэтому все они могут передаваться методу, // принимающему ICloneable в качестве параметра. CloneMe(myStr); CloneMe(unixOS); CloneMe(sqlCnn); Console.ReadLine (); } private static void CloneMe(ICloneable c) { // Клонируем любой получаемый тип //и выводим его имя в окне консоли. object theClone = c.CloneO; Console.WriteLine("Your clone is a: {0}", theClone.GetType().Name); } } При запуске этого приложения в окне консоли будет выводиться имя каждого класса посредством метода GetType (), унаследованного от System.Object. (В главе 16 более подробно рассказывается об этом методе и предлагаемых в .NET службах рефлексии.) Исходный код. Проект ICloneableExample доступен в подкаталоге Chapter 9. Другим ограничением традиционных абстрактных базовых классов является то, что в каждом производном типе должен обязательно поддерживаться соответствующий набор абстрактных членов и предоставляться их реализация. Чтобы увидеть, в чем заключается проблема, давайте вспомним иерархию фигур, которая приводилась в главе 6. Предположим, что в базовом классе Shape определен новый абстрактный метод по имени GetNumberOf Points (), позволяющий производным типам возвращать информацию о количестве вершин, которое требуется для визуализации фигуры: abstract class Shape { // Каждый производный класс теперь должен // обязательно поддерживать такой метод! public abstract byte GetNumberOfPoints (); } Очевидно, что изначально единственным типом, который в принципе имеет вершины, является Hexagon. Но теперь, из-за внесенного обновления, каждый производный класс (Circle, Hexagon и ThreeDCircle) должен предоставлять конкретную реализацию метода GetNumberOf Points (), даже если в этом нет никакого смысла. В этом случае снова на помощь приходит тип интерфейса. Определив интерфейс, представляющий поведение "наличие вершин", можно будет просто вставить его в тип Hexagon и не трогать типы Circle и ThreeDCircle.
324 Часть III. Дополнительные конструкции программирования на С# Определение специальных интерфейсов Теперь, имея более четкое представление о роли интерфейсов, давайте рассмотрим пример определения и реализации специальных интерфейсов. Создадим новый проект типа Console Application по имени Customlnterface и, выбрав в меню Project (Проект) пункт Add Existing Item (Добавить существующий элемент), вставим в него файл или файлы, содержащие определения типов фигур (файл Shapes.cs в примерах кода), которые были созданы в главе 6. После этого переименуем пространство имен, в котором содержатся определения отвечающих за фигуры типов, в Customlnterface (просто чтобы не импортировать их из этого пространства имен в новый проект): namespace Customlnterface { // Здесь должны идти определения // созданных ранее типов фигур... } Теперь вставим в проект новый интерфейс по имени I Pointy, выбрав в меню Project пункт Add Existing Item, как показано на рис. 9.1. Name: IPoinlycs Add Cancel Рис. 9.1. Интерфейсы, как и классы, могут определяться в любом файле * . cs На синтаксическом уровне любой интерфейс определяется с помощью ключевого слова interface. В отличие от классов, базовый класс (даже System.Object) для интерфейсов никогда не указывается (хотя, как будет показано позже в главе, базовые интерфейсы указываться могут). Более того, модификаторы доступа для членов интерфейсов тоже никогда не указываются (поскольку все члены интерфейсов всегда являются неявно общедоступными и абстрактными). Ниже показано, как должно выглядеть определение специального интерфейса IPointyHaC#: // Этот интерфейс определяет поведение "наличие вершин". public interface IPointy { // Этот член является неявно общедоступным и абстрактным. byte GetNumberOfPoints (); }
Глава 9. Работа с интерфейсами 325 Вспомните, что при определении членов интерфейсов область их реализации не задается. Интерфейсы являются чистым протоколом, и потому реализация в них никогда не предоставляется (за это отвечает поддерживающий класс или структура). Следовательно, использование показанной ниже версии I Pointy привело бы к возникновению различных ошибок на этапе компиляции: // Внимание! Полно ошибок! public interface IPointy { // Ошибка! Интерфейсы не могут иметь поля! public int numbOfPoints; // Ошибка! Интерфейсы не могут иметь конструкторы! public IPointy() { numbOfPoints = 0;}; // Ошибка! В интерфейсах не может предоставляться // реализация методов! byte GetNumberOfPoints () { return numbOfPoints; } } В любом случае, в начальном интерфейсе IPointy определен только один метод. Однако в .NET допустимо определять в типах интерфейсов любое количество прототипов свойств. Например, можно было бы создать интерфейс IPointy так, чтобы в нем использовалось доступное только для чтения свойство, а не традиционный метода доступа: // Определение в IPointy свойства, доступного только для чтения. public interface IPointy { // Свойство, доступное как для чтения, так и для записи, //в этом интерфейсе может выглядеть так: // retVal PropName { get; set; } //а свойство, доступное только для записи - так: // retVal PropName { set; } byte Points { get; } } На заметку! Типы интерфейсов также могут содержать определения событий (см. главу 11) и индексаторов (см. главу 12). Сами по себе типы интерфейсов довольно бесполезны, поскольку представляют собой не более чем просто именованную коллекцию абстрактных членов. Например, размещать типы интерфейсов таким же образом, как классы или структуры, не разрешается: // Внимание! Размещать типы интерфейсов не допускается! static void Main(string[] args) { IPointy p = new IPointy(); // Компилятор сообщит об ошибке! } Интерфейсы ничего особого не дают, если не реализуются в каком-то классе или структуре. Здесь IPointy представляет собой интерфейс, который выражает поведение "наличие вершин". Стоящая за его созданием идея выглядит довольно просто: некоторые классы в иерархии фигур (например, Hexagon) должны иметь вершины, а некоторые (вроде Circle) — нет.
326 Часть III. Дополнительные конструкции программирования на С# Реализация интерфейса Чтобы расширить функциональность какого-то класса или структуры за счет обеспечения в нем поддержки интерфейсов, необходимо предоставить в его определении список соответствующих интерфейсов, разделенных запятыми. Следует иметь в виду, что непосредственный базовый класс должен быть обязательно перечислен в этом списке первым, сразу же после двоеточия. Когда тип класса наследуется прямо от System.Object, допускается перечислять только лишь поддерживаемый им интерфейс или интерфейсы, поскольку компилятор С# автоматически расширяет типы возможностями System.Object в случае, если не было указано иначе. Из-за того, что структуры всегда наследуются от класса System. ValueType (см. главу 4), интерфейсы просто должны перечисляться после определения структуры. Ниже приведены примеры. // Этот класс унаследован от System.Object //и реализует единственный интерфейс. public class Pencil : IPointy // Этот класс тоже унаследован от System.Object //и реализует единственный интерфейс. public class SwitchBlade : object, IPointy // Этот класс унаследован от специального базового // класса и реализует единственный интерфейс. public class Fork : Utensil, IPointy // Эта структура неявно унаследована от System.ValueType //и реализует два интерфейса. public struct Arrow : IClonable, IPointy Важно понимать, что реализация интерфейса работает по принципу "все или ничего". Поддерживающий тип не способен выбирать, какие члены должны быть реализованы, а какие — нет. Из-за того, что в интерфейсе IPointy определено лишь одно доступное только для чтения свойство, нагрузка на поддерживающий тип получается не такой уж большой. Однако в случае реализации интерфейса с десятью членами (такого как показанный ранее IDbConnection) типу придется отвечать за воплощение деталей всех десяти абстрактных сущностей. Давайте вернемся к рассматриваемому примеру и добавим в него новый тип класса по имени Triangle, унаследованный от Shape и поддерживающий интерфейс IPointy. Обратите внимание, что реализация доступного только для чтения свойства Points в нем предусматривает просто возврат соответствующего количества вершин (в данном случае 3). // Новый производный от Shape класс по имени Triangle. class Triangle : Shape, IPointy { public Triangle () { } public Triangle(string name) : base(name) { } public override void Draw() { Console.WriteLine("Drawing {0} the Triangle", PetName); } }
Глава 9. Работа с интерфейсами 327 // Реализация интерфейса IPointy. public byte Points { get { return 3; } } } Модифицируем существующий тип Hexagon так, чтобы он тоже поддерживал интерфейс IPointy: // Hexagon теперь реализует IPointy. class Hexagon : Shape, IPointy public Hexagon () { } public Hexagon(string name) public override void Draw() { Console.WriteLine("Drawing {0 base(name){ } the Hexagon", PetName); // Реализация интерфейса IPointy. public byte Points get { return 6; } } } Чтобы подвести итог всему изученному к этому моменту, на рис. 9.2 приведена созданная с помощью Visual Studio 2010 диаграмма классов, на которой все совместимые с IPointy классы представлены с помощью обозначения в виде "леденца на палочке". Обратите внимание на диаграмме, что в Circle и ThreeDCircle интерфейс IPointy не реализован, потому что предоставляемое им поведение для этих классов не имеет смысла. Shape Abstract Class :; J IPointy J= Circle Class ■+ Shape i?; Triangle Class -* Shape z f ThreeDCircle Class + Circle в] J U IPointy Class •* Shape Рис. 9.2. Иерархия фигур, теперь с интерфейсами На заметку! Чтобы скрыть или отобразить имена интерфейсов в визуальном конструкторе классов, щелкните правой кнопкой мыши на значке, представляющем интерфейс, и выберите в контекстном меню пункт Collapse (Свернуть) или Expand (Развернуть).
328 Часть III. Дополнительные конструкции программирования на С# Вызов членов интерфейса на уровне объектов Теперь, когда уже есть несколько классов, поддерживающих интерфейс I Pointy, давайте посмотрим, как взаимодействовать с новой функциональностью. Самый простой способ взаимодействия с функциональными возможностями, предлагаемыми заданным интерфейсом, предусматривает вызов его членов прямо на уровне объектов (при условии, что члены этого интерфейса не реализованы явным образом, о чем более подробно рассказывается в разделе "Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом" далее в главе). Например, рассмотрим следующий метод Main (): static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Interfaces *****\n"); // Вызов свойства Points, определенного в IPointy. Hexagon hex = new Hexagon(); Console.WriteLine("Points: {0}", hex.Points); // вывод числа вершин Console.ReadLine (); } Такой подход конкретно в данном случае вполне подходит, поскольку здесь точно известно, что в типе Hexagon был реализован запрашиваемый интерфейс и, следовательно, поддерживается свойство Points. Однако в других случаях определить, какие интерфейсы поддерживает данный тип, может быть невозможно. Например, предположим, что имеется массив, содержащий 50 совместимых с Shape типов, при этом интерфейс IPointy поддерживает только частью из них. Очевидно, что при попытке вызвать свойство Points для типа, в котором не был реализован IPointy, будет возникать ошибка. Как динамически определить, поддерживает ли класс или структура нужный интерфейс? Одним из способов для определения во время выполнения того, поддерживает ли тип конкретный интерфейс, является применение операции явного приведения. В случае если тип не поддерживает запрашиваемый интерфейс, будет генерироваться исключение InvalidCastException, для аккуратной обработки которого можно использовать методику структурированной обработки исключений, как, например, показано ниже: static void Main(string[] args) { // Перехват возможного исключения InvalidCastException. Circle с = new Circle ("Lisa"); IPointy ltfPt = null; try { ltfPt = (IPointy)c; Console.WriteLine(ltfPt.Points); } catch (InvalidCastException e) { Console.WriteLine(e.Message); } Console.ReadLine (); } Разумеется, можно использовать логику try/catch и надеяться на лучшее, но в идеале все-таки правильнее выяснять, какие интерфейсы поддерживаются, перед вызовом их членов. Давайте рассмотрим два способа, которыми это можно делать.
Глава 9. Работа с интерфейсами 329 Получение ссылок на интерфейсы с помощью ключевого слова as Определить, поддерживает ли данный тип тот или иной интерфейс, можно с использованием ключевого слова as, которое впервые рассматривалось в главе 6. Если объект удается интерпретировать как указанный интерфейс, то возвращается ссылка на интересующий интерфейс, а если нет, то ссылка null. Следовательно, перед продолжением в коде необходимо предусмотреть проверку на null: static void Main(string [ ] args) { // Можно ли интерпретировать hex2 как IPointy? Hexagon hex2 = new Hexagon("Peter"); IPointy itfPt2 = hex2 as IPointy; if (itfPt2 != null) // Вывод числа вершин. Console.WriteLine ("Points: {0}", itfPt2.Points); else // Это не интерфейс IPointy. Console.WriteLine ("OOPS! Not pointy..."); Console.ReadLine (); } Обратите внимание, что в случае применения ключевого слова as использовать логику try/catch нет никакой необходимости, поскольку возврат ссылки, отличной от null, означает, что вызов осуществляется с использованием действительной ссылки на интерфейс. Получение ссылок на интерфейсы с помощью ключевого слова is Проверить, был ли реализован нужный интерфейс, можно также с помощью ключевого слова is (которое тоже впервые упоминалось в главе 6). Если запрашиваемый объект не совместим с указанным интерфейсом, возвращается значение false, а если совместим, то можно спокойно вызывать члены этого интерфейса без применения логики try/catch. Для примера предположим, что имеется массив типов Shape, некоторые из членов которого реализуют интерфейс IPointy. Ниже показано, как можно определить, какие из элементов в этом массиве поддерживают данный интерфейс с помощью ключевого слова is внутри обновленной соответствующим образом версии метода Main (): static void Main(string [ ] args) { Console.WriteLine("***** Fun with Interfaces *****\n"); // Создание массива типов Shapes. Shape[] myShapes = { new Hexagon (), new Circle(), new Triangle("Joe"), new Circle("JoJo")} ; for(int i = 0; i < myShapes.Length; i++) { // Вспомините, что в базовом классе Shape определен абстрактный // метод Draw(), поэтому все фигуры знают, как себя рисовать. myShapes[i].Draw(); //У каких фигур есть вершины? if (myShapes[i] is IPointy) // Вывод числа вершин. Console.WriteLine("-> Points: {0}", ((IPointy) myShapes[l]).Points); else
330 Часть III. Дополнительные конструкции программирования на С# // Это не интерфейс IPointy. Console.WriteLine ("-> {0}\'s not pointy!", myShapes[i].PetName); Console.WriteLine(); } Console.ReadLine (); } Ниже приведен вывод, полученный в результате выполнения этого кода: ***** Fun W1th Interfaces ***** Drawing NoName the Hexagon -> Points : 6 Drawing NoName the Circle -> NoName's not pointy! Drawing Joe the Triangle -> Points: 3 Drawing JoJo the Circle -> JoJo's not pointy! Использование интерфейсов в качестве параметров Благодаря тому, что интерфейсы являются допустимыми типами .NET, можно создавать методы, принимающие интерфейсы в качестве параметров, вроде метода CloneMe (), который был показан ранее в главе. Для примера предположим, что определен еще один интерфейс по имени IDraw3D: // Моделирует способность визуализировать тип в трехмерном формате. public interface IDraw3D { void Draw3D(); } Далее сконфигурируем две из трех наших фигур (а именно — Circle и Hexagon) таким образом, чтобы они поддерживали это новое поведение: // Circle поддерживает IDraw3D. class ThreeDCircle : Circle, IDraw3D { public void Draw3D() { Console.WriteLine("Drawing Circle in 3D!"); } } // Hexagon поддерживает IPointy и IDraw3D. class Hexagon : Shape, IPointy, IDraw3D { public void Draw3D() { Console.WriteLine("Drawing Hexagon in 3D!"); } } На рис. 9.3 показано, как после этого выглядит диаграмма классов в Visual Studio 2010. Если теперь определить метод, принимающий интерфейс IDraw3D в качестве параметра, то ему можно будет передавать, по сути, любой объект, реализующий интерфейс IDraw3D (при попытке передать тип, не поддерживающий необходимый интерфейс, компилятор сообщит об ошибке). Например, давайте определим в классе Program следующий метод:
Глава 9. Работа с интерфейсами 331 // Будет рисовать любую фигуру, поддерживающую IDraw3D. static void DrawIn3D(IDraw3D itf3d) { Console.WriteLine ("-> Drawing IDraw3D compatible type"); itf3d.Draw3D(); Circle Class •♦Shape .1 . ... _ LL 0 IDrawi ThreeDCiri Class -» Circle " . D fc (iT ~i : Shape j Abstract Class <Z» IPoi ! Triangle | Class •♦Shape T 1 nty № Щ ^J IPointy Interface В Properties ч IPointy IDraw3D Hexagon Class -«•Shape ■Щ ® IDraw3D Interlace "Methods ♦ Draw3D v a Рис. 9.З. Обновленная иерархия фигур Теперь можно проверить, поддерживает ли элемент в массиве Shape новый интерфейс, и если да, то передать его методу DrawIn3D () на обработку: static void Main(string[] args) { Console.WriteLine ("***** Fun with Interfaces *****\n"); Shape[] myShapes = { new Hexagon (), new Circle (), new Triangle (), new Circle("JoJo") } ; for(int i = 0; i < myShapes.Length; i++) { // Можно ли нарисовать эту фигуру в трехмерном формате? if(myShapes [i] is IDraw3D) DrawIn3D((IDraw3D)myShapes [ 1]); } } Ниже показано, как будет выглядеть вывод в результате выполнения этой модифицированной версии приложения. Обратите внимание, что в трехмерном формате отображается только объект Hexagon, поскольку все остальные члены массива Shape не реализуют интерфейса IDraw3D. ***** pun with Interfaces ***** Drawing NoName the Hexagon -> Points: 6 -> Drawing IDraw3D compatible type Drawing Hexagon in 3D! Drawing NoName the Circle -> NoName's not pointy! Drawing Joe the Triangle -> Points: 3 Drawing JoJo the Circle -> JoJo's not pointy!
332 Часть III. Дополнительные конструкции программирования на С# Использование интерфейсов в качестве возвращаемых значений Интерфейсы можно также использовать и в качестве возвращаемых значений методов. Для примера создадим метод, который принимает в качестве параметра массив объектов System.Object и возвращает ссылку на первый элемент, поддерживающий интерфейс I Pointy: // Этот метод возвращает первый объект в массиве, // который реализует интерфейс IPointy. { foreach (Shape s in shapes) { if (s is IPointy) return s as IPointy; } return null; } Взаимодействовать с этим методом можно следующим образом: static void Main(string[ ] args) { Console.WriteLine ("***** Fun with Interfaces *****\n"); // Создание массива объектов Shapes. Shape [ ] myShapes = { new Hexagon(), new Circle (), new Triangle("Joe"), new Circle("JoJo")}; // Получение первого элемента, который имеет вершины. // Ради безопасности не помешает предусмотреть проверку // firstPointyltem на предмет равенства null. IPointy firstPointyltem = FindFirstPointyShape(myShapes); // Вывод числа вершин. Console.WriteLine("The item has {0} points", firstPointyltem.Points); } Массивы типов интерфейсов Вспомните, что один и тот же интерфейс может быть реализован во множестве типов, даже если они находятся не в одной и той же иерархии классов и не имеют никакого общего родительского класса, помимо System. Object. Это позволяет формировать очень мощные программные конструкции. Например, давайте создадим в текущем проекте три новых типа класса, два из которых (Knife (нож) и Fork (вилка)) будут представлять кухонные принадлежности, а третий (PitchFork (вилы)) — инструмент для работы в саду (рис. 9.4). Имея определения типов PitchFork, Fork и Knife, можно определить массив объектов, совместимых с IPointy. Поскольку все эти члены поддерживают один и тот же интерфейс, можно выполнять проход по массиву и интерпретировать каждый его элемент как совместимый с IPointy объект, несмотря на разницу между иерархиями классов. static void Main(string [ ] args) { //В этом массиве могут содержаться только типы, // которые реализуют интерфейс IPointy. IPointy[] myPointyObjects = {new Hexagon (), new Knife(), new Triangle (), new Fork(), new PitchFork()};
Глава 9. Работа с интерфейсами 333 foreach(IPointy i in myPointyObjects) // Вывод числа вершин. Console.WriteLine("Object has {0} points.", 1.Points); Console.ReadLine() ; Shape Abstract Class ■ J ' IPointy ( Circle Class •♦Shape ^ .. U IDrawi D ThreeDCkcle Class ■* Circle ®1 ) '*! J Tria Class -♦Shape Щ. I ~IPoint> IDrawBD Hexagon Class -♦Shape V —J О IPointy IPointy Fork r yn IPointy PrtchFork Class ч..г _\ ■ _.^_ rs 1 Knife I Class f» Рис. 9.4. Вспомните, что интерфейсы могут "встраиваться" в любой тип внутри любой части иерархии классов Исходный код. Проект Customlnterf асе доступен в подкаталоге Chapter 9. Реализация интерфейсов с помощью Visual Studio 2010 Хотя программирование с применением интерфейсов и является очень мощной технологией, реализация интерфейсов может сопровождаться довольно приличным объемом ввода. Поскольку интерфейсы представляют собой именованные наборы абстрактных членов, для каждого метода интерфейса в каждом типе, который должен поддерживать такое поведение, требуется вводить и определение, и реализацию. К счастью, в Visual Studio 2010 поддерживаются различные инструменты, которые существенно упрощают процесс реализации интерфейсов. Для примера давайте вставим в текущий проект еще один класс по имени PomtyTestClass. При реализации для него интерфейса IPointy (или любого другого подходящего интерфейса) можно будет заметить, как по окончании ввода имени интерфейса (или при размещении на нем курсора мыши в окне кода) первая буква будет выделена подчеркиванием (или, согласно формальной терминологии, снабжена так называемой контекстной меткой — смарт-тегом (smart tag)). В результате щелчка на этом смарт-теге появится раскрывающийся список с различными возможными вариантами реализации этого интерфейса (рис. 9.5).
334 Часть III. Дополнительные конструкции программирования на С# PointyTestCliss.es* X Щ ^iCustomlnterface.PointyTestClass fusing System; | using System.Collections.Ge using Systea.Linq; Lusing System.Text; ^namespace Customlnterface ф class PointyTestClass : i i l У 1} Рис. 9.5. Реализация интерфейсов в Visual Studio 2010 Обратите внимание, что в этом списке предлагаются два варианта, причем второй из них (реализация интерфейса явным образом) подробно рассматривается в следующем разделе. Пока что выберем первый вариант. В этом случае Visual Studio 2010 сгенерирует (внутри именованного раздела кода) показанный ниже удобный для дальнейшего обновления код-заглушку (обратите внимание, что в реализации по умолчанию предусмотрена генерация исключения System.NotlmplementedException, что вполне можно удалить). namespace Customlnterface { class PointyTestClass : IPointy { #region IPointy Members public byte Points { get { throw new NotlmplementedException (); } } #endregion } } На заметку! В Visual Studio 2010 также поддерживается опция рефакторинга типа выделения интерфейса (Extract Interface), которая доступа в меню Refactoring (Рефакторинг). Она позволяет извлекать определение нового интерфейса из существующего определения класса. Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом Как было показано ранее в главе, единственный класс или структура может реализовать любое количество интерфейсов. Из-за этого всегда существует вероятность реализации интерфейсов с членами, имеющими идентичные имена, и, следовательно, возникает необходимость в устранении конфликтов на уровне имен. Чтобы ознакомиться с различными способами решения этой проблемы, давайте создадим новый проект типа Console Application по имени Interf aceNameClash и добавим в него три специальных интерфейса, представляющих различные места, в которых реализующий их тип может визуализировать свой вывод: IPointy ш s Implement interface IPomty* к Explicitly implement interface IPointy'
Глава 9. Работа с интерфейсами 335 // Прорисовывание изображения в форме. public interface IDrawToForm void Draw(); // Отправка изображения в буфер в памяти. public interface IDrawToMemory void Draw(); // Вывод изображения на принтере. public interface IDrawToPrinter void Draw () ; Обратите внимание, что в каждом из этих интерфейсов присутствует метод по имени Draw () с идентичной сигнатурой (без аргументов). Если теперь необходимо, чтобы каждый из этих интерфейсов поддерживался в одном классе по имени Octagon, компилятор позволит использовать следующее определение: class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter { public void Draw() { // Совместно используемая логика рисования. Console.WriteLine("Drawing the Octagon..."); Хотя компиляция этого кода пройдет гладко, одна возможная проблема в нем все- таки присутствует. Попросту говоря, предоставление единой реализации метода Draw () не позволяет предпринимать уникальные действия на основе того, какой интерфейс получен от объекта Octagon. Например, следующий код будет приводить к вызову одного и того же метода Draw (), какой бы интерфейс не был получен: static void Main(string[] args) { Console.WriteLine ("***** Fun with Interface Name Clashes *****\n"); //Во всех этих случаях будет вызываться один и тот же метод DrawQ ! Octagon oct = new Octagon (); oct.Draw (); IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw (); IDrawToPrinter itfPriner = (IDrawToPrinter)oct; itfPriner.Draw(); IDrawToMemory itfMemory = (IDrawToMemory)oct; ltfMemory.Draw(); Console.ReadLine(); } Очевидно, что код, требуемый для визуализации изображения в окне, довольно сильно отличается от того, который необходим для визуализации изображения на сетевом принтере или в области памяти. При реализации нескольких интерфейсов, имеющих идентичные члены, можно разрешать подобный конфликт на уровне имен за счет применения так называемого синтаксиса явной реализации интерфейсов. Например, модифицируем тип Octagon следующим образом:
336 Часть III. Дополнительные конструкции программирования на С# class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter { // Явная привязка реализаций DrawQ к конкретному интерфейсу. void IDrawToForm.Draw () Console.WriteLine("Drawing to form..."); void IDrawToMemory.Draw () Console.WriteLine("Drawing to memory..."); void IDrawToPrinter.Draw () Console.WriteLine("Drawing to a printer..."); Как здесь показано, при явной реализации члена интерфейса схема, которой нужно следовать, в общем случае выглядит следующим образом: возвращаемыйТип ИмяИнтерфейса .ИмяМетода (параметры) Обратите внимание, что в этом синтаксисе указывать модификатор доступа не требуется, поскольку члены, реализуемые явным образом, автоматически считаются приватными. Например, следующий код является недопустимым: // Ошибка! Модификатора доступа быть не должно! public void IDrawToForm.Draw() { Console.WriteLine("Drawing to form..."); } Из-за того, что реализуемые явным образом члены всегда неявно считаются приватными, они перестают быть доступными на уровне объектов. На самом деле, если применить к типу Octagon операцию точки, никаких членов Draw () в списке IntelliSense отображаться не будет (рис. 9.6). Program.cs" X J$InterfaceNameCfcash.Program — * I j^Maintstringfl args) static void Main(string[] args) { Console.WriteLine("***** Fun with Interface Name Clashes *** // All of these invocations call the // sane Draw() method! Octagon oct - new Octagon(); oct. IDraj itfri . IOr J * E4UalS |<Ctrl+Alt+Space> j^fpj ■'♦ GetHashCode к Юг J ■• GetType ** itfH ♦ ToString Con sole.Read LineG; |ioForm)oct; tDrawToPrinter)oct; 3rawToMemory)oct; *\n ID Рис. 9.6. Реализуемые явным образом члены интерфейсов перестают быть доступными на уровне объектов Как и следовало ожидать, для получения доступа к необходимым функциональным возможностям в таком случае потребуется явное приведение, например:
Глава 9. Работа с интерфейсами 337 static void Main(string[] args) { Console.WriteLine ("***** Fun with Interface Name Clashes *****\n"); Octagon oct = new Octagon (); // Теперь для получения доступа к членам Draw() // должно использоваться приведение. IDrawToForm itfForm = (IDrawToForm)oct; ltfForm.Draw() ; // Сокращенная версия на случай, если переменную интерфейса //не планируется использовать позже. ( (IDrawToPrinter)oct) .Draw() ; // Можно было бы также использовать ключевое слово as. if (oct is IDrawToMemory) ( (IDrawToMemory)oct) .Draw(); Console.ReadLine(); } Хотя такой синтаксис довольно полезен, когда необходимо устранить конфликты на уровне имен, приемом явной реализации интерфейсов можно пользоваться и просто для сокрытия более "сложных" членов на уровне объектов. В таком случае при применении операции точки пользователь объекта будет видеть только некоторую часть общей функциональности типа. Те же пользователи, которым необходим доступ к более сложным поведениям, все равно смогут получать их из желаемого интерфейса через явное приведение. Исходный код. Проект InterfaceNameClash доступен в подкаталоге Chapter 9. Проектирование иерархий интерфейсов Интерфейсы могут быть организованы в иерархии. Как и в иерархии классов, в иерархии интерфейсов, когда какой-то интерфейс расширяет существующий, он наследует все абстрактные члены своего родителя (или родителей). Конечно, в отличие от классов, производные интерфейсы никогда не наследуют саму реализацию. Вместо этого они просто расширяют собственное определение за счет добавления дополнительных абстрактных членов. Использовать иерархию интерфейсов может быть удобно, когда нужно расширить функциональность определенного интерфейса без нарушения уже существующих кодовых баз. Для примера создадим новый проект типа Console Application по имени InterfaceHierarchyn добавим в него новый набор отвечающих за визуализацию интерфейсов так, чтобы IDrawable был корневым интерфейсом в дереве этого семейства: public interface IDrawable { void Draw (); } Поскольку в интерфейсе IDrawable определяется лишь базовое поведение рисования, мы теперь можем создать производный от него интерфейс, который расширяет его функциональность, добавляя возможность выполнения визуализации в других форматах, например: public interface IAdvancedDraw : IDrawable { void DrawInBoundingBox(int top, int left, int bottom, int right); void DrawUpsideDown (); }
338 Часть III. Дополнительные конструкции программирования на С# При таком дизайне для реализации интерфейса IAdvancedDraw в классе потребуется реализовать каждый из определенных в цепочке наследования членов (т.е. Draw (), DrawInBoundingBox () и DrawUpsideDown()): public class Bitmaplrnage : IAdvancedDraw { public void Draw() Console.WriteLine("Drawing..."); public void DrawInBoundingBox (int top, int left, int bottom, int right) Console.WriteLine("Drawing in a box..."); public void DrawUpsideDown () Console.WriteLine("Drawing upside down'"); } Теперь при использовании Bitmaplrnage можно вызывать каждый метод на уровне объекта (поскольку все они являются общедоступными), а также извлекать ссылку на каждый поддерживаемый интерфейс явным образом с помощью приведения: static void Main(string [ ] args) { Console.WriteLine ("*****Simple Interface Hierarchy *****"); // Выполнение вызова на уровне объекта. Bitmaplrnage myBitmap = new Bitmaplrnage(); myBitmap.Draw(); myBitmap.DrawInBoundingBoxA0, 10, 100, 150); myBitmap.DrawUpsideDown (); // Получение IAdvancedDraw явным образом. IAdvancedDraw iAdvDraw; iAdvDraw = (IAdvancedDraw)myBitmap; iAdvDraw.DrawUpsideDown(); Console.ReadLine (); } Исходный код. Проект InterfaceHierarchy доступен в подкаталоге Chapter 9. Множественное наследование в случае типов интерфейсов В отличие от классов, один интерфейс может расширять сразу несколько базовых интерфейсов, что позволяет проектировать очень мощные и гибкие абстракции. Для примера создадим новый проект типа Console Application по имени MI Interf aceHierarchy и добавим в него еще одну коллекцию интерфейсов, моделирующих различные связанные с визуализацией и фигурами абстракции. Обратите внимание, что интерфейс I Shape в этой коллекции расширяет оба интерфейса IDrawable и I Printable. // Множественное наследование в случае типов // интерфейсов является вполне допустимым. interface IDrawable { void Draw (); }
Глава 9. Работа с интерфейсами 339 interface IPrintable { void Print(); void Draw(); // <-- Здесь возможен конфликт имен! } // Множественное наследование интерфейса. Все в порядке! public interface IShape : IDrawable, IPrintable { int GetNumberOfSides(); } На рис. 9.7 показано, как теперь выглядит иерархия интерфейсов. IDrawable ~®\ ZJ I* Interface •* IDrawable -* IPrintable Рис. 9.7. В отличие от классов, интерфейсы могут расширять • сразу несколько базовых интерфейсов Теперь главный вопрос состоит в том, сколько методов потребуется реализовать при создании класса, поддерживающего IShape? Ответ будет несколько. Если нужно предоставить простую реализацию метода Draw (), понадобится только реализовать три его члена, как показано ниже на примере типа Rectangle: class Rectangle : IShape { public int GetNumberOfSides () { return 4; } public void Draw() { Console.WriteLine("Drawing. ..") ; } public void Print() { Console.WriteLine("Prining..."); } При желании предоставить более конкретные реализации для каждого метода Draw () (что в данном случае имеет больше всего смысла) придется разрешать конфликт на уровне имен счет применения синтаксиса реализации интерфейсов явным образом, как показано ниже на примере типа Square: class Square : IShape { // Применение синтаксиса явной реализации для устранения // конфликта между именами членов. void IPrintable.Draw () { // Рисование на принтере ... } void IDrawable.Draw() { // Рисование на экране ... }
340 Часть III. Дополнительные конструкции программирования на С# public void Print () { // Печать.. . } public int GetNumberOfSides() { return 4; } } К этому моменту процесс определения и реализации специальных интерфейсов на С# должен стать более понятным. По правде говоря, на привыкание к программированию с применением интерфейсов может уйти некоторое время. Однако уже сейчас важно уяснить, что интерфейсы являются фундаментальным компонентом .NET Framework. Какого бы типа приложение не разрабатывалось (веб- приложение, приложение с настольным графическим интерфейсом, библиотека доступа к данными и т.п.), работа с интерфейсами будет обязательной частью этого процесса. Подводя итог всему изложенному, отметим, что интерфейсы могут приносить чрезвычайную пользу в следующих случаях. • При наличии единой иерархии, в которой только какой-то набор производных типов поддерживает общее поведение. • При необходимости моделировать общее поведение, которое должно встречаться в нескольких иерархиях, не имеющих общего родительского класса помимо System.Object. Теперь, когда мы разобрались со специфическими деталями построения и реализации специальных интерфейсов, необходимо посмотреть, какие стандартные интерфейсы предлагаются в библиотеках базовых классов .NET. Исходный код. Проект MI Inter faceHierarchy доступен в подкаталоге Chapter 9. Создание перечислимых типов (IEnumerable и IEnumerator) Прежде чем приступать к исследованию процесса реализации существующих интерфейсов .NET, давайте сначала рассмотрим роль типов IE numerable и IEnumerator. Вспомните, что в С# поддерживается ключевое слово f oreach, которое позволяет осуществлять проход по содержимому массива любого типа: // Итерация по массиву элементов. int[] myArrayOflnts = {10, 20, 30, 40}; foreach(int i in myArrayOflnts) { Console.WriteLine (i); } Хотя может показаться, что данная конструкция подходит только для массивов, на самом деле с ее помощью можно анализировать любой тип, который поддерживает метод GetEnumerator (). Для примера создадим новый проект типа Console Application no имени CustomEnumerator и добавим в него файлы Car.cs и Radio.cs, которые были определены в примере SimpleException в главе 7 (выбрав в меню Project (Проект) пункт Add Existing Item (Добавить существующий элемент)). На заметку! Может возникнуть желание переименовать содержащее типы Саг и Radio пространство имен в CustomEnumerator, чтобы не импортировать в новый проект пространство имен CustomException.
Глава 9. Работа с интерфейсами 341 Вставим новый класс Garage (гараж), обеспечивающий сохранение ряда объектов Саг (автомобиль) внутри System.Array: // Garage содержит набор объектов Саг. public class Garage { private Car [ ] carArray = new Car[4]; // Заполнение какими-то объектами Car при запуске. public Garage() { carArray[0] = new Car("Rusty", 30); carArray[1] = new Car("Clunker", 55); carArray[2] = new Car ("Zippy", 30); carArray [3] = new Car ("Fred", 30); } } В идеале удобно было бы осуществлять проход по элементам объекта Garage как по массиву значений данных с применением конструкции f oreach: // Такой вариант кажется целесообразным... public class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with IEnumerable / IEnumerator *****\n"); Garage carLot = new Garage(); // Проход по всем объектам Car в коллекции? foreach (Car с in carLot) { Console.WriteLine ("{0} is going {1} MPH", c.PetName, c. Speed); } Console.ReadLine (); } } К сожалению, в этом случае компилятор сообщит, что в классе Garage не реализован метод GetEnumerator (). Этот метод формально определен в интерфейсе IEnumerable, который находится глубоко внутри пространства имен System.Collections. На заметку! В следующей главе будет рассказываться о роли обобщений и пространства имен System.Collections.Generic. В этом пространстве имен содержатся обобщенные версии Enumerable и IEnumerator, которые предоставляют более безопасный в отношении типов способ для реализации прохода по подобъектам. Классы или структуры, которые поддерживают такое поведение, позиционируются как способные предоставлять содержащиеся внутри них подэлементы вызывающему коду (в данном примере это само ключевое слово foreach): // Этот интерфейс информирует вызывающий код о том, // что подэлементы объекта могут перечисляться. public interface IEnumerable { IEnumerator GetEnumerator (); } Метод GetEnumerator () возвращает ссылку на еще один интерфейс под названием System. Collections . IEnumerator. Этот интерфейс обеспечивает инфраструктуру,
342 Часть III. Дополнительные конструкции программирования на С# позволяющую вызывающему коду проходить по внутренним объектам, которые содержатся в совместимом с IEnumerable контейнере: // Этот интерфейс позволяет вызывающему коду получать // содержащиеся в контейнере внутренние подэлементы. public interface IEnumerator { bool MoveNext (); // Перемещение внутренней позиции курсора. object Current { get;} // Извлечение текущего элемента // (свойство, доступное только для чтения). void PesetO; // Сброс курсора перед первым членом. } При модификации типа Garage для поддержки этих интерфейсов можно пойти длинным путем и реализовать каждый метод вручную. Хотя, конечно же, предоставлять специализированные версии методов GetEnumerator (), MoveNext (), Current и Reset () вполне допускается, существует более простой путь. Поскольку в типе System.Array (как и во многих других классах коллекций) интерфейсы IEnumerable и IEnumerator уже реализованы, можно просто делегировать запрос к System.Array показанным ниже образом: using System.Collections; public class Garage : IEnumerable { // В System.Array интерфейс IEnumerator уже реализован! private Car [ ] carArray = new Car[4]; public Garage () { carArray = new Car[4]; carArrayfO] = new Car ( "FeeFee", 200, 0) ; carArray[l] = new Car ( "Clunker", 90, 0) ; carArray[2] = new Car ( "Zippy", 30, 0) ; carArray[3] = new Car ("Fred", 30, 0) ; } public IEnumerator GetEnumerator() { // Возврат интерфейса IEnumerator объекта массива. return carArray.GetEnumerator(); } } Изменив тип Garage подобным образом, теперь можно спокойно использовать его внутри конструкции foreach. Более того, поскольку метод GetEnumerator () был определен как общедоступный, пользователь объекта тоже может взаимодействовать с IEnumerator: // Работа с IEnumerator вручную IEnumerator i = carLot.GetEnumerator() ; i.MoveNext() ; Car myCar = (Car)l.Current; Console.WriteLine ("{0 } is going {1} MPH", myCar.PetName, myCar. CurrentSpeed); Если нужно скрыть функциональные возможности IEnumerable на уровне объекта, достаточно применить синтаксис явной реализации интерфейса: public IEnumerator IEnumerable.GetEnumerator () { // Возврат интерфейса IEnumerator объекта массива. return carArray.GetEnumerator();
Глава 9. Работа с интерфейсами 343 После этого обычный пользователь объекта не будет видеть метода GetEnumerator () в Garage, хотя конструкция f oreach будет все равно получать интерфейс незаметным образом, когда это необходимо. Исходный код. Проект CustomEnumerator доступен в подкаталоге Chapter 9. Создание методов итератора с помощью ключевого слова yield Раньше, когда требовалось создать специальную коллекцию (вроде Garage), способную поддерживать перечисление элементов посредством конструкции f oreach, реализация интерфейса IEnumerable (и возможно IEnumerator) была единственным доступным вариантом. Потом, однако, появился альтернативный способ для создания типов, способных работать с циклами f oreach, который предусматривает использование так называемых итераторов (iterator). Попросту говоря, итератором называется такой член, который указывает, каким образом должны возвращаться внутренние элементы контейнера при обработке в цикле f oreach. Хотя метод итератора должен все равно носить имя GetEnumerator (), а его возвращаемое значение относиться к типу IEnumerator, необходимость в реализации каких-либо ожидаемых интерфейсов в специальном классе при таком подходе отпадает. Для примера давайте создадим новый проект типа Console Application по имени CustomEnumeratorWithYield и вставим в него типы Car, Radio и Garage из предыдущего примера (при желании переименовав пространство имен в соответствии с текущим проектом), после чего модифицируем тип Garage показанным ниже образом: public class Garage { private Car[] carArray = new Car[4]; // Метод итератора. public IEnumerator GetEnumerator () { foreach (Car с in carArray) { yield return c; } } } Обратите внимание, что в данной реализации GetEnumerator () проход по подэле- ментам осуществляется с использованием внутренней логики foreach, а каждый объект Саг возвращается вызывающему коду с применением синтаксиса yield return. Ключевое слово yield служит для указания значения или значений, которые должны возвращаться конструкции foreach в вызывающем коде. При достижении оператора yield return производится сохранение текущего местоположении в контейнере, и при следующем вызове итератора выполнение начинается уже с этого места (детали будут описаны позже). Использовать ключевое слово foreach в методах итераторов для возврата содержимого не требуется. Метод итератора можно также определять следующим образом: public IEnumerator GetEnumerator () { yield return carArray[0]; yield return carArray[1]; yield return carArray[2]; yield return carArray[3]; }
344 Часть III. Дополнительные конструкции программирования на С# В этой реализации важно обратить внимание, что метод GetEnumerator () явным образом возвращает вызывающему коду новое значение после каждого прогона. В данном примере подобный подход не особо удобен, поскольку в случае добавления большего числа объектов в переменную экземпляра carArray метод GetEnumerator () вышел бы из-под контроля. Тем не менее, такой синтаксис все-таки может быть довольно полезным, когда необходимо возвращать из метода локальные данные для последующей обработки внутри f о reach. Создание именованного итератора Еще интересен тот факт, что ключевое слово yield формально может применяться внутри любого метода, как бы ни выглядело его имя. Такие методы (называемые именованными итераторами) уникальны тем, что могут принимать любое количество аргументов. При создании именованного итератора необходимо очень хорошо понимать, что метод будет возвращать интерфейс IE numerable, а не ожидаемый совместимый с IEnumerator тип. Для примера добавим к типу Garage следующий метод: public IEnumerable GetTheCars(bool ReturnRevesed) { // Возврат элементов в обратном порядке. if (ReturnRevesed) { for (int i = carArray. Length; 1 != 0; 1 —) yield return carArray[l-l]; } } else { // Возврат элементов в том порядке, //в котором они идут в массиве. foreach (Car с in carArray) { yield return с; } } } Обратите внимание, что добавленный новый метод позволяет вызывающему коду получать подэлементы как в прямом, так и в обратном порядке, если во входном параметре передается значение true. Теперь с ним можно взаимодействовать следующим образом: static void Main(string[ ] args) { Console.WriteLine ("***** Fun with the Yield Keyword *****\n"); Garage carLot = new Garage (); // Получение элементов с помощью GetEnumerator(). foreach (Car с in carLot) { Console.WriteLine(" {0} is going {1} MPH" , c.PetName, с.CurrentSpeed) ; } Console.WriteLine(); // Получение элементов (в обратном порядке) //с помощью именованного итератора.
Глава 9. Работа с интерфейсами 345 foreach (Car с in carLot.GetTheCars(true) ) { Console.WriteLine("{0} is going {1} MPH", c.PetName, с.CurrentSpeed); } Console.ReadLine() ; } Нельзя не согласиться с тем, что именованные итераторы представляют собой очень полезные конструкции, поскольку позволяют определять в единственном специальном контейнере сразу несколько способов для запрашивания возвращаемого набора. Внутреннее представление метода итератора Столкнувшись с методом итератора, компилятор С# динамически генерирует внутри соответствующего типа (в данном случае Garage) определение вложенного класса. В этом сгенерированном вложенном классе, в свою очередь, автоматически реализуются такие члены, как GetEnumerator (), MoveNext () и Current (но, как ни странно, не метод Reset (), и при попытке его вызвать возникает исключение времени выполнения). Если загрузить текущее приложение в утилиту ildasm.exe, можно обнаружить в нем два вложенных типа, в каждом из которых будет содержаться логика, необходимая для конкретного метода итератора. На рис. 9.8 видно, что эти сгенерированные автоматически компилятором типы имеют имена <GetEnumerator>d 0 и <GetTheCars>d 6. Р H:\My Books\C« Book\C# and the .NET Platform 5th ed\First DraftXCha... file View Help H:\My Books\C# Book\C# and the .NET Platform 5th ed\First Draft\Chapter _09\Code\CustomEi ► MANIFEST W CustomEnumeratorWithYield tf CustomEnumeratorWithYield. Car a £ CustomEnumeratorWithYield.Garage ► .class public auto ansi beforefieldinit ► implements [mscorlib]System.Collections.IEnumerable i £ <GetTheCars>d_6 v/ car Array : private class CustomEnumerator With Yield. Car[] ■ .ctor : void() ■ GetEnumerator: class [mscorlib]System.Collections.IEnumerator() ■ GetTheCars : class [mscorlib]System.Collections.IEnumerable(bool) : CustomEnumeratorWithYield.Program E CustomEnumeratorWithYield. Radio .assembly CustomEnumeratorWithYield Рис. 9.8. Методы итераторов внутренне реализуются с помощью автоматически сгенерированного вложенного класса Воспользовавшись утилитой ildasm.exe для просмотра реализации метода GetEnumerator () в типе Garage, можно обнаружить, что он был реализован так, чтобы в нем "за кулисами" использовался тип <GetEnumerator>d 0 (в методе GetTheCars () аналогичным образом используется вложенный тип <GetTheCars>d 6): .method public hidebysig instance class [mscorlib]System.Collections.IEnumerator GetEnumerator() cil managed newobj instance void CustomEnumeratorWithYield.Garage/'<GetEnumerator>d 0' : :.ctor (int32) } // end of method Garage::GetEnumerator
346 Часть III. Дополнительные конструкции программирования на С# В завершение темы построения перечислимых объектов запомните, что для того, чтобы специальные типы могли работать с ключевым словом foreach, в контейнере должен обязательно присутствовать метод по имени GetEnumerator (), который уже был формально определен типом интерфейса I Enumerable. Реализация данного метода обычно осуществляется за счет делегирования внутреннему члену, который отвечает за хранение подобъектов; однако можно также использовать синтаксис yield return и предоставлять с его помощью множество методов "именованных итераторов". Исходный код. Проект CustomEnumeratorWithYield доступен в подкаталоге Chapter 9. Создание клонируемых объектов (iCloneable) Как уже рассказывалось в главе 6, в System. Ob j ect имеется член по имени MemberwiseClone (). Он представляет собой метод и позволяет получить поверхностную копию (shallow copy) текущего объекта. Пользователи объекта не могут вызывать этот метод напрямую, поскольку он является защищенным, но сам объект вполне может это делать во время так называемого процесса клонирования. Для примера давайте создадим новый проект типа Console Application по имени CloneablePoint и добавим в него класс Point, представляющий точку. // Класс Point. public class Point { public int X { get; set; } public int Y { get; set; } public Point (int xPos, int yPos) { X = xPos; Y = yPos;} public Point () {} // Переопределение Object.ToStringO. public override string ToStringO { return string.Format("X = {0}; Y = {1}", X, Y ); } } Как уже должно быть известно из материала о ссылочных типах и типах значения (см. главу 4), в случае присваивания одной переменной ссылочного типа другой получается две ссылки, указывающие на один и тот же объект в памяти. Следовательно, показанная ниже операция присваивания будет приводить к получению двух ссылок, указывающих на один и тот же объект Point в куче, при этом внесение изменений с использованием любой из этих ссылок будет оказывать воздействие на тот же самый объект в куче: static void Main(string [] args) { Console.WriteLine ("***** Fun with Object Cloning *****\n"); // Две ссылки на один и тот же объект! Point pi = new Point E0, 50); Point p2 = pi; р2.Х = 0; Console.WriteLine (pi); Console.WriteLine(p2); Console.ReadLine(); }
Глава 9. Работа с интерфейсами 347 Чтобы обеспечить специальный тип способностью возвращать идентичную копию самого себя вызывающему коду, можно реализовать стандартный интерфейс I Clone able. Как уже показывалось в начале настоящей главы, этот интерфейс имеет единственный метод по имени Clone (): public interface ICloneable { object Clone (); } На заметку! По поводу полезности интерфейса ICloneable в сообществе .NET ведутся горячие споры. Проблема связана с тем, что в формальной спецификации никак явно не говорится о том, что объекты, реализующие данный интерфейс, должны обязательно возвращать детальную копию (deep copy) объекта (т.е. внутренние ссылочные типы объекта должны приводить к созданию совершенно новых объектов с идентичным состоянием). Из-за этого с технической точки зрения возможно, что объекты, реализующие ICloneable, на самом деле будут возвращать поверхностную копию интерфейса (т.е. внутренние ссылки будут указывать на один и тот же объект в куче), что вызывает приличную путаницу. В рассматриваемом примере предполагается, что метод Clone () должен быть реализован так, чтобы возвращать полную, детальную копию объекта. Разумеется, реализация метода Clone () в различных объектах может выглядеть по- разному. Однако базовая функциональность обычно остается неизменной и заключается в копировании переменных экземпляра в новый экземпляр объекта того же типа и в возврате его пользователю. Для примера изменим класс Point следующим образом: // Класс Point теперь поддерживает возможность клонирования. public class Point : ICloneable { public int X { get; set; } public int Y { get; set; } public Point (int xPos, int yPos) { X = xPos; Y = yPos; } public Point () { } // Переопределение Object.ToString () . public override string ToString () { return string.Format("X = {0} ; Y = {1} ", X, Y ) ; } // Возврат копии текущего объекта, public object Clone () { return new Point(this.X, this.Y); } } Теперь можно создавать точные автономные копии типа Point так, как показано ниже: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Object Cloning *****\n"); // Обратите внимание, что Clone() возвращает // простой тип объекта. Для получения производного // типа требуется явное приведение. Point рЗ = new Point A00, 100); Point p4 = (Point)p3.Clone (); // Изменение р4.Х (которое не приводит к изменению рЗ.х) . р4.Х = 0;
348 Часть III. Дополнительные конструкции программирования на С# // Вывод объектов на консоль. Console.WriteLine (рЗ); Console.WriteLine (p4); Console.ReadLine(); } Хотя текущая реализация Point отвечает всем требованиям, ее все равно можно немного улучшить. В частности, поскольку Point не содержит никаких внутренних переменных ссылочного типа, реализацию метода Clone () можно упростить следующим образом: public object Clone() { // Копируем каждое поле Point почленно. return this.MemberwiseClone() ; } Следует, однако, иметь в виду, что если бы в Point все-таки содержались внутренние переменные ссылочного типа, метод MemberwiseClone () копировал бы ссылки на эти объекты (т.е. создавал бы поверхностную копию). Тогда для поддержки построения детальной копии потребовалось бы создавать во время процесса клонирования новый экземпляр каждой из переменных ссылочного типа. Давайте рассмотрим соответствующий пример. Более сложный пример клонирования Теперь предположим, что в классе Point содержится переменная экземпляра ссылочного типа по имени PointDescription, предоставляющая удобное для восприятия имя вершины, а также ее идентификационный номер в виде System.Guid (глобально уникальный идентификатор GUID представляет собой статистически уникальное 128-битное число). Соответствующая реализация показана ниже. // Этот класс описывает точку. public class PointDescription { public string PetName {get; set;} public Guid PointID {get; set;} public PointDescription () { PetName = "No-name"; PointID = Guid.NewGuidO ; } } Как здесь видно, для начала был модифицирован сам класс Point, чтобы его метод ToStringO принимал во внимание подобные новые фрагменты данных о состоянии. Кроме того, был определен и создан ссылочный тип PointDescription. Чтобы позволить "внешнему миру" указывать желаемое дружественное имя (PetName) для Point, необходимо также изменить аргументы, передаваемые перегруженному конструктору. public class Point : ICloneable { public int X { get; set; } public int Y { get; set; } public PointDescription desc = new PointDescription(); public Point (int xPos, int yPos, string petName) { X = xPos; Y = yPos; desc.PetName = petName; }
Глава 9. Работа с интерфейсами 349 public Point(int xPos, int yPos) { X = xPos; Y = yPos; } public Eoint () { } // Переопределение Object.ToString () . public override string ToString () { return string. Format ("X = {0}; Y = {1}; Name = {2};\nID = {3}\n", X, Y, desc.PetName, desc.PointID); } // Возврат копии текущего объекта, public object Clone () { return this.MemberwiseClone (); } } Обратите внимание, что метод Clone () пока еще не обновлялся. Следовательно, в текущей реализации при запросе клонирования пользователем объекта будет создаваться поверхностная (почленная) копия. Чтобы удостовериться в этом, изменим метод Main () следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Object Cloning *****\n"); Console.WriteLine("Cloned p3 and stored new Point in p4"); Point p3 = new Point A00, 100, "Jane"); Point p4 = (Point) p3.Clone () ; //До изменения. Console.WriteLine("Before modification:"); Console.WriteLine ("p3: {0}", p3); Console.WriteLine ("p4 : {0}", p4); p4.desc.PetName = "My new Point; p4.X = 9; // После изменения. Console.WriteLine("\nChanged p4.desc.petName and p4.X"); Console.WriteLine("After modification:") ; Console.WriteLine("p3: {0}", p3); Console.WriteLine("p4: {0}", p4); Console.ReadLine(); } Ниже показано, как будет выглядеть вывод в таком случае. Обратите внимание, что хотя типы значения на самом деле изменились, у внутренних ссылочных типов остались те же самые значения, поскольку они "указывают" на одинаковые объекты в памяти. (В частности, дружественное имя у обоих объектов сейчас выглядит как My new Point.). ***** pun W1th Object Cloning ***** Cloned p3 and stored new Point in p4 Before modification: p3: X = 100; Y = 100; Name = Jane; ID = 133d66a7-0837-4bd7-95c6-b22ab0434509 p4: X = 100; Y = 100; Name = Jane; ID = 133d66a7-0837-4bd7-95c6-b22ab0434509 Changed p4.desc.petName and p4.X After modification: p3: X = 100; Y = 100; Name = My new Point; ID = 133d66a7-0837-4bd7-95c6-b22ab0434509° p4: X = 9; Y = 100; Name = My new Point; ID = 133d66a7-0837-4bd7-95c6-b22ab0434509
350 Часть III. Дополнительные конструкции программирования на С# Для того чтобы метод Clone () создавал полную детальную копию внутренних ссылочных типов, необходимо настроить возвращаемый методом MemberwiseClone () объект, чтобы он принимал во внимание имя текущего объекта Point (тип System.Guid на самом деле представляет собой структуру, поэтому в действительности числовые данные будут копироваться). Ниже показан один из возможных вариантов реализации. // Теперь необходимо подстроить код таким образом, чтобы // в нем принимался во внимание член PointDescription. public object Clone () { // Сначала получаем поверхностную копию. Point newPoint = (Point)this.MemberwiseClone() ; // Теперь заполняем пробелы. PointDescription currentDesc = new PointDescription(); currentDesc.PetName = this.desc.PetName; newPoint.desc = currentDesc; return newPoint; } Если теперь запустить приложение и посмотреть на его вывод (который показан ниже), то будет видно, что возвращаемый методом Clone () объект Point сейчас действительно начал копировать свои внутренние переменные экземпляра ссылочного типа (обратите внимание, что дружественные имена у рЗ и р4 теперь стали уникальными). ***** pun with Object Cloning ***** Cloned рЗ and stored new Point in p4 Before modification: p3: X = 100; Y = 100; Name = Jane; ID = 51f64f25-4b0e-47ac-ba35-37d263496406 p4: X = 100; Y = 100; Name = Jane; ID = 0d3776b3-bl59-490d-b022-7f3f60788e8a Changed p4.desc.petName and p4.X After modification: p3: X = 100; Y = 100; Name = Jane; ID = 51f64f25-4b0e-47ac-ba35-37d263496406 p4: X = 9; Y = 100; Name = My new Point; ID = 0d3776b3-bl59-490d-b022-7f3f60788e8a Подведем итог по процессу клонирования. При наличии класса или структуры, в которой не содержится ничего кроме типов значения, достаточно реализовать метод Clone () с использованием MemberwiseClone (). Однако если есть специальный тип, поддерживающий другие ссылочные типы, необходимо создать новый объект, принимающий во внимание каждую из переменных экземпляра ссылочного типа. Исходный код. Проект CloneablePoint доступен в подкаталоге Chapter 9. Создание сравнимых объектов (IComparable) Интерфейс System. IComparable обеспечивает поведение, которое позволяет сортировать объект на основе какого-то указанного ключа. Формально его определение выглядит так: // Этот интерфейс позволяет объекту указывать // его отношения с другими подобными объектами, public interface IComparable { int CompareTo(object o) ; }
Глава 9. Работа с интерфейсами 351 На заметку! В обобщенной версии этого интерфейса (IComparable<T>) предлагается более безопасный в отношении типов способ для обработки сравнений объектов. Обобщения будут более подробно рассматриваться в главе 10. Для примера создадим новый проект типа Console Application по имени ComparableCar и вставим в него следующую обновленную версию класса Саг (обратите внимание, что здесь просто добавлено новое свойство для представления уникального идентификатора каждого автомобиля и модифицированный конструктор): public class Car { public int CarlD {get; set;} public Car(string name, int currSp, int id) { CurrentSpeed = currSp; PetName = name; CarlD = id; } } Теперь создадим массив объектов Car, как показано ниже: static void Main(string[] args) { Console.WriteLine ("***** Fun with Object Sorting *****\n"); // Создание массива объектов Car. Car[] myAutos = new Car [5]; myAutos[0] = new Car("Rusty", 80, 1) ; myAutos[l] = new Car ("Mary", 40, 234); myAutos[2] = new Car("Viper", 40, 34); myAutos[3] = new Car ("Mel", 40, 4); myAutos[4] = new Car("Chucky", 40, 5) ; Console.ReadLine(); } В классе System. Array определен статический метод Sort (). При вызове этого метода на массиве внутренних типов (int, short, string и т.д.) элементы массива могут сортироваться в числовом или алфавитном порядке, поскольку эти внутренние типы данных реализуют интерфейс IComparable. Однако что будет происходить в случае передачи методу Sort () массива типов Саг, как показано ниже? // Будет ли выполняться сортировка автомобилей? Array.Sort(myAutos); В случае выполнения этого тестового кода в исполняющей среде будет возникать исключение, потому что в классе Саг необходимый интерфейс не поддерживается. При создании специальных типов для обеспечения возможности сортировки массивов, которые содержат элементы этих типов, можно реализовать интерфейс IComparable. При реализации деталей CompareTo () решение о том, что должно браться за основу в операции упорядочивания, необходимо принимать самостоятельно. Для рассматриваемого типа Саг с логической точки зрения наиболее подходящим на эту роль "кандидатом" является внутренняя переменная CarlD: // Упорядочивание элементов при итерации Саг // может производиться на основе CarlD. public class Car : IComparable {
352 Часть III. Дополнительные конструкции программирования на С# // Реализация IComparable. int IComparable.CompareTo(object obj) { Car temp = obj as Car; if (temp != null) { if (this.CarlD > temp.CarlD) return 1; if (this.CarlD < temp.CarlD) return -1; else return 0; } else throw new ArgumentException ("Parameter is not a Car!11); // Параметр не является объектом типа Car! } } Как здесь показано, логика CompareTo () состоит в сравнении входного объекта с текущим экземпляром по конкретному элементу данных. Возвращаемое значение CompareTo () служит для выяснения того, является данный объект меньше, больше или равным объекту, с которым он сравнивается (табл. 9.1). Таблица 9.1. Значения, которые может возвращать CompareTo () Возвращаемое значение Описание Любое число меньше нуля Обозначает, что данный экземпляр находится перед указанным объектом в порядке сортировки Нуль Обозначает, что данный экземпляр равен указанному объекту Любое число больше нуля Обозначает, что данный экземпляр находится после указанного объекта в порядке сортировки Предыдущую реализацию CompareTo () можно упростить, благодаря тому, что в С# тип данных int (который представляет собой сокращенный вариант обозначения типа System. Int32 в CLR) реализует интерфейс IComparable. Реализовать CompareTo () в Саг можно следующим образом: int IComparable.CompareTo(object ob]) { Car temp = obj as Car; if (temp != null) return this.CarlD.CompareTo(temp.CarlD); else throw new ArgumentException ("Parameter is not a Car!11); // Параметр не является объектом типа Car1 } Далее в обоих случаях, чтобы тип Саг понимал, каким образом сравнивать себя с подобными объектами, можно написать следующий код: // Использование интерфейса IComparable. static void Main(string[] args) { // Создание массива объектов Car.
Глава 9. Работа с интерфейсами 353 // Отображение текущего массива. Console.WriteLine ("Here is the unordered set of cars:"); foreach(Car с in myAutos) Console.WriteLine("{0} {1}", c.CarlD, c.PetName); // Сортировка массива с применением интерфейса ХСохорагаЫе. Array.Sort(myAutos); // Отображение отсортированного массива. Console.WriteLine("Here is the ordered set of cars:"); foreach (Car с in myAutos) Console.WriteLine("{0} {1}", c.CarlD, c.PetName); Console.ReadLine (); } Ниже показан вывод после выполнения приведенного выше метода Main (): ***** Fun with Object Sorting ***** Here is the unordered set of cars: 1 Rusty 234 Mary 34 Viper 4 Mel 5 Списку Here is the ordered set of cars: 1 Rusty 4 Mel 5 Списку 34 Viper 234 Mary Указание множества критериев для сортировки (IComparer) В предыдущей версии класса Саг в качестве основы для порядка сортировки использовался идентификатор автомобиля (carlD). В другой версии для этого могло бы применяться дружественное название автомобиля (для перечисления автомобилей в алфавитном порядке). А что если возникнет желание создать класс Саг, способный производить сортировку и по идентификатору, и по дружественному названию? Для этого должен использоваться другой стандартный интерфейс по имени I Comparer, который поставляется в пространстве имен System. Collections и определение которого выглядит следующим образом: // Общий способ для сравнения двух объектов. interface IComparer { int Compare(object ol, object o2); } На заметку! В обобщенной версии этого интерфейса (IComparere<T>) предлагается более безопасный в отношении типов способ для обработки сравнений между объектами. Обобщения более подробно рассматриваются в главе 10. В отличие от I Comparable, интерфейс I Compare r обычно реализуется не в самом подлежащем сортировке типе (Саг в рассматриваемом случае), а в наборе соответствующих вспомогательных классов, по одному для каждого порядка сортировки (по дружественному названию, идентификатору и т.д.). В настоящее время типу Саг (автомобиль) уже "известно", как ему следует сравнивать себя с другими автомобилями по
354 Часть III. Дополнительные конструкции программирования на С# внутреннему идентификатору. Следовательно, чтобы позволить пользователю объекта производить сортировку массива объектов Саг еще и по значению petName, понадобится создать дополнительный вспомогательный класс, реализующий IComparer. Ниже показан весь необходимый для этого код (перед его использованием важно не забыть импортировать в файл кода пространство имен System.Collections). // Этот вспомогательный класс предназначен для // обеспечения возможности сортировки массива // объектов Саг по дружественному названию. public class PetNameComparer : IComparer I // Проверка дружественного названия каждого объекта. int IComparer.Compare(object ol, object o2) { Car tl = ol as Car; Car t2 = o2 as Car; if (tl != null && t2 != null) return String.Compare(tl.PetName, t2.PetName); else throw new ArgumentException ("Parameter is not a Car!"); // Параметр не является объектом типа Car1 } } Теперь можно использовать этот вспомогательный класс в коде. В System.Array имеется набор перегруженных версий метода Sort (), в одной из которых принимается в качестве параметра объект, реализующий интерфейс IComparer. static void Main(string [ ] args) { // Теперь выполнение сортировки по дружественному названию. Array.Sort(myAutos, new PetNameComparer()); // Вывод отсортированного массива в окне консоли. Console. WriteLine ("Ordering by pet name:"); foreach(Car с in myAutos) Console.WriteLine ("{0} {1}", CarlD, c.PetName); } Использование специальных свойств и специальных типов для сортировки Следует отметить, что можно также использовать специальное статическое свойство для оказания пользователю объекта помощи с сортировкой типов Саг по какому-то конкретному элементу данных. Для примера добавим в класс Саг статическое доступное только для чтения свойство по имени SortByPetName, которое возвращает экземпляр объекта, реализующего интерфейс IComparer (в этом случае PetNameComparer): // Обеспечение поддержки для специального свойства, // способного возвращать правильный интерфейс IComparer. public class Car : IComparable { // Свойство, воэвращажщее компаратор SortByPetName. public static IComparer SortByPetName { get { return (IComparer)new PetNameComparer() ; } } }
Глава 9. Работа с интерфейсами 355 Теперь в коде пользователя объекта сортировка по дружественному названию может выполняться с помощью строго ассоциированного свойства, а не автономного класса PetNameComparer: // Сортировка по дружественному названию теперь немного проще. Array.Sort(myAutos, Car.SortByPetName); Исходный код. Проект ComparableCar доступен в подкаталоге Chapter 9. К этому моменту должны быть понятны не только способы определения и реализации собственных интерфейсов, но и то, какую пользу они могут приносить. Следует отметить, что интерфейсы встречаются в каждом крупном пространстве имен .NET, и в остальной части книги придется неоднократно иметь дело с различными стандартными интерфейсами. Резюме Интерфейс может быть определен как именованная коллекция абстрактных членов. Из-за того, что никаких касающихся реализации деталей в интерфейсе не предоставляется, интерфейс часто рассматривается как поведение, которое может поддерживаться тем или иным типом. В случае, когда один и тот же интерфейс реализуют два или более класса, каждый из типов может обрабатываться одинаково (что называется обеспечением полиморфизма на основе интерфейса), даже если эти типы расположены в разных иерархиях классов. Для определения новых интерфейсов в С# предусмотрено ключевое слово interface. Как было показано в этой главе, в любом типе может обеспечиваться поддержка для любого количества интерфейсов за счет предоставления соответствующего разделенного запятыми списка их имен. Более того, также допускается создавать интерфейсы, унаследованные от нескольких базовых интерфейсов. Помимо возможности создания специальных интерфейсов, в библиотеках .NET предлагается набор стандартных (поставляемых вместе с платформой) интерфейсов. Как было показано в этой главе, можно создавать специальные типы, реализующие предопределенные интерфейсы, и получать доступ к желаемым возможностям, таким как клонирование, сортировка и перечисление.
ГЛАВА 10 Обобщения Самым элементарным контейнером на платформе .NET является тип System. Array. Как было показано в главе 4, массивы С# позволяют определять наборы типизированных элементов (включая массив объектов типа System.Object, по сути представляющий собой массив любых типов) с фиксированным верхним пределом. Хотя базовые массивы могут быть удобны для управления небольшими объемами известных данных, бывает также немало случаев, когда требуются более гибкие структуры данных, такие как динамически растущие и сокращающиеся контейнеры или контейнеры, которые хранят только элементы, отвечающие определенному критерию (например, элементы, унаследованные от заданного базового класса, реализующие определенный интерфейс или что-то подобное). После появления первого выпуска платформы .NET программисты часто использовали пространство имен System.Collections для получения более гибкого способа управления данными в приложениях. Однако, начиная с версии .NET 2.0, язык программирования С# был расширен поддержкой средства, которое называется обобщением (generic). Вместе с ним библиотеки базовых классов пополнились совершенно новым пространством имен, связанным с коллекциями — System. Col lections. Generic. Как будет показано в настоящей главе, обобщенные контейнеры во многих отношениях превосходят свои необобщенные аналоги, обеспечивая высочайшую безопасность типов и выигрыш в производительности. После общего знакомства с обобщениями будут описаны часто используемые классы и интерфейсы из пространства имен System. Collections.Generic. В оставшейся части этой главы будет показано, как строить собственные обобщенные типы. Вы также узнаете о роли ограничений (constraint), которые позволяют строить контейнеры, исключительно безопасные в отношении типов. На заметку! Можно также создавать обобщенные типы делегатов; об этом пойдет речь в следующей главе. Проблемы, связанные с необобщенными коллекциями С момента появления платформы .NET программисты часто использовали пространство имен System.Collecitons из сборки mscorlib.dll. Здесь разработчики платформы предоставили набор классов, позволявших управлять и организовывать большие объемы данных. В табл. 10.1 документированы некоторые наиболее часто используемые классы коллекций, а также основные интерфейсы, которые они реализуют.
Глава 10. Обобщения 357 Таблица 10.1. Часто используемые классы из System.Collections Класс Назначение Основные реализуемые интерфейсы ArrayList Представляет коллекцию динамически изменяемого размера, содержащую объекты в определенном порядке Hashtable Представляет коллекцию пар "ключ/значение", организованных на основе хеш-кода ключа Queue Представляет стандартную очередь, работающую по алгоритму FIFO ("первый вошел — первый вышел") SortedList Представляет коллекцию пар "ключ/значение", отсортированных по ключу и доступных по ключу и по индексу Stack Представляет стек LIFO ("последний вошел — первый вышел"), поддерживающий функциональность заталкивания (push) и выталкивания (pop), а также считывания (peek) IList, ICollection, IEnumerable и ICloneable IDictionary, ICollection, IEnumerable и ICloneable ICollection, IEnumerable иICloneable IDictionary, Icollection, IEnumerable и ICloneable ICollection, IEnumerable иICloneable Интерфейсы, реализованные этими базовыми классами коллекций, представляют огромное "окно" в их общую функциональность. В табл. 10.2 представлено описание общей природы этих основных интерфейсов, часть из которых поверхностно рассматривалась в главе 9. Таблица 10.2. Основные интерфейсы, поддерживаемые классами System.Collections Интерфейс Назначение ICollection Определяет общие характеристики (т.е. размер, перечисление и безопасность к потокам) всех необобщенных типов коллекций ICloneable Позволяет реализующему объекту возвращать копию самого себя вызывающему коду IDictionary Позволяет объекту необобщенной коллекции представлять свое содержимое в виде пар "имя/значение" IEnumerable Возвращает объект, реализующий интерфейс IEnumerator (см. следующую строку в таблице) IEnumerator Позволяет итерацию в стиле f oreach по элементам коллекции IList Обеспечивает поведение добавления, удаления и индексирования элементов в списке объектов В дополнение к этим базовым классам (и интерфейсам) добавляются несколько специализированных типов коллекций, таких как BitVector32, ListDictionary, StringDictionary и StringCollection, определенных в пространстве имен System. Collections.Specialized из сборки System.dll. Это пространство имен также содержит множество дополнительных интерфейсов и абстрактных классов, которые можно использовать в качестве отправной точки при создании специальных классов коллекций.
358 Часть III. Дополнительные конструкции программирования на С# Хотя за последние годы с применением этих "классических" классов коллекций (и интерфейсов) было построено немало успешных приложений .NET, опыт показал, что применение этих типов может быть сопряжено с множеством проблем. Первая проблема состоит в том, что использование классов коллекций System. Collections и System.Collections.Specialized приводит к созданию низкопроизводительного кода, особенно если осуществляются манипуляции со структурами данных (т.е. типами значения). Как вскоре будет показано, при сохранении структур в классических классах коллекций среде CLR приходится выполнять массу операций перемещения данных в памяти, что может значительно снизить скорость выполнения. Вторая проблема связана с тем, что эти классические классы коллекций не являются безопасными к типам, так как они (по большей части) были созданы для работы с System.Object и потому могут содержать в себе все что угодно. Если разработчику .NET требовалось создать безопасную в отношении типов коллекцию (т.е. контейнер, который может содержать только объекты, реализующие определенный интерфейс), то единственным реальным вариантом было создание совершенно нового класса коллекции собственноручно. Это не слишком трудоемкая задача, но довольно утомительная. Учитывая эти (и другие) проблемы, разработчики .NET 2.0 добавили новый набор классов коллекций, собранных в пространстве имен System. Col lections. Generic. В любом новом проекте, который создается с помощью платформы .NET 2.0 и последующих версий, предпочтение должно отдаваться соответствующим обобщенным классам перед унаследованными необобщенными. На заметку! Следует повториться: любое приложение, которое строится на платформе версии .NET 2.0 и выше, должно игнорировать классы из пространства имен System.Collect ions, а использовать вместо них классы из пространства имен System.Collections.Generic. Прежде чем будет показано, как использовать обобщения в своих программах, стоит глубже рассмотреть недостатки необобщенных коллекций; это поможет лучше понять проблемы, которые был призван решить механизм обобщений. Давайте создадим новое консольное приложение по имени IssuesWithNongenericCollections и затем импортируем пространство имен System.Collections в начале кода С#: using System.Collections; Проблема производительности Как уже должно быть известно из главы 4, платформа .NET поддерживает две обширные категории данных: типы значения и ссылочные типы. Поскольку в .NET определены две основных категории типов, однажды может возникнуть необходимость представить переменную одной категории в виде переменной другой категории. Для этого в С# предлагается простой механизм, называемый упаковкой (boxing), который служит для сохранения данных типа значения в ссылочной переменной. Предположим, что в методе по имени SimpleBoxUnboxOperation() создана локальная переменная типа int: static void SimpleBoxUnboxOperation () { // Создать переменную ValueType (int). int mylnt = 25; } Если далее в приложении понадобится представить этот тип значения в виде ссылочного типа, значение следует упаковать, как показано ниже: private static void SimpleBoxUnboxOperation () {
Глава 10. Обобщения 359 // Создать переменную ValueType (int). int mylnt = 25; // Упаковать int в ссылку на object, object boxedlnt = mylnt; } Упаковку можно определить формально как процесс явного присваивания типа значения переменной System.Object. При упаковке значения CLR-среда размещает в куче новый объект и копирует значение типа значения (в данном случае — 25) в этот экземпляр. В качестве результата возвращается ссылка на вновь размещенный в куче объект. При таком подходе не нужно использовать набор классов-оболочек для временной трактовки данных стека как объектов, размещенных в куче. Противоположная операция также возможна, и она называется распаковкой (unboxing). Распаковка — это процесс преобразования значения, хранящегося в объектной ссылке, обратно в соответствующий тип значения в стеке. Синтаксически операция распаковки выглядит как нормальная операция приведения, однако ее семантика несколько отличается. Среда CLR начинает с проверки того, что полученный тип данных эквивалентен упакованному типу; и если это так, копирует значение обратно в находящуюся в стеке переменную. Например, следующие операции распаковки работают успешно при условии, что типом boxedlnt в действительности является int: private static void SimpleBoxUnboxOperation() { // Создать переменную ValueType (int). int mylnt = 25; // Упаковать int в ссылку на object, object boxedlnt = mylnt; // Распаковать ссылку обратно в int. int unboxedlnt = (int)boxedlnt; } Когда компилятор С# встречает синтаксис упаковки/распаковки, он генерирует CIL- код, содержащий коды операций box/unbox. Заглянув в сборку с помощью утилиты ildasm.exe, можно найти там следующий CIL-код: .method private hidebysig static void SimpleBoxUnboxOperation () cil managed { // Code size 19 @x13) .maxstack 1 .locals init ([0] int32 mylnt, [1] object boxedlnt, [2] int32 unboxedlnt) IL_0000: nop IL_0001: ldc.i4.s 25 IL_0003: stloc.O IL_0004: ldloc.O IL_0005: box [mscorlib] System. Int32 IL_000a: stloc.l IL_000b: ldloc.l IL_000c: unbox.any [mscorlib]System.Int32 IL_0011: stloc.2 IL_0012: ret } // end of method Program::SimpleBoxUnboxOperation Помните, что в отличие от обычного приведения распаковка должна производиться только в соответствующий тип данных. Попытка распаковать порцию данных в некорректную переменную приводит к генерации исключения InvalidCastException. Для полной безопасности следовало бы поместить каждую операцию распаковки в конструкцию try/catch, однако делать это для абсолютно каждой операции распаковки в
360 Часть III. Дополнительные конструкции программирования на С# приложении может оказаться довольно трудоемкой задачей. Взгляните на следующий измененный код, который выдаст ошибку, поскольку предпринята попытка распаковать упакованный int в long: private static void SimpleBoxUnboxOperation () { // Создать переменную ValueType (int). int mylnt = 25; // Упаковать int в ссылку на object, object boxedlnt = mylnt; // Распаковать в неверный тип данных, чтобы инициировать // исключение времени выполнения. try { long unboxedlnt = (long)boxedlnt; } catch (InvalidCastException ex) { Console.WriteLine(ex.Message); } } На первый взгляд упаковка/распаковка может показаться довольно несущественным средством языка, представляющим скорее академический интерес, чем практическую ценность. На самом деле процесс упаковки /распаковки очень полезен, поскольку позволяет предположить, что все можно трактовать как System.Object, причем CLR берет на себя все заботы о деталях, связанных с организацией памяти. Давайте посмотрим на практическое применение этих приемов. Предположим, что создан необобщенный класс System. Col lections. Array List для хранения множества числовых (расположенных в стеке) данных. Члены ArrayList прототипированы для работы с данными System.Object. Теперь рассмотрим методы Add(), Insert (), Remove(), а также индексатор класса: public class ArrayList : object, IList, ICollection, IEnumerable, ICloneable { public virtual int Add(object value); public virtual void Insert(int index, object value); public virtual void Remove(object obj); public virtual object this[int index] {get; set; } } Класс ArrayList ориентирован на работу с экземплярами object, которые представляют данные, расположенные в куче, поэтому может показаться странным, что следующий код компилируется и выполняется без ошибок: static void WorkWithArrayList () { // Типы значений упаковываются автоматически // при передаче методу, запросившему объект. ArrayList mylnts = new ArrayList (); mylnts.Add(lO), mylnts.AddB0), mylnts.AddC5); Console.ReadLine();
Глава 10. Обобщения 361 Несмотря на непосредственную передачу числовых данных в методы, которые принимают тип object, исполняющая среда автоматически упаковывает их в данные, расположенные в стеке. При последующем извлечении элемента из ArrayList с использованием индексатора типа потребуется распаковать операцией приведения объект, находящийся в куче, в целочисленное значение, расположенное в стеке. Помните, что индексатор ArrayList возвращает System.Object, а не System.Int32: static void WorkWithArrayList() { // Типы значений автоматически упаковываются, когда // передаются члену, принимающему объект. ArrayList mylnts = new ArrayList(); mylnts.Add(lO); mylnts.AddB0); mylnts.AddC5); // Распаковка происходит, когда объект преобразуется // обратно в расположенные в стеке данные. int i = (int)mylnts[0]; // Теперь значение вновь упаковывается, // так как WriteLine() требует объектных типов' Console.WriteLine("Value of your int: {0}", 1); Console.ReadLine(); } Обратите внимание, что расположенные в стеке значения System.Int32 упаковываются перед вызовом ArrayList.AddO, чтобы их можно было передать в требуемом виде System.Object. Кроме того, объекты System.Object распаковываются обратно в System.Int32 после их извлечения из ArrayList с использованием индексатора типа только для того, чтобы вновь быть упакованными для передачи в метод Console. WriteLine(), поскольку этот метод оперирует переменными System.Object. Хотя упаковка и распаковка очень удобна с точки зрения программиста, этот упрощенный подход к передаче данных между стеком и кучей влечет за собой проблемы производительности (это касается как скорости выполнения, так и размера кода), а также недостаток безопасности в отношении типов. Чтобы понять, в чем состоят проблемы с производительностью, взгляните на перечень действий, которые должны быть выполнены при упаковке и распаковке простого целого числа. 1. Новый объект должен быть размещен в управляемой куче. 2. Значение данных, находящихся в стеке, должно быть передано в выделенное место в памяти. 3. При распаковке значение, которое хранится в объекте, находящемся в куче, должно быть передано обратно в стек. 4. Неиспользуемый больше объект в куче будет (в конечном) итоге удален сборщиком мусора. Хотя существующий метод Main() не является основным узким местом в смысле производительности, вы определенно это почувствуете, если ArrayList будет содержать тысячи целочисленных значений, к которым программа обращается на регулярной основе. В идеальном случае хотелось бы манипулировать расположенными в стеке данными внутри контейнера, не имея проблем с производительностью. Было бы хорошо иметь возможность извлекать данные из контейнера, обходясь без конструкций try/catch (именно это обеспечивают обобщения).
362 Часть III. Дополнительные конструкции программирования на С# Проблемы с безопасностью типов Проблема безопасности типов уже затрагивалась, когда речь шла об операции распаковки. Вспомните, что данные должны быть распакованы в тот же тип, который был для них объявлен перед упаковкой. Однако есть и другой аспект безопасности типов, который следует иметь в виду в мире без обобщений: тот факт, что классы из System.Collections могут хранить все что угодно, поскольку их члены прототипирова- ны для работы с System.Object. Например, в следующем методе контейнер ArrayList хранит произвольные фрагменты несвязанных данных: static void ArrayListOfRandomObjects() { // ArrayList может хранить все что угодно. ArrayList allMyObject = new ArrayList(); allMyObjects.Add(true); allMyObjects.Add(new OperatingSystem(PlatformlD.MacOSX, new VersionA0, 0) ) ) ; allMyObjects.AddF6); allMyObjects.AddC.14) ; } В некоторых случаях действительно необходим исключительно гибкий контейнер, который может хранить буквально все. Однако в большинстве ситуаций понадобится безопасный в отношении типов контейнер, который может оперировать только определенным типом данных, например, контейнер, который хранит только подключения к базе данных, битовые образы или объекты, совместимые с I Pointy. До появления обобщений единственным способом решения этой проблемы было создание вручную строго типизированных коллекций. Предположим, что создана специальная коллекция, которая может содержать только объекты типа Person: public class Person { public int Age {get; set;} public string FirstName {get; set;} public string LastName {get; set;} public Person () { } public Person(string firstName, string lastName, int age) { Age = age; FirstName = firstName; LastName = lastName; } public override string ToStringO { return string.Format("Name: {0} {1}, Age: {2}", FirstName, LastName, Age); } } Чтобы построить коллекцию только элементов Person, можно определить переменную-член System.Collection.ArrayList внутри класса, именуемого PeopleCollection, и сконфигурировать все члены для работы со строго типизированными объектами Person вместо объектов типа System.Object. Ниже приведен простой пример (реальная коллекция производственного уровня должна включать множество дополнительных членов и расширять абстрактный базовый класс из пространства имен System. Col lections):
Глава 10. Обобщения 363 public class PeopleCollection : IEnumerable { private ArrayList arPeople = new ArrayList() ; // Приведение для вызывающего кода. public Person GetPerson(int pos) { return (Person)arPeople[pos]; } // Вставка только объектов Person. public void AddPerson (Person p) { arPeople.Add(p); } public void ClearPeople () { arPeople.Clear(); } public int Count { get { return arPeople.Count; } } // Поддержка перечисления с помощью foreach. IEnumerator IEnumerable.GetEnumerator () { return arPeople.GetEnumerator(); } } Обратите внимание, что класс PeopleCollection реализует интерфейс IEnumerable, который делает возможной итерацию в стиле foreach по всем содержащимся в коллекции элементам. Кроме того, методы GetPersonO HAddPerson() прототипированы на работу только с объектами Person, а не битовыми образами, строками, подключениями к базе данных или другими элементами. За счет создания таких классов обеспечивается безопасность типов; при этом компилятору С# позволяется определять любую попытку вставки элемента неподходящего типа: static void UsePers-onCollection () { Console.WriteLine("***** Custom Person Collection *****\n"); PersonCollection myPeople = new PersonCollection (); myPeople.AddPerson(new Person("Homer", "Simpson", 40)); myPeople.AddPerson(new Person("Marge", "Simpson", 38)); myPeople.AddPerson(new Person("Lisa", "Simpson", 9)); myPeople.AddPerson(new Person("Bart", "Simpson", 7)); myPeople.AddPerson(new Person("Maggie", "Simpson", 2)); // Это вызовет ошибку при компиляции! // myPeople.AddPerson(new Car()); foreach (Person p in myPeople) Console.WriteLine(p); } Хотя подобные специальные коллекции гарантируют безопасность типов, такой подход все же обязывает создавать (почти идентичные) специальные коллекции для каждого уникального типа данных, который планируется хранить. Таким образом, если нужна специальная коллекция, которая будет способна оперировать только классами, унаследованными от базового класса Саг, понадобится построить очень похожий класс коллекции: public class CarCollection : IEnumerable { private ArrayList arCars = new ArrayList (); // Приведение для вызывающего кода. public Car GetCar(int pos) { return (Car) arCars[pos]; } // Вставка только объектов Саг. public void AddCar(Car с) { arCars.Add(с); }
364 Часть III. Дополнительные конструкции программирования на С# public void ClearCarsO { arCars.Clear(); } public int Count { get { return arCars.Count; } } // Поддержка перечисления с помощью foreach. IEnumerator IEnumerable.GetEnumerator() { return arCars.GetEnumerator(); } } Однако эти специальные контейнеры мало помогают в решении проблем упаковки/ распаковки. Даже если создать специальную коллекцию по имени IntCollection, предназначенную для работы только с элементами System.Int32, все равно придется выделить некоторый тип объекта для хранения данных (т.е. System.Array и ArrayList): public class IntCollection : IEnumerable { private ArrayList arlnts = new ArrayList () ; // Распаковать для вызывающего кода. public int Getlnt(int pos) { return (int)arlnts[pos]; } // Операция упаковки! public void Addlnt(int i) { arlnts.Add(i); } public void ClearlntsO { arlnts.Clear(); } public int Count { get { return arlnts.Count; } } IEnumerator IEnumerable.GetEnumerator () { return arlnts.GetEnumerator(); } } Независимо от того, какой тип выбран для хранения целых чисел, дилеммы упаковки нельзя избежать, применяя необобщенные контейнеры. В случае использования классов обобщенных коллекций исчезают все описанные выше проблемы, включая затраты на упаковку/распаковку и недостаток безопасности типов. Кроме того, необходимость в построении собственного специального класса обобщенной коллекции возникает редко. Вместо построения специальных коллекций, которые могут хранить людей, автомобили и целые числа, можно обратиться к обобщенному классу коллекции и указать тип хранимых элементов. В показанном ниже методе класс Lis to (из пространства имен System. Col lection. Generic) используется для хранения различных типов данных в строго типизированной манере (пока не обращайте внимания на детали синтаксиса обобщений): static void UseGenericList () { Console.WriteLine ("***** Fun with Generics *****\n"); // Этот List<> может хранить только объекты Person. List<Person> morePeople = new List<Person> (); morePeople.Add (new Person ("Frank", "Black", 50)); Console.WriteLine(morePeople[0]); // Этот List<> может хранить только целые числа. List<int> morelnts = new List<int>(); morelnts.AddA0); morelnts.AddB); int sum = morelnts [0] + morelnts [1] ; // Ошибка компиляции! Объект Person не может быть добавлен к списку целых! // morelnts.Add(new Person ());
Глава 10. Обобщения 365 Первый объект Listo может хранить только объекты Person. Поэтому выполнять приведение при извлечении элементов из контейнера не требуется, что делает этот подход более безопасным в отношении типов. Второй Listo может хранить только целые, и все они размещены в стеке; другими словами, здесь не происходит никакой скрытой упаковки/распаковки, как это имеет место в необобщенном ArrayList. Ниже приведен краткий список преимуществ обобщенных контейнеров перед их необобщенными аналогами. • Обобщения обеспечивают более высокую производительность, поскольку не страдают от проблем упаковки /распаковки. • Обобщения более безопасны в отношении типов, так как могут содержать только объекты указанного типа. • Обобщения значительно сокращают потребность в специальных типах коллекций, потому что библиотека базовых классов предлагает несколько готовых контейнеров. Исходный код. Проект IssuesWithNonGenericCollections доступен в подкаталоге Chapter 10. Роль параметров обобщенных типов Обобщенные классы, интерфейсы, структуры и делегаты буквально разбросаны по всей базовой библиотеке классов .NET, и они могут быть частью любого пространства имен .NET. На заметку! Обобщенными могут быть только классы, структуры, интерфейсы и делегаты, но не перечисления. Отличить обобщенный элемент в документации .NET Framework 4.0 SDK или браузере объектов Visual Studio 2010 от других элементов очень легко по наличию пары угловых скобок с буквой или другой лексемой. На рис. 10.1 показано множество обобщенных элементов в пространстве имен System.Collections.Generic, включая выделенный класс List<T>. Щ Object Browser X Ц I Вляме .NET Framework 4 ■ Slch/^ л (} System.Collections.Generic > ^ Comparer<T> > ^ Dictionary<TKey. TVeluo !> "j)t Dictionery<TKey, TValue> > *$ Dictionary<TKey, TValuo t> "!► Dictionary<TKey, TValuo S> ^$ Dictionary*TKey, TValue> - „. | 4" нкнммр Enumerator KeyCollection KeyCollection.Enumerator ValueCollection > -ф> Dictionary<TKey,TValue>.ValueCollection.Enumerator > fy EqualttyComparer<T> > -° ICollection<T> t> *"° lComparer<T> > "° roictionary<TKey, TValue> ^ 'M° lEnumerable<T> > *"° IEnumerator<7> r> *^> lEqualityComparer<T> » —° IList<T> !> ^X KeyNotFoundException b p KeyValuePafr<TKey,TVatue> tIESffl > -$> List<T>.Enumerator ► Add(T) 4> AddRangeCSystem.Collectioni.Generic.lEnumerabl ♦ AsReadOntyQ ♦ BinarySearch(int, tnt, T, System.Collections.Generic ■'♦ BinarySearchfT) ♦ BinarySearch(T, System.Collections.GenericIComp ♦ Clean! '• Contair»(T) ♦ ConvertAII<T0utput>(Sy5tem.Converter<T,T0utp ♦ CopyTo(int, T[ 1 int, int) ♦ CopyTo(T[ D JLfJ^IoflLLinfl . public class Lirt<T> Member of Syttcm.Co Summary: Represents a strongly typed list of objects that can be accessed by index. Provides methods to search sort, and manipulate lists. ■ Рис. 10.1. Обобщенные элементы, поддерживающие параметры типа
366 Часть III. Дополнительные конструкции программирования на С# Формально эти лексемы можно называть параметрами типа, однако в более дружественных к пользователю терминах их можно считать просто местами подстановки (placeholder). Конструкцию <Т> можно воспринимать как типа Т. Таким образом, IEnumerable<T> можно читать как IEnumerable типа Т, или, говоря иначе, перечисление типа Т. На заметку! Имя параметра типа (места подстановки) не важно, и это — дело вкуса того, кто создает обобщенный элемент. Тем не менее, обычно для представления типов используется т, ТКеу или К — для представления ключей, а также TValue или V — для представления значений. При создании обобщенного объекта, реализации обобщенного интерфейса или вызове обобщенного члена должно быть указано значение для параметра типа. Как в этой главе, так и в остальной части книги будет показано немало примеров. Однако для начала следует ознакомиться с основами взаимодействия с обобщенными типами и членами. Указание параметров типа для обобщенных классов и структур При создании экземпляра обобщенного класса или структуры параметр типа указывается, когда объявляется переменная и когда вызывается конструктор. В предыдущем фрагменте кода было показано, что UseGenericListO определяет два объекта Listo: // Этот Listo может хранить только объекты Person. List<Person> morePeople = new List<Person> () ; Этот фрагмент можно трактовать как Listo объектов Т, где Т — тип Person, или более просто — список объектов персон. Однажды указав параметр типа обобщенного элемента, его нельзя изменить (помните: обобщения предназначены для поддержки безопасности типов). После указания параметра типа для обобщенного класса или структуры все вхождения заполнителей заменяются указанным значением. Просмотрев полное объявление обобщенного класса List<T> в браузере объектов Visual Studio 2010, можно заметить, что заполнитель Т используется в определении повсеместно. Ниже приведен частичный листинг (обратите внимание на выделенные элементы): // Частичный листинг класса List<T>. namespace System.Collections.Generic { public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable { public void Add(T item); public ReadOnlyCollection<T> AsReadOnly(); public int BinarySearch (T item); public bool Contains (T item); public void CopyTo(T[] array); public int Findlndex(System.Predicate<T> match); public T FindLast(System.Predicate<T> match); public bool Remove(T item); public int RemoveAll(System.Predicate<T> match); public T[] ToArrayO; public bool TrueForAll(System.Predicate<T> match); public T this [int index] { get; set; } } }
Глава 10. Обобщения 367 Когда создается List<T> с указанием объектов Person, это все равно, как если бы тип List<T> был определен следующим образом: namespace System.Collections.Generic { public class List<Person> : IList<Person>, ICollection<Person>, IEnumerable<Person>, IList, ICollection, IEnumerable { public void Add(Person item); public ReadOnlyCollection<Person> AsReadOnly(); public int BinarySearch(Person item); public bool Contains(Person item); public void CopyTo(Person[] array); public int Findlndex(System.Predicate<Person> match); public Person FindLast(System.Predicate<Person> match); public bool Remove(Person item); public int RemoveAll(System.Predicata<Person> match); public Person [] ToArrayO; public bool TrueForAll(System.Predicate<Person> match); public Person this[int index] { get; set; } } } Разумеется, при создании программистом обобщенной переменной List<T> компилятор на самом деле не создает буквально совершенно новую реализацию класса List<T>. Вместо этого он обрабатывает только члены обобщенного типа, к которым действительно производится обращение. На заметку! В следующей главе рассматриваются обобщенные делегаты, которые также при создании требуют указания параметра типа. Указание параметров типа для обобщенных членов Для необобщенного класса или структуры вполне допустимо поддерживать несколько обобщенных членов (т.е. методов и свойств). В таких случаях указывать значение заполнителя нужно также и во время вызова метода. Например, System.Array поддерживает несколько обобщенных методов (которые появились в .NET 2.0). В частности, статический метод Sort() теперь имеет обобщенный конструктор по имени Sort<T>(). Рассмотрим следующий фрагмент кода, в котором Т — это тип int: int[] mylnts = { 10, 4, 2, 33, 93 }; // Указание заполнителя для обобщенного метода Sort<>(). Array.Sort<int>(mylnts); foreach (int i in mylnts) { Console.WriteLine(i); } Указание параметров типов для обобщенных интерфейсов Обобщенные интерфейсы обычно реализуются при построении классов или структур, которые должны поддерживать различные поведения платформы (т.е. клонирование, сортировку и перечисление). В главе 9 рассматривалось множество необобщенных интерфейсов, таких как IComparable, IEnumerable, IEnumerator и IComparer. Вспомните, как определен необобщенный интерфейс IComparable:
368 Часть III. Дополнительные конструкции программирования на С# public interface IComparable { int CompareTo(object obj); } В той же главе 9 этот интерфейс был реализован в классе Саг для обеспечения сортировки в стандартном массиве. Однако код требовал нескольких проверок времени выполнения и операций приведения, потому что параметром был общий тип System.Object: public class Car : IComparable { // Реализация IComparable. int IComparable.CompareTo (object obj ) { Car temp = obj as Car; if (temp ' = null) { if (this.CarlD > temp.CarlD) return 1; if (this.CarlD < temp.CarlD) return -1; else return 0; } else throw new ArgumentException("Parameter is not a Car!"); } } Теперь воспользуемся обобщенным аналогом этого интерфейса: public interface IComparable<T> -{ int CompareTo(T obj); } В этом случае код реализации будет значительно яснее: public class Car : IComparable<Car> { // Реализация IComparable<T>. int IComparable<Car>.CompareTo (Car obj) { if (this.CarlD > ob].CarlD) return 1; if (this.CarlD < obj.CarlD) return -1; else return 0; } } Здесь уже не нужно проверять, относится ли входной параметр к типу Саг, потому что он может быть только Саг! В случае передачи несовместимого типа данных возникает ошибка времени компиляции. Получив начальные сведения о том, как взаимодействовать с обобщенными элементами, а также ознакомившись с ролью параметров типа (т.е. заполнителей), можно приступать к изучению классов и интерфейсов из пространства имен System.Collect ions.Generic.
Глава 10. Обобщения 369 Пространство имен System.Collections .Generic Основная часть пространства имен System.Collections.Generic располагается в сборках mscorlib.dll и system.dll. В начале этой главы кратко упоминались некоторые из необобщенных интерфейсов, реализованных необобщенными классами коллекций. Не должно вызывать удивление, что в пространстве имен System.Collections. Generic определены обобщенные замены для многих из них. В действительность есть много обобщенных интерфейсов, которые расширяют свои необобщенные аналоги. Это может показаться странным; однако благодаря этому реализации новых классов также поддерживают унаследованную функциональность, имеющуюся у их необобщенных аналогов. Например, IEnumerable<T> расширяет IEnumerable. В табл. 10.3 документированы основные обобщенные интерфейсы, с которыми придется иметь дело при работе с обобщенными классами коллекций. На заметку! Если вы работали с обобщениями до выхода .NET 4.0, то должны быть знакомы с типами lSet<T> и SortedSet<T>, которые более подробно рассматриваются далее в этой главе. Таблица 10.3. Основные интерфейсы, поддерживаемые классами из пространства имен System.Collections.Generic Интерфейс Назначение System.Collections ICollection<T> Определяет общие характеристики (например, размер, перечисление и безопасность к потокам) для всех типов обобщенных коллекций IComparer<T> Определяет способ сравнения объектов IDictionary<TKey, Позволяет объекту обобщенной коллекции представлять свое со- TValue> держимое посредством пар "ключ/значение" IEnumerable<T> Возвращает интерфейс IEnumerator<T> для заданного объекта IEnumerator<T> Позволяет выполнять итерацию в стиле f oreach по элементам коллекции IList<T> Обеспечивает поведение добавления, удаления и индексации элементов в последовательном списке объектов ISet<T> Предоставляет базовый интерфейс для абстракции множеств В пространстве имен System.Collectiobs.Generic также определен набор классов, реализующих многие из этих основных интерфейсов. В табл. 10.4 описаны часто используемые классы из этого пространства имен, реализуемые ими интерфейсы и их базовая функциональность. Пространство имен System.Collections.Generic также определяет ряд вспомогательных классов и структур, работающих в сочетании со специфическим контейнером. Например, тип LinkedListNode<T> представляет узел внутри обобщенного контейнера LinkedList<T>, исключение KeyNotFoundException генерируется при попытке получить элемент из коллекции с указанием несуществующего ключа, и т.д. Следует отметить, что mscorlib.dll и System.dll — не единственные сборки, которые добавляют новые типы в пространство имен System.Collections.Generic. Например, System.Core.dll добавляет класс HashSet<T>. Детальные сведения о пространстве имен System.Collections.Generic доступны в документации .NET Framework 4.0 SDK.
370 Часть III. Дополнительные конструкции программирования на С# Таблица 10.4. Классы из пространства имен System.Collections.Generic Обобщенный класс Поддерживаемые основные интерфейсы Назначение Dictionary<TKey, TValue> List<T> LinkedList<T> Queue<T> SortedDictionary<TKey, TValue> SortedSet<T> Stack<T> ICollection<T>, IDictionary<TKey, TValue>, IEnumerable<T> ICollection<T>, IEnumerable<T>, IList<T> ICollection<T>, IEnumerable<T> ICollection (Это не опечатка! Это интерфейс необобщенной коллекции!), IEnumerable<T> ICollection<T>, IDictionary<TKey, TValue>, IEnumerable<T> ICollection<T>, IEnumerable<T>, ISet<T> ICollection (Это не опечатка! Это интерфейс необобщенной коллекции!), IEnumerable<T> Представляет обобщенную коллекцию ключей и значений Динамически изменяемый последовательный список элементов Представляет двунаправленный список Обобщенная реализация очереди — списка, работающего по алгоритму "первые вошел — первый вышел" (FIFO) Обобщенная реализация сортированного множества пар "ключ/ значение" Представляет коллекцию объектов, поддерживаемых в сортированном порядке без дублирования Обобщенная реализация стека — списка, работающего по алгоритму "последний вошел — первый вышел" (LIFO) В любом случае следующей задачей будет научиться использовать некоторые из этих обобщенных классов коллекций. Но прежде давайте рассмотрим языковые средства С# (впервые появившиеся в .NET 3.5), которые упрощают наполнение данными обобщенных (и необобщенных) коллекций. Синтаксис инициализации коллекций В главе 4 был представлен синтаксис инициализации объектов, который позволяет устанавливать свойства для новой переменной во время ее конструирования. Тесно связан с этим синтаксис инициализации коллекций. Это средство языка С# позволяет наполнять множество контейнеров (таких как ArrayList или List<T>) элементами, используя синтаксис, похожий на тот, что применяется для наполнения базового массива. На заметку! Синтаксис инициализации коллекций может применяться только к классам, которые поддерживают метод Add(), формализованный интерфейсами lCollection<T>/ ICollection.
Глава 10. Обобщения 371 Рассмотрим следующие примеры: // Инициализация стандартного массива. int[] myArrayOflnts = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // Инициализация обобщенного списка Listo элементов int. List<int> myGenericList = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // Инициализация ArrayList числовыми данными. ArrayList myList = new ArrayList { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Если контейнер управляет коллекцией классов или структур, можно смешивать синтаксис инициализации объектов с синтаксисом инициализации коллекций, создавая некоторый функциональный код. Возможно, вы помните класс Point из главы 5, в котором были определены два свойства X и Y. Чтобы построить обобщенный список List<T> объектов Р, можно написать такой код: List<Point> myListOfPoints = new List<Point> { new Point { X = 2, Y = 2 }, new Point { X = 3, Y = 3 }, new Point(PointColor.BloodRed){ X = 4, Y = 4 } }; foreach (var pt in myListOfPoints) { Console.WriteLine (pt); } Преимущество этого синтаксиса в экономии большого объема клавиатурного ввода. Хотя вложенные фигурные скобки затрудняют чтение, если не позаботиться о форматировании, представьте объем кода, который бы потребовался для наполнения следующего списка List<T> объектов Rectangle, если бы не было синтаксиса инициализации коллекций (вспомните, как в главе 4 создавался класс Rectangle, который содержал два свойства, инкапсулирующих объекты Point): List<Rectangle> myListOfRects = new List<Rectangle> { new Rectangle {TopLeft = new Point { X = 10, Y = 10 }, BottomRight = new Point { X = 200, Y = 200}}, new Rectangle {TopLeft = new Point { X = 2, Y = 2 }, BottomRight = new Point { X = 100, Y = 100}}, new Rectangle {TopLeft = new Point { X = 5, Y = 5 }, BottomRight = new Point { X = 90, Y = 75}} }; foreach (var r in myListOfRects) { Console.WriteLine(r); } Работа с классом List<T> Для начала создадим новый проект консольного приложения по имени FunWithGenericCollections. Проекты этого типа автоматически ссылаются на сборки mscorlib.dll и System.dll, что обеспечивает доступ к большинству обобщенных классов коллекций. Обратите внимание, что в первоначальном файле кода С# уже импортируется пространство имен System.Collections.Generic. Первый обобщенный класс, который мы рассмотрим — это List<T>, который уже использовался ранее в этой главе. Из всех классов пространства имен System. Collections.Generic класс List<T> будет применяться наиболее часто, потому что он позволяет динамически изменять размер своего содержимого. Чтобы проиллюстри-
372 Часть III. Дополнительные конструкции программирования на С# ровать основы этого типа, добавьте в класс Program метод UseGenericListO, в котором List<T> используется для манипуляций множеством объектов Person; вы должны помнить, что в классе Person определены три свойства (Age, FirstName и LastName) и специальная реализация метода ToStringO. private static void UseGenericListO { // Создать список объектов Person и заполнить его с помощью // синтаксиса инициализации объектов/коллекций. List<Person> people = new List<Person> () { new Person {FirstName= "Homer", LastName="Simpson", Age=47}, new Person {FirstName= "Marge", LastName="Simpson", Age=45}, new Person {FirstName= "Lisa", LastName="Simpson", Age=9}, new Person {FirstName= "Bart", LastName="Simpson", Age=8} }; // Вывести на консоль количество элементов в списке. Console.WriteLine("Items in list: {0}", people.Count); // Перечислить список, foreach (Person p in people) Console.WriteLine(p); // Вставить новую персону. Console.WriteLine("\n->Inserting new person."); people.Insert B, new Person { FirstName = "Maggie", LastName = "Simpson", Age = 2 }); Console.WriteLine("Items in list: {0}", people.Count); // Скопировать данные в новый массив. Person[] arrayOfPeople = people.ToArray() ; for (int i=0; i < arrayOfPeople.Length; i++) { Console.WriteLine("First Names: {0}", arrayOfPeople[l].FirstName); } } Здесь вы используете синтаксис инициализации для наполнения вашего List<T> объектами, как сокращенную нотацию вызовов Add () п раз. Выведя количество элементов в коллекции (а также пройдясь по каждому элементу), вы вызываете Insert (). Как можно видеть, Insert () позволяет вам вставить новый элемент в List<T> по указанному индексу. И наконец, обратите внимание на вызов метода ToArray(), который возвращает массив объектов Person, основанный на содержимом исходного List<T>. Затем вы выполняете проход по всем элементам этого массива, используя синтаксис индекса массива. Если вы вызовете этот метод из Main(), то получите следующий вывод: ***** Fun with Generic Collections ***** Items in list: 4 Name: Homer Simpson, Age: 47 Name: Marge Simpson, Age: 4 5 Name: Lisa Simpson, Age: 9 Name: Bart Simpson, Age: 8 ->Inserting new person. Items in list: 5 First Names: Homer First Names: Marge First Names: Maggie First Names: Lisa First Names: Bart
Глава 10. Обобщения 373 В классе List<T> определено множество дополнительных членов, представляющих интерес, поэтому за дополнительной информацией обращайтесь в документацию .NET Framework 4.0 SDK. Теперь рассмотрим еще несколько обобщенных коллекций: Stack<T>, Queue<T> и SortedSet<T>. Это даст более полное понимание базовых вариантов хранения данных в приложении. Работа с классом Stack<T> Класс Stack<T> представляет коллекцию элементов, работающую по алгоритму "последний вошел — первый вышел" (LIFO). Как и можно было ожидать, в Stack<T> определены члены Push() иРор(), предназначенные для вставки и удаления элементов в стеке. Приведенный ниже метод создает коллекцию Stack<T> объектов Person: static void UseGenericStack() { Stack<Person> stackOfPeople = new Stack<Person>(); stackOfPeople.Push(new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 }); stackOfPeople.Push(new Person { FirstName = "Marge", LastName = "Simpson", Age =45 }); stackOfPeople.Push(new Person { FirstName = "Lisa", LastName = "Simpson", Age =9 }); // Просмотреть верхний элемент, вытолкнуть его и просмотреть снова. Console.WriteLine("First person is: {0}", stackOfPeople.Peek ()); Console.WriteLine("Popped off {0}", stackOfPeople.Pop ()); Console.WriteLine("\nFirst person is: {0}", stackOfPeople.Peek()); Console.WriteLine("Popped off {0}", stackOfPeople.Pop ()); Console.WriteLine("\nFirst person item is: {0}", stackOfPeople.Peek()); Console.WriteLine("Popped off {0}", stackOfPeople.Pop()); try { Console.WriteLine("\nFirst person is: {0}", stackOfPeople.Peek()); Console.WriteLine("Popped off {0}", stackOfPeople . Pop ()) ; } catch (InvalidOperationException ex) { Console.WriteLine("\nError! {0}", ex.Message); // стек пуст } } В коде строится стек, содержащий информацию о трех людях, добавленных по порядку имен: Homer, Marge и Lisa. Заглядывая (peek) в стек, вы всегда видите объект, находящийся на его вершине; поэтому первый вызов Реек() вернет третий объект Person. После серии вызовов Рор() и Реек() стек, наконец, опустошается, после чего вызовы Peek() и Рор() приводят к генерации системного исключения. Вывод этого примера показан ниже: ***** Fun with Generic Collections ***** First person is: Name: Lisa Simpson, Age: 9 Popped off Name: Lisa Simpson, Age: 9 First person is: Name: Marge Simpson, Age: 45 Popped off Name: Marge Simpson, Age: 45 First person item is: Name: Homer Simpson, Age: 47 Popped off Name: Homer Simpson, Age: 47 Error1 Stack empty.
374 Часть III. Дополнительные конструкции программирования на С# Работа с классом Queue<T> Очереди — это контейнеры, гарантирующие доступ к элементам в стиле "первый вошел — первый вышел" (FIFO). К сожалению, людям приходится сталкиваться с очередями каждый день: очереди в банк, очереди в кинотеатр, очереди в кафе. Когда нужно смоделировать сценарий, в котором элементы обрабатываются в режиме FIFO, класс Queue<T> подходит наилучшим образом. В дополнение к функциональности, предоставляемой поддерживаемыми интерфейсами, Queue определяет основные члены, которые перечислены в табл. 10.5. Таблица 10.5. Члены типа Queue<T> Член Назначение Dequeue () Удаляет и возвращает объект из начала Queue<T> Enqueue () Добавляет объект в конец Queue<T> Peek () Возвращает объект из начала Queue<T>, не удаляя его Теперь давайте посмотрим на эти методы в работе. Можно снова вернуться к классу Person и построить объект Queue<T>, эмулирующий очередь людей, которые ожидают заказа кофе. Для начала представим, что имеется следующий статический метод: static void GetCoffee(Person p) { Console.WriteLine ( "{0} got coffee!", p.FirstName); } Кроме того, есть также дополнительный вспомогательный метод, который вызывает GetCof fee () внутренне: static void UseGenericQueue () { // Создать очередь из трех человек. Queue<Person> peopleQ = new Queue<Person> (); peopleQ.Enqueue (new Person {FirstIIame= "Homer", LastName="Simpson", Age=47}); peopleQ.Enqueue (new Person {FirstName= "Marge", LastName="Simpson", Age=45}); peopleQ.Enqueue(new Person {FirstName= "Lisa", LastName="Sirrpson", Age=9}); // Кто первый в очереди? Console.WriteLine("{0} is first in line!", peopleQ.Peek().FirstName); // Удалить всех из очереди. GetCoffee(peopleQ.Dequeue ()), GetCoffee(peopleQ.Dequeue()), GetCoffee(peopleQ.Dequeue()), // Попробовать извлечь кого-то из очереди снова? try { GetCoffee(peopleQ.Dequeue() ) ; } catch(InvalidOperationException e) { Console.WriteLine("Error' {0}", e.Message); // очередь пуста } }
Глава 10. Обобщения 375 Здесь вы вставляете три элемента в класс Queue<T>, используя метод Enqueue(). Вызов Peek () позволяет вам просматривать (но не удалять) первый элемент, находящийся в данный момент в Queue. Наконец, вызов Dequeue () удаляет элемент из очереди и посылает его вспомогательной функции GetCof fee () для обработки. Заметьте, что если вы пытаетесь удалять элементы из пустой очереди, генерируется исключение времени выполнения. Приведем вывод, который вы получаете при вызове этого метода: ***** Fun with Generic Collections ***** Homer is first in line1 Homer got coffee! Marge got coffee! Lisa got coffee! Error! Queue empty. Работа с классом SortedSet<T> Последний из классов обобщенных коллекций, который мы рассмотрим здесь, появился в версии .NET 4.0. Класс SortedSet<T> удобен тем, что при вставке или удалении элементов он автоматически обеспечивает сортировку элементов в наборе. Класс SortedSet<T> понадобится информировать о том, как должны сортироваться объекты, за счет передачи его конструктору аргумента — объекта, реализующего интерфейс IComparer<T>. Начнем с создания нового класса по имени SortPeopleByAge, реализующего IComparer<T>, где Т — тип Person. Вспомните, что этот интерфейс определяет единственный метод по имени Compare(), в котором можно запрограммировать логику сравнения элементов. Ниже приведена простая реализация этого класса: class SortPeopleByAge : IComparer<Person> { public int Compare(Person firstPerson, Person secondPerson) { if (firstPerson.Age > secondPerson.Age) return 1; if (firstPerson.Age < secondPerson.Age) return -1; else return 0; } } Теперь добавим в класс Program следующий новый метод, который должен будет вызван в Main(): private static void UseSortedSet () { // Создать несколько людей разного возраста. SortedSet<Person> setOfPeople = new SortedSet<Person>(new SortPeopleByAge ()) { new Person {FirstName= "Homer", LastName="Simpson", Age=47}, new Person {FirstName= "Marge", LastName="Simpson", Age=45}, new Person {FirstName= "Lisa", LastName="Simpson", Age=9}, new Person {FirstName= "Bart", LastName="Simpson", Age=8} }; // Обратите внимание, что элементы отсортированы по возрасту. foreach (Person p in setOfPeople) { Console.WriteLine(p); }
376 Часть III. Дополнительные конструкции программирования на С# Console.WriteLine() ; // Добавить еще несколько людей разного возраста. setOfPeople.Add(new Person { FirstName = "Saku", LastName = "Jones", Age =1 }); setOfPeople.Add(new Person { FirstName = "Mikko", LastName = "Jones", Age = 32 }); // Элементы по-прежнему отсортированы по возрасту. foreach (Person p in setOfPeople) { Console.WriteLine(p); } } После запуска приложения видно, что список объектов будет всегда упорядочен по значению свойства Age, независимо от порядка вставки и удаления объектов в коллекцию: ***** Fun with Generic Collections ***** Name: Bart Simpson, Age: 8 Name: Lisa Simpson, Age: 9 Name: Marge Simpson, Age: 4 5 Name: Homer Simpson, Age: 4 7 Name: Saku Jones, Age: 1 Name: Bart Simpson, Age: 8 Name: Lisa Simpson, Age: 9 Name: Mikko Jones, Age: 32 Name: Marge Simpson, Age: 4 5 Name: Homer Simpson, Age: 4 7 Великолепно! Теперь вы должны почувствовать себя увереннее, причем не только в отношении преимуществ обобщенного программирования вообще, но также в использовании обобщенных типов из библиотеки базовых классов .NET. В завершении этой главы будет также показано, как и для чего строить собственные обобщенные типы и обобщенные методы. Исходный код. Проект FunWithGenericCollections доступен в подкаталоге Chapter 10. Создание специальных обобщенных методов Хотя большинство разработчиков обычно используют существующие обобщенные типы из библиотек базовых классов, можно также строить собственные обобщенные методы и специальные обобщенные типы. Чтобы понять, как включать обобщения в собственные проекты, начнем с построения обобщенного метода обмена, предварительно создав новое консольное приложение по имени GenericMethods. Построение специальных обобщенных методов представляет собой более развитую версию традиционной перегрузки методов. В главе 2 было показано, что перегрузка — это определение нескольких версий одного метода, отличающихся друг от друга количеством или типами параметров. Хотя перегрузка — полезное средство объектно-ориентированного языка, при этом возникает проблема, вызванная появлением огромного числа методов, которые по существу делают одно и то же. Например, предположим, что требуется создать методы, которые позволяют менять местами два фрагмента данных. Можно начать с написания простого метода для обмена двух целочисленных значений:
Глава 10. Обобщения 377 // Обмен двух значений int. static void Swap(ref int a, ref int b) { int temp; temp = a; a = b; b = temp; } Пока все хорошо. А теперь представим, что нужно поменять местами два объекта Person; для этого понадобится новая версия метода Swap(): // Обмен двух объектов Person. static void Swap(ref Person a, ref Person b) { Person temp; temp = a; a = b; b = temp; } Уже должно стать ясно, куда это ведет. Если также потребуется поменять местами два значения с плавающей точкой, две битовые карты, два объекта автомобилей, придется писать дополнительные методы, что в конечном итоге превратится в кошмар при сопровождении. Правда, можно построить один (необобщенный) метод, оперирующий параметрами типа object, но тогда возникнут проблемы, которые были описаны ранее в этой главе, те. упаковка, распаковка, недостаток безопасности типов, явное приведение и т.п. Всякий раз, когда имеется группа перегруженных методов, отличающихся только входными аргументами — это явный признак того, что за счет применения обобщений удастся облегчить себе жизнь. Рассмотрим следующий обобщенный метод Swap<T>, который может менять местами два значения Т: // Этот метод обменивает между собой значения двух // элементов типа, переданного в параметре <Т>. static void Swap<T>(ref T a, ref T b) { Console.WriteLine("You sent the Swap() method a {0}", typeof(T)); T temp; temp = a; a = b; b = temp; } Обратите внимание, что обобщенный метод определен за счет спецификации параметра типа после имени метода и перед списком параметров. Здесь устанавливается, что метод Swap () может оперировать любыми двумя параметрами типа <Т>. Чтобы немного прояснить картину, имя подставляемого типа выводится на консоль с использованием операции typeof (). Теперь рассмотрим следующий метод Мат(), обменивающий значениями целочисленные и строковые переменные: static void Main(string[] args) { Console.WriteLine ("***** Fun with Custom Generic Methods *****\n"); // Обмен двух значений int. int a = 10, b = 90; Console.WriteLine("Before swap: {0}, {1}", a, b) ; Swap<int>(ref a, ref b) ; Console.WriteLine("After swap: {0}, {1}", a, b) ; Console.WriteLine();
378 Часть III. Дополнительные конструкции программирования на С# // Обмен двух строк, string si = "Hello", s2 = "There"; Console.WriteLine("Before swap: {0} {1}!", si, s2) ; Swap<string> (ref si, ref s2); Console.WriteLine("After swap: {0} {1}!", si, s2); Console.ReadLine(); } Ниже показан вывод этого примера: ***** Fun with Custom Generic Methods ***** Before swap: 10, 90 You sent the SwapO method a System. Int32 After swap: 90, 10 Before swap: Hello There 1 You sent the SwapO method a System. String After swap: There Hello! Основное преимущество этого подхода в том, что нужно будет сопровождать только одну версию Swap<T>(), хотя она может оперировать любыми двумя элементами определенного типа, причем в безопасной к типам манере. Еще лучше то, что находящиеся в стеке элементы остаются в стеке, а расположенные в куче — соответственно, в куче. Выведение параметра типа При вызове таких обобщенных методов, как Swap<T>, можно опускать параметр тип, если (и только если) обобщенный метод требует аргументов, поскольку компилятор может вывести параметр типа из параметров членов. Например, добавив к Main () следующий код, можно обменивать значения System.Boolean: // Компилятор самостоятельно выведет тип System.Boolean. bool Ы = true, Ь2 = falser- Console .WriteLine ("Before swap: {0}, {1}", Ы, b2); Swap(ref Ы, ref b2) ; Console.WriteLine ("After swap: {0}, {1}", Ы, b2) ; Несмотря на то что компилятор может определить параметр типа на основе типа данных, использованного в объявлении Ы и Ь2, стоит выработать привычку всегда указывать параметр типа явно: Swap<bool> (ref Ы, ref Ь2); Это даст понять неопытным программистам, что данный метод на самом деле является обобщенным. Более того, выведение типов параметров работает только в том случае, если обобщенный метод имеет, по крайней мере, один параметр. Например, предположим, что в классе Program определен следующий обобщенный метод: static void DisplayBaseClass<T> () { // BaseType — это метод, используемый в рефлексии; // он будет рассматриваться в главе 15. Console.WriteLine("Base class of {0} is: {1}.", typeof(T), typeof(T).BaseType); } При его вызове потребуется указать параметр типа: static void Main(string [ ] args) { // Необходимо указать параметр типа если метод не принимает параметров.
Глава 10. Обобщения 379 DisplayBaseClass<int>(); DisplayBaseClass<string>(); // Ошибка на этапе компиляции! Нет параметров? // Значит, необходимо указать тип для подстановки! // DisplayBaseClass() ; Console.ReadLine(); } В настоящее время обобщенные методы Swap<T> и DisplayBaseClass<T> определены в классе Program приложения. Конечно, как и любой другой метод, если вы захотите определить эти члены в отдельном классе (MyGenericMethods), то можно поступить так: public static class MyGenericMethods { public static void Swap<T>(ref T a, ref T b) { Console.WriteLine("You sent the SwapO method a {0}", typeof(T)); T temp; temp = a; a = b; b = temp; } public static void DisplayBaseClass<T> () { Console.WriteLine ("Base class of {0} is: {1}.", typeof(T), typeof(T).BaseType); } } Статические методы Swap<T>HDisplayBaseClass<T> находятся в контексте нового статического типа класса, поэтому потребуется указать имя типа при вызове каждого члена, например: MyGenericMethods.Swap<int> (ref a, ref b) ; Разумеется, методы не обязательно должны быть статическими. Если бы Swap<T> и DisplayBaseClass<T> были методами уровня экземпляра (определенными в нестатическом классе), нужно было бы просто создать экземпляр MyGenericMethods и вызывать их с использованием объектной переменной: MyGenericMethods с = new MyGenericMethods(); c.Swap<int>(ref a, ref b) ; Исходный код. Проект CustomGenericMethods доступен в подкаталоге Chapter 10. Создание специальных обобщенных структур и классов Теперь, когда известно, как определяются и вызываются обобщенные методы, давайте посмотрим, как конструировать обобщенную структуру (процесс построения обобщенного класса идентичен) в новом проекте консольного приложения по имени GenericPoint. Предположим, что строится обобщенная структура Point, которая поддерживает единственный параметр типа, определяющий внутреннее представление координат (х, у). Вызывающий код должен иметь возможность создавать типы Point<T> следующим образом:
380 Часть III. Дополнительные конструкции программирования на С# // Точка с координатами int. Point<int> p = new Point<int>A0, 10); // Точка с координатами double. Point<double> р2 = new Point<double>E.4, 3.3); А вот полное определение Point<T> с последующим анализом: // Обобщенная структура Point. public struct Point<T> { // Обобщенные данные состояния. private T xPos; private T yPos; // Обобщенный конструктор. public Point(T xVal, T yVal) { xPos = xVal; yPos = yVal; } // Обобщенные свойства. public T X { get { return xPos; } set { xPos = value; } } public T Y { get { return yPos; } set { yPos = value; } } public override string ToStringO { return string.Format (" [{0 }, {1}]", xPos, yPos); } // Сбросить поля в значения по умолчанию для заданного параметра типа. public void ResetPointO { xPos = default(T); yPos = default(T); Ключевое слово default в обобщенном коде Как видите, Point<T> использует параметр типа в определении данных полей, аргументов конструктора и определении свойств. Обратите внимание, что в дополнение к переопределению ToStringO, в Point<T> определен метод по имени ResetPointO, в котором используется некоторый новый синтаксис: // Ключевое слово default перегружено в С#. // При использовании с обобщениями оно представляет // значение по умолчанию для параметра типа. public void ResetPointO { X = default(T); Y = default(T); }
Глава 10. Обобщения 381 С появлением обобщений ключевое слово default обрело второй смысл. В дополнение к использованию с конструкцией switch оно теперь может применяться для установки значения по умолчанию для параметра типа. Это очень удобно, учитывая, что обобщенный тип не знает заранее, что будет подставлено вместо заполнителя в угловых скобках, и потому не может безопасно строить предположений о значениях по умолчанию. Умолчания для параметров типа следующие: • значение по умолчанию числовых величин равно 0; • ссылочные типы имеют значение по умолчанию null; • поля структур устанавливаются в 0 (для типов значений) или в null (для ссылочных типов). Для Point<T> можно было установить значение X и Y в 0 напрямую, исходя из предположения, что вызывающий код будет применять только числовые значения. Однако за счет использования синтаксиса default(T) повышается общая гибкость обобщенного типа. В любом случае теперь можно использовать методы Point<T> следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Generic Structures *****\n"); // Объект Point, в котором используются int. Point<int> p = new Point<int>A0, 10); Console.WriteLine ("p.ToString () ={ 0} ", p.ToStringO ) ; p.ResetPoint(); Console.WriteLine("p.ToString()={0}", p.ToString ()); Console.WriteLine (); // Объект Point, в котором используются double. Point<double> p2 = new Point<double>E.4, 3.3); Console.WriteLine("p2.ToString()={0}", p2.ToString ()); p2.ResetPoint(); Console.WriteLine("p2.ToString()={0}", p2.ToString ()); Console.ReadLine(); } Ниже показан вывод этого примера: ***** Fun with Generic Structures ***** p.ToString()=[10, 10] p.ToStnng() = [0, 0] p2.ToStnng() = [5.4, 3.3] p2.ToStnng() = [0, 0] Исходный код. Проект GenericPoint доступен в подкаталоге Chapter 10. Обобщенные базовые классы Обобщенные классы могут служить базовыми для других классов, и потому определять любое количество виртуальных или абстрактных методов. Однако типы-нас- | ледники должны подчиняться нескольким правилам, чтобы гарантировать сохранение природы обобщенной абстракции. Во-первых, если необобщенный класс расширяет I обобщенный класс, то класс-наследник должен указывать параметр типа:
382 Часть III. Дополнительные конструкции программирования на С# // Предположим, что создан специальный класс обобщенного списка. public class MyList<T> { private List<T> listOfData = new List<T>(); } // Необобщенные типы должны указывать параметр типа // при наследовании от обобщенного базового класса. public class MyStnngList : MyList<string> {} Во-вторых, если обобщенный базовый класс определяет обобщенные или абстрактные методы, то тип-наследник должен переопределять обобщенные методы, используя указанный параметр типа: // Обобщенный класс с виртуальным методом. public class MyList<T> { private List<T> listOfData = new List<T>(); public virtual void PrintList(T data) { } } public class MyStnngList : MyList<string> { // Должен подставлять параметр типа, использованный //в родительском классе, в унаследованные методы. public override void PrintList(string data) { } } В-третьих, если тип-наследник также является обобщенным, дочерний класс может (дополнительно) повторно использовать тот же заполнитель типа в своем определении. Однако имейте в виду, что любые ограничения, наложенные на базовый класс, должны соблюдаться в типе-наследнике. Например: // Обратите внимание на ограничение — конструктор по умолчанию. public class MyList<T> where T : new() { private List<T> listOfData = new List<T>(); public virtual void PrintList(T data) { } } // Тип-наследник должен соблюдать ограничения. public class MyReadOnlyList<T> : MyList<T> where T : new() { public override void PrintList(T data) { } } При решении повседневных программистских задач вряд ли часто придется создавать иерархии специальных обобщенных классов. Тем не менее, это вполне возможно (до тех пор, пока вы придерживаетесь правил). Ограничение параметров типа Как показано в этой главе, любой обобщенный элемент имеет, по крайней мере, один параметр типа, который должен быть указан при взаимодействии с обобщенным типом или членом. Одно это позволит строить безопасный в отношении типов код; однако платформа .NET позволяет использовать ключевое слово where для"указания особых требований к определенному параметру типа. С помощью ключевого слова this можно добавлять набор ограничений к конкретному параметру типа, которые компилятор С# проверит во время компиляции. В частности, параметр типа можно ограничить, как описано в табл. 10.6.
Глава 10. Обобщения 383 Таблица 10.6. Возможные ограничения параметров типа для обобщений Ограничение обобщения Назначение where T : struct Параметр типа <Т> должен иметь в своей цепочке наследования System.ValueType. Другими словами, <Т> должен быть структурой where T : class Параметр типа <Т> должен не иметь System .ValueType в своей цепочке наследования (т.е. <т> должен быть ссылочным типом) where T : new() Параметр типа <Т> должен иметь конструктор по умолчанию. Это очень полезно, если обобщенный тип должен создавать экземпляры параметра типа, поскольку не удается предположить формат специальных конструкторов. Обратите внимание, что в типе со многими ограничениями это ограничение должно указываться последним where T : ИмяБазовогоКласса Параметр типа <Т> должен быть наследником класса, указанного в ИмяБазовогоКласса where T : ИмяИнтерфейса Параметр типа <Т> должен реализовать интерфейс, указанный в ИмяИнтерфейса. Можно задавать несколько интерфейсов, разделяя их запятыми Если только не требуется строить какие-то исключительно безопасные к типам специальные коллекции, возможно, никогда не придется использовать ключевое слово where в проектах С#. Так или иначе, но в следующих нескольких примерах (частичного) кода демонстрируется работа с ключевым словом where. Примеры использования ключевого слова where Будем исходить из того, что создан специальный обобщенный класс, и необходимо гарантировать наличие в параметре типа конструктора по умолчанию. Это может быть полезно, когда специальный обобщенный класс должен создавать экземпляры Т, потому что конструктор по умолчанию — это единственный конструктор, потенциально общий для всех типов. Также подобного рода ограничение Т позволит производить проверку во время компиляции; если Т — ссылочный тип, то компилятор напомнит программисту о необходимости переопределения конструктора по умолчанию в объявлении класса (если помните, конструкторы по умолчанию удаляются из классов, в которых определяются собственные конструкторы). // Класс MyGenericClass унаследован от object, примем содержащиеся //в нем элементы должны иметь конструктор по умолчанию, public class MyGenericClass<T> where T : new() { Обратите внимание, что конструкция where указывает параметр типа, на который накладывается ограничение, а за ним следует операция двоеточия. После этой операции перечисляются все возможные ограничения (в данном случае — конструктор по умолчанию). Ниже показан еще один пример: // MyGenericClass унаследован от Object, причем содержащиеся // в нем элементы должны относиться к классу, реализующему IDrawable. //и поддерживать конструктор по умолчанию. public class MyGenericClass<T> where T : class, IDrawable, new() {...}
384 Насть III. Дополнительные конструкции программирования на С# В данном случае к Т предъявляются три требования. Во-первых, это должен быть ссылочный тип (не структура), что помечено лексемой class. Во-вторых, Т должен реа- лизовывать интерфейс IDrawable. В-третьих, он также должен иметь конструктор по умолчанию. Множество ограничений перечисляются в списке, разделенном запятыми; однако имейте в виду, что ограничение new() всегда должно идти последним! По этой причине следующий код не скомпилируется: // Ошибка! Ограничение new() должно быть последним в списке! public class MyGenericClass<T> where T : new(), class, IDrawable { } В случае создания обобщенного класса коллекции с несколькими параметрами типа, можно указывать уникальный набор ограничений для каждого параметра с помощью отдельной конструкции where: // <К> должен расширять SomeBaseClass и иметь конструктор по умолчанию, в то время // как <Т> должен быть структурой и реализовывать обобщенный интерфейс IComparable. public class MyGenericClass<K, T> where К : SomeBaseClass, new() where T : IComparable<T> { } Необходимость построения полностью нового обобщенного класса коллекции возникает редко; однако ключевое слово where также допускается применять и в обобщенных методах. Например, если необходимо гарантировать, чтобы метод Swap<T>() работал только со структурами, обновите код следующим образом: // Этот метод обменяет местами любые структуры, но не классы. static void Swap<T>(ref T a, ref T b) where T : struct { } Обратите внимание, что если ограничить метод SwapO подобным образом, обменивать местами объекты string (как это делалось в коде примера) уже не получится, поскольку string является ссылочным типом. Недостаток ограничений операций В конце этой главы следует упомянуть об одном моменте относительно обобщенных методов и ограничений. При создании обобщенных методов может оказаться сюрпризом появление ошибок компиляции во время применения любых операций С# (+, -, *, == и т.д.) к параметрам типа. Например, подумайте о пользе от класса, который может выполнять Add(), SubstractO, MultiplyO HDevideO над обобщенными типами: // Ошибка на этапе компиляции' Нельзя применять // арифметические операции к параметрам типа! public class Bas±cMath<T> { public T Add(T argl, T arg2) { return argl + arg2; } public T Subtract (T argl, T arg2) { return argl - arg2; } public T Multiply(T argl, T arg2) { return argl * arg2; } public T Divide(T argl, T arg2) { return argl / arg2; } }
Глава 10. Обобщения 385 К сожалению, приведенный выше класс BasicMath<T> не скомпилируется. Хотя это может показаться серьезным недостатком, следует снова вспомнить, что обобщения являются общими. Естественно, числовые данные работают достаточно хорошо с бинарными операциями С#. Однако если <Т> будет специальным классом или структурой, то компилятор мог бы предположить, что этот класс поддерживает операции +, -, * и /. В идеале язык С# должен был бы позволять ограничивать обобщенные типы поддерживаемыми операциями, например: // Код только для иллюстрации! public class BasicMath<T> where T : operator +, operator -, operator *, operator / { public T Add(T argl, T arg2) { return argl + arg2; } public T Subtract (T argl, T arg2) { return argl - arg2; } public T Multiply (T argl, T arg2) { return argl * arg2; } public T Divide (T argl, T arg2) { return argl / arg2; } } К сожалению, ограничения операций в текущей версии С# не поддерживаются. Однако можно (хотя это и потребует дополнительной работы) достичь желаемого эффекта, определив интерфейс, поддерживающий эти операции (интерфейсы С# могут определять операции) и затем указать ограничение интерфейса для обобщенного класса. На этом первоначальный обзор построения специальных обобщенных типов завершен. В следующей главе мы вновь обратимся к теме обобщений, когда будем рассматривать тип делегата .NET. Резюме Настоящая глава начиналась с рассмотрения использования классических контейнеров, включенных в пространство имен System.Collections. Хотя эти типы будут и далее поддерживаться для обратной совместимости, новые приложения .NET выиграют от применения вместо них новых обобщенных аналогов из пространства имен System.Collections.Generic. Как вы видели, обобщенный элемент позволяет специфицировать заполнители (параметры типа), которые указываются во время создания (или вызова — в случае обобщенных методов). По сути, обобщения предлагают решение проблем упаковки и обеспечения безопасности типов, которые досаждали при разработке программного обеспечения для .NET 1.1. Вдобавок обобщенные типы в значительной мере исключают необходимость в построении специальных типов коллекций. Хотя чаще всего обобщенные типы, представленные в библиотеках базовых классов .NET, просто будут использоваться, можно также создавать и собственные обобщенные типы (и обобщенные методы). При этом есть возможность задавать любое количество ограничений (с помощью ключевого слова where), чтобы повышать уровень безопасности в отношении типов и гарантировать выполнение операций над типами в "известном количестве"; это обеспечивает предоставление определенных базовых возможностей.
ГЛАВА 11 Делегаты, события и лямбда-выражения Вплоть до этого момента большинство разработанных приложений добавляли различные порции кода к методу Main(), тем или иным способом посылающие запросы заданному объекту. Однако многие приложения требуют, чтобы объект мог обращаться обратно к сущности, которая создала его — посредством механизма обратного вызова. Хотя механизмы обратного вызова могут применяться в любом приложении, они особенно важны в графических интерфейсах пользователя, где элементы управления (такие как кнопки) нуждаются в вызове внешних методов при надлежащих условиях (выполнен щелчок на кнопке, курсор мыши находится на поверхности кнопки и т.п.). На платформе .NET тип делегата является предпочтительным средством определения и реагирования на обратные вызовы в приложении. По сути, тип делегата .NET — это безопасный к типам объект, который "указывает" на метод или список методов, которые могут быть вызваны позднее. Однако в отличие от традиционного указателя на функцию C++, делегаты .NET представляют собой классы, обладающие встроенной поддержкой группового выполнения и асинхронного вызова методов. В этой главе будет показано, как создавать и управлять типами делегатов, а также использовать ключевое слово event в С#, которое облегчает работу с типами делегатов. По пути вы также изучите несколько языковых средств С#, ориентированных на делегаты и события, включая анонимные методы и групповые преобразования методов. Завершается глава исследованием лямбда-выраженияй (lambda expressions). Используя лямбда-операцию С# (=>), теперь можно указывать блок операторов кода (и параметры для передачи этим операторам) везде, где требуется строго типизированный делегат. Как будет показано, лямбда-выражение — это немногим более чем маскировка анонимного метода, и представляет собой упрощенный подход к работе с делегатами. Понятие типа делегата .NET Прежде чем приступить к формальному определению делегатов .NET, давайте оглянемся немного назад. В интерфейсе Windows API часто использовались указатели на функции в стиле С для создания сущностей, именуемых функциями обратного вызова (callback functions) или просто обратными вызовами (callbacks). С помощью обратных вызовов программисты могли конфигурировать одну функцию таким образом, чтобы она осуществляла обратный вызов другой функции в приложении. Применяя такой подход, разработчики Windows смогли обрабатывать щелчки на кнопках, перемещения курсора мыши, выбор пунктов меню и общие двусторонние коммуникации между программными сущностями в памяти.
Глава 11. Делегаты, события и лямбда-выражения 387 Проблема со стандартными функциями обратного вызова в стиле С заключается в том, что они представляют собой лишь немногим более чем простой адрес в памяти. В идеале обратные вызовы могли бы конфигурироваться для включения дополнительной безопасной к типам информации, такой как количество и типы параметров и возвращаемого значения (если оно есть) метода, на который они указывают. К сожалению, это невозможно с традиционными функциями обратного вызова и это, как следовало ожидать, является постоянным источником ошибок, аварийных завершений и прочих неприятностей во время выполнения. Тем не менее, обратные вызовы — удобная вещь. В .NET Framework обратные вызовы по-прежнему возможны, и их функциональность обеспечивается в гораздо более безопасной и объектно-ориентированной манере с использованием делегатов. По сути, делегат — это безопасный в отношении типов объект, указывающий на другой метод (или, возможно, список методов) приложения, который может быть вызван позднее. В частности, объект делегата поддерживает три важных фрагмента информации: • адрес метода, на котором он вызывается; • аргументы (если есть) этого метода; • возвращаемое значение (если есть) этого метода. На заметку! Делегаты .NET могут указывать как на статические, так и на методы экземпляра. Как только делегат создан и снабжен необходимой информацией, он может динамически вызывать методы, на которые указывает, во время выполнения. Каждый делегат в .NET Framework (включая специальные делегаты) автоматически снабжается способностью вызывать свои методы синхронно или асинхронно. Этот факт значительно упрощает задачи программирования, поскольку позволяет вызывать метод во вторичном потоке выполнения без ручного создания и управления объектом Thread. На заметку! Асинхронное поведение типов делегатов будет рассматриваться во время исследования пространства имен System.Threading в главе 19. Определение типа делегата в С# Для определения делегата в С# используется ключевое слово delegate. Имя делегата может быть любым. Однако сигнатура определяемого делегата должна соответствовать сигнатуре метода, на который он будет указывать. Например, предположим, что планируется построить тип делегата по имени BinaryOp, который может указывать на любой метод, возвращающий целое число и принимающий два целых числа в качестве входных параметров: // Этот делегат может указывать на любой метод, который // принимает два целых и возвращает целое значение. public delegate int BinaryOp(int x, int y); Когда компилятор С# обрабатывает тип делегата, он автоматически генерирует запечатанный (sealed) класс, унаследованный от System.MulticastDelegate. Этот класс (в сочетании с его базовым классом System.Delegate) предоставляет необходимую инфраструктуру для делегата, чтобы хранить список методов, подлежащих вызову в более позднее время. Например, если просмотреть делегат BinaryOp с помощью утилиты ildasm.exe, обнаружится класс, показанный на рис. 11.1.
388 Часть III. Дополнительные конструкции программирования на С# File View Help v H:\My Books\C# Book\C# and the .NET Platform 5th ed\First Draft\Chapter_l l\Code\SimpleDelegate\obj\x86\Debug\Simpl< j ► MANIFEST й Щ SimpleDelegate ► .class public auto ansi sealed ► extends [mscorlib]5ystem.MulticastDelegate I ■ .ctor: void(object, native int) ■ Beginlnvoke : class [mscorlib]5ystem.IAsyncResult(int32,int32,class [mscorlibjSystem.AsyncCallbackjobject) ■ Endlnvoke : int32(class[mscorlib]System.IAsyncResult) j ■ Invoke : int32(int32,int32) ffi-flE SimpleDelegate.Program .assembly SimpleDelegate Рис. 11.1. Ключевое слово delegate представляет запечатанный класс, унаследованный от System.MulticastDelegate Как видите, сгенерированный компилятором класс BinaryOp определяет три общедоступных метода. Invoke () — возможно, главный из них, поскольку он используется для синхронного вызова каждого из методов, поддерживаемых объектом делегата; это означает, что вызывающий код должен ожидать завершения вызова, прежде чем продолжить свою работу. Может показаться странным, что синхронный метод Invoke () не должен вызываться явно в коде С#. Как вскоре будет показано, Invoke () вызывается "за кулисами", когда применяется соответствующий синтаксис С#. Методы Beginlnvoke() и Endlnvoke() предлагают возможность вызова текущего метода асинхронным образом, в отдельном потоке выполнения. Имеющим опыт в многопоточной разработке должно быть известно, что одной из основных причин, вынуждающих разработчиков создавать вторичные потоки выполнения, является необходимость вызова методов, которые требуют определенного времени для завершения. Хотя в библиотеках базовых классов .NET предусмотрено целое пространство имен, посвященное многопоточному программированию (System.Threading), делегаты предлагают эту функциональность в готовом виде. Каким же образом компилятор знает, как следует определить методы Invoke(), Beginlnvoke () и Endlnvoke()? Чтобы разобраться в процессе, ниже приведен код сгенерированного компилятором типа класса BinaryOp (полужирным курсивом выделены элементы, указанные определенным типом делегата): sealed class BinaryOp : System.MulticastDelegate { public int Invoke(int x, int y) ; public IAsyncResult Beginlnvoke(int x, int y, AsyncCallback cb, object state); public int Endlnvoke(IAsyncResult result); Первым делом, обратите внимание, что параметры и возвращаемый тип для метода Invoke () в точности соответствуют определению делегата BinaryOp. Начальные параметры для членов Beginlnvoke () (в данном случае — два целых) также основаны на делегате BinaryOp; однако Beginlnvoke () всегда будет предоставлять два финальных параметра (типа AsyncCallback и object], используемых для облегчения асинхронного вызова методов. И, наконец, возвращаемый тип Endlnvoke () идентичен исходному объявлению делегата и всегда принимает единственный параметр — объект, реализующий интерфейсIAsyncResult.
Глава 11. Делегаты, события и лямбда-выражения 389 Давайте рассмотрим другой пример. Предположим, что определен тип делегата, который может указывать на любой метод, возвращающий string и принимающий три входных параметра System.Boolean: public delegate string MyDelegate(bool a, bool b, bool c) ; На этот раз сгенерированный компилятором класс будет выглядеть так: sealed class MyDelegate : System.MulticastDelegate { public string Invoke (bool a, bool b, bool c) ; public IAsyncResult Beginlnvoke(bool a, bool b, bool c, AsyncCallback cb, object state); public string Endlnvoke(IAsyncResult result); } Делегаты также могут "указывать" на методы, содержащие любое количество параметров out и ref (как и массивов параметров, помеченных ключевым словом params). Например, предположим, что имеется следующий тип делегата: public delegate string MyOtherDelegate (out bool a, ref bool b, int c) ; Сигнатуры методов Invoke () и Beginlnvoke () выглядят так, как и следовало ожидать; однако взгляните на метод Endlnvoke (), который теперь включает набор аргументов out/ref, определенных типом делегата: sealed class MyOtherDelegate : System.MulticastDelegate { public string Invoke (out bool a, ref bool b, int c) ; public IAsyncResult Beginlnvoke (out bool a, ref bool b, int c, AsyncCallback cb, object state); public string Endlnvoke (out bool a, ref bool b, IAsyncResult result); } Чтобы подытожить: определение типа делегата С# порождает запечатанный класс с тремя сгенерированными компилятором методами, типы параметров и возвращаемых значений которых основаны на объявлении делегата. Базовый шаблон может быть описан с помощью следующего псевдокода: // Это только псевдокод! public sealed class ИмяДелегата : System.MulticastDelegate { public возвращаемоеЗначениеДелегата Invoke (всеВходныеВ.е£и0и1ПараметрыДелегата) ; public IAsyncResult Beginlnvoke (всеВходныеВ.е£иОиЬПараметрыДелегата, AsyncCallback cb, object state); public возвращаемоеЗначениеДелегата Endlnvoke (всеВходныеЯе£и0иИ1араметрыДелегата, IAsyncResult result); } Базовые классы System.MulticastDelegate и System.Delegate При построении типа, использующего ключевое слово delegate, неявно объявляется тип класса, унаследованного от System.MulticastDelegate. Этот класс обеспечивает своих наследников доступом к списку, который содержит адреса методов, поддерживаемых типом делегата, а также несколько дополнительных методов (и несколько перегруженных операций), чтобы взаимодействовать со списком вызовов. Ниже показаны некоторые избранные методы System.MulticastDelegate:
390 Часть III. Дополнительные конструкции программирования на С# public abstract class MulticastDelegate : Delegate { // Возвращает список методов, на которые "указывает" делегат. public sealed override Delegate[] GetlnvocationList (); // Перегруженные операции. public static bool operator ==(MulticastDelegate dl, MulticastDelegate d2) ; public static bool operator ! = (MulticastDelegate dl, MulticastDelegate d2) ; // Используются внутренне для управления списком методов, поддерживаемых делегатом. private IntPtr _invocationCount; private object _invocationList; } Класс System.MulticastDelegate получает дополнительную функциональность от своего родительского класса System.Delegate. Ниже показан фрагмент определения класса: public abstract class Delegate : ICloneable, ISerializable { // Методы для взаимодействия со списком функций. public static Delegate Combine(params Delegate[] delegates); public static Delegate Combine(Delegate a, Delegate b) ; public static Delegate Remove(Delegate source, Delegate value); public static Delegate RemoveAll(Delegate source, Delegate value); // Перегруженные операции. public static bool operator ==(Delegate dl, Delegate d2) ; public static bool operator !=( Delegate dl, Delegate d2); // Свойства, показывающие цель делегата. public Methodlnfo Method { get; } public object Target { get; } } Запомните, что вы никогда не сможете напрямую наследовать от этих базовых классов в коде (при попытке сделать это выдается ошибка компиляции). Тем не менее, при использовании ключевого слова delegate неявно создается класс, который "является" MulticastDelegate. В табл. 11.1 описаны основные члены, общие для всех типов делегатов. Таблица 11.1. Основные члены System.MultcastDelegate/System.Delegate Член Назначение Method Это свойство возвращает объект System.Reflection.Method, который представляет детали статического метода, поддерживаемого делегатом Target Если метод, подлежащий вызову, определен на уровне объекта (т.е. не является статическим), то Target возвращает объект, представляющий метод, поддерживаемый делегатом. Если возвращенное Target значение равно null, значит, подлежащий вызову метод является статическим Combine () Этот статический метод добавляет метод в список, поддерживаемый делегатом. В С# этот метод вызывается за счет использования перегруженной операции += в качестве сокращенной нотации GetlnvokationListO Этот метод возвращает массив типов System.Delegate, каждый из которых представляет определенный метод, доступный для вызова Remove () Эти статические методы удаляют метод (или все методы) из списка RemoveAl 1 () вызовов делегата. В С# метод Remove () может быть вызван неявно, посредством перегруженной операции -=
Глава 11. Делегаты, события и лямбда-выражения 391 Простейший пример делегата При первоначальном знакомстве делегаты могут показаться очень сложными. Рассмотрим- для начала очень простое консольное приложение под названием SimpleDelegate, в котором используется ранее показанный тип делегата BinaryOp. Ниже приведен полный код. namespace SimpleDelegate { // Этот делегат может указывать на любой метод, // принимающий два целых и возвращающий целое. public delegate int BinaryOp(int x, int y) ; // Этот класс содержит методы, на которые будет указывать BinaryOp. public class SimpleMath { public static int Add(int x, int y) { return x + y; } public static int Subtract (int x, int y) { return x - y; } } class Program { static void Main(string[] args) { Console.WriteLine ("***** Simple Delegate Example *****\n"); // Создать объект делегата BinaryOp, "указывающий" на SimpleMath.Add(). BinaryOp b = new BinaryOp(SimpleMath.Add); // Вызвать метод AddQ непрямо с использованием объекта делегата. Console.WriteLine(0 + 10 is {0}", bA0, 10)); Console.ReadLine(); } } } Обратите внимание на формат объявления типа делегата BinaryOp: он определяет, что объекты делегата BinaryOp могут указывать на любой метод, принимающий два целых и возвращающий целое (действительное имя метода, на который он указывает, не существенно). Здесь создан класс по имени SimpleMath, определяющий два статических метода, которые соответствуют шаблону, определенному делегатом BinaryOp. Когда нужно вставить целевой метод в заданный объект делегата, просто передайте имя этого метода конструктору делегата: // Создать делегат BinaryOp, который "указывает" на SimpleMath.Add () . BinaryOp b = new BinaryOp(SimpleMath.Add); С этого момента указанный метод можно вызывать с использованием синтаксиса, который выглядит как прямой вызов функции: // Invoke() действительно вызывается здесь! Console.WriteLine(0 + 10 is {0}", bA0, 10)); "За кулисами" исполняющая система на самом деле вызывает сгенерированный компилятором метод Invoke () на производном классе MulticastDelegate. В этом можно убедиться, открыв сборку в утилите ildasm.exe и просмотрев код CIL в методе Main(): .method private hidebysig static void Main(string [ ] args) cil managed { callvirt instance int32 SimpleDelegate.BinaryOp::Invoke(int32, int32) }
392 Часть III. Дополнительные конструкции программирования на С# Хотя С# и не требует явного вызова Invoke () в коде, это вполне можно сделать. То есть следующий оператор кода является допустимым: Console.WriteLine(0 + 10 is {0}", b.InvokeA0, 10)); Вспомните, что делегаты .NET безопасны в отношении типов. Поэтому, попытка передать делегату метод, не соответствующий шаблону, приводит к ошибке времени компиляции. Чтобы проиллюстрировать это, предположим, что в классе SimpleMath теперь определен дополнительный метод по имени SquareNumber(), принимающий единственный целочисленный аргумент: public class SimpleMath { public static int SquareNumber (int a) { return a * a; } } Учитывая, что делегат В i па г у Op может указывать только на методы, принимающие два целых и возвращающие целое, следующий код неверен и компилироваться не будет: // Ошибка компиляции! Метод не соответствует шаблону делегата1 BinaryOp Ь2 = new BinaryOp(SimpleMath.SquareNumber); Исследование объекта делегата Давайте усложним текущий пример, создав статический метод (по имени DisplayDelegatelnfoO) в классе Program. Этот метод будет выводить на консоль имена методов, поддерживаемых объектом делегата, а также имя класса, определяющего метод. Для этого будет реализована итерация по массиву System.Delegate, возвращенному GetInvocationList(), с обращением к свойствам Target и Method каждого элемента. static void DisplayDelegatelnfо (Delegate delObj) { // Вывести на консоль имена каждого члена списка вызовов делегата. foreach (Delegate d in delObj.GetlnvocationList ()) { Console.WriteLine("Method Name: {0}", d.Method); // имя метода Console.WriteLine("Type Name: {0}", d.Target); // имя типа } } Исходя из предположения, что в метод Main() добавлен вызов этого вспомогательного метода, вывод приложения будет выглядеть следующим образом: ***** simple Delegate Example ***** Method Name: Int32 Add(Int32, Int32) Type Name: 10 + 10 is 20 Обратите внимание, что имя типа (SimpleMath) в настоящий момент не отображается при обращении к свойству Target. Причина в том, что делегат BinaryOp указывает на статический метод, и потому просто нет объекта, на который нужно ссылаться! Однако если сделать методы Add() и Substract() нестатическими (удалив в их объявлениях ключевые слова static), можно будет создавать экземпляр типа SimpleMath и указывать методы для вызова с использованием ссылки на объект:
Глава 11. Делегаты, события и лямбда-выражения 393 static void Main(string [ ] args) { Console.WriteLine("***** Simple Delegate Example *****\nM); // Делегаты .NET могут указывать на методы экземпляра. SimpleMath.m = new SimpleMath(); BinaryOp b = new BinaryOp(m.Add); // Вывести сведения об объекте. DisplayDelegatelnfо(b); Console.WriteLine(0 + 10 is {0}", bA0, 10)); Console.ReadLine(); } В этом случае вывод будет выглядеть, как показано ниже: ***** simple Delegate Example ***** Method Name: Int32 Add(Int32, Int32) Type Name: SimpleDelegate.SimpleMath 10 + 10 is 20 Исходный код. Проект SimpleDelegate доступен в подкаталоге Chapter 11. Отправка уведомлений о состоянии объекта с использованием делегатов Ясно, что предыдущий пример SimpleDelegate был предназначен только для целей иллюстрации, потому что нет особого смысла создавать делегат только для того, чтобы просуммировать два числа. Рассмотрим более реалистичный пример, в котором делегаты используются для определения класса Саг, обладающего способностью информировать внешние сущности о текущем состоянии двигателя. Для этого понадобится предпринять перечисленные ниже действия. • Определение нового типа делегата, который будет отправлять уведомления вызывающему коду. • Объявление переменной-члена этого делегата в классе Саг. • Создание вспомогательной функции в С а г, которая позволяет вызывающему коду указывать метод для обратного вызова. • Реализация метода Accelerate () для обращения к списку вызовов делегата при нужных условиях. Для начала создадим проект консольного приложения по имени CarDelegate. Затем определим новый класс Саг, который изначально выглядит следующим образом: public class Car { // Данные состояния. public int CurrentSpeed { get; set; } public int MaxSpeed { get; set; } public string PetName { get; set; } // Исправен ли автомобиль? private bool carlsDead; // Конструкторы класса, public Car() { MaxSpeed = 100; } public Car (string name, int maxSp, int currSp) I CurrentSpeed = currSp;
394 Часть III. Дополнительные конструкции программирования на С# MaxSpeed = maxSp; PetName = name; } } Ниже показаны обновления, связанные с реализацией трех первых пунктов: public class Car { // 1. Определить тип делегата. public delegate void CarEngineHandler(string msgForCaller); 112. Определить переменную-член типа этого делегата. private CarEngineHandler listOfHandlers; // 3. Добавить регистрационную функцию для вызывающего кода. public void RegisterWithCarEngine(CarEngineHandler methodToCall) { listOfHandlers = methodToCall; } } Обратите внимание, что в этом примере типы делегатов определяются непосредственно в контексте класса Саг. Исследование библиотек базовых классов покажет, что это довольно обычно — определять делегат внутри класса, который будет с ними работать. Наш тип делегата — CarEngineHandler — может указывать на любой метод, принимающий значение string в качестве параметра и имеющий void в качестве типа возврата. , Кроме того, была объявлена приватная переменная-член делегата (по имени listOfHandlers) и вспомогательная функция (по имени RegisterWithCarEngine()), которая позволяет вызывающему коду добавлять метод к списку вызовов делегата. На заметку! Строго говоря, можно было бы определить переменную-член делегата как public, избежав необходимости в добавлении дополнительных методов регистрации. Однако за счет определения этой переменной-члена делегата как private усиливается инкапсуляция и обеспечивается более безопасное к типам решение. Опасности объявления переменных-членов делегатов как public еще будут рассматриваться в этой главе, когда речь пойдет о ключевом слове event. Теперь необходимо создать метод Accelerate(). Вспомните, что здесь стоит задача позволить объекту Саг отправлять касающиеся двигателя сообщения любому подписавшемуся слушателю. Ниже показаны необходимые изменения в коде. public void Accelerate(int delta) { // Если этот автомобиль сломан, отправить сообщение об этом, if (carlsDead) { if (listOfHandlers '= null) listOfHandlers("Sorry, this car is dead..."); } else { CurrentSpeed += delta; // Автомобиль почти сломан? if A0 == (MaxSpeed - CurrentSpeed) && listOfHandlers != null) { listOfHandlers("Careful buddy! Gonna blow!"); }
Глава 11. Делегаты, события и лямбда-выражения 395 if (CurrentSpeed >= MaxSpeed) carlsDead = true; else Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed); } } Обратите внимание, что прежде чем вызывать методы, поддерживаемые переменной- членом listOf Handlers, она проверяется на равенство null. Причина в том, что размещать эти объекты вызовом вспомогательного метода RegisterWithCarEngine () — задача вызывающего кода. Если вызывающий код не вызовет этот метод, а мы попытаемся обратиться к списку вызовов делегата, то получим исключение NullRef erenceException и нарушим работу исполняющей системы (что очевидно нехорошо). Теперь, имея всю инфраструктуру делегатов, рассмотрим обновления класса Program: class Program { static void Main(string[] args) { Console.WriteLine ("***** Delegates as event enablers *****\n"); // Сначала создадим объект Car. Car cl = new Car ("SlugBug", 100, 10); // Теперь сообщим ему, какой метод вызывать, // когда он захочет отправить сообщение. cl.RegisterWithCarEngine(new Car.CarEngineHandler (OnCarEngineEvent)) ; // Ускорим (это инициирует события). Console.WriteLine ("***** Speeding up *****"); for (int l = 0; l < 6; i + +) cl.AccelerateB0); Console.ReadLine() ; } // Это цель для входящих сообщений. public static void OnCarEngineEvent (string msg) { Console.WriteLine("\n***** Message From Car Object *****"); Console.WriteLine("=> {0}", msg); Console WriteLine("***********************************\n" ) * } } Метод Main() начинается с создания нового объекта Car. Поскольку мы заинтересованы в событиях, связанных с двигателем, следующий шаг заключается в вызове специальной регистрационной функции RegisterWithCarEngine(). Вспомните, что этот метод ожидает получения экземпляра вложенного делегата CarEngineHandler, и как с любым делегатом, метод, на который он должен указывать, задается в параметре конструктора. Трюк в этом примере состоит в том, что интересующий метод находится в классе Program! Обратите внимание, что метод OnCarEngineEvent () полностью соответствует связанному делегату в том, что принимает string и возвращает void. Ниже показан вывод этого примера: ***** Delegates as event enablers ***** ***** Speeding up ***** CurrentSpeed = 30 CurrentSpeed = 50 CurrentSpeed = 70
396 Часть III. Дополнительные конструкции программирования на С# ***** Message From Car Object ***** => Careful buddy! Gonna blow! *********************************** CurrentSpeed = 90 ***** Message From Car Object ***** => Sorry, this car is dead.. . *********************************** Включение группового вызова Вспомните, что делегаты .NET обладают встроенной возможностью группового вызова. Другими словами, объект делегата может поддерживать целый список методов для вызова, а не просто единственный метод. Для добавления нескольких методов к объекту делегата используется перегруженная операция +=, а не прямое присваивание. Чтобы включить групповой вызов в типе Саг, можно модифицировать метод RegisterWithCarEngineO следующим образом: public class Car { // Добавление поддержки группового вызова. // Обратите внимание на использование операции +=, // а не операции присваивания (=) . public void RegisterWithCarEngine(CarEngineHandler methodToCall) { listOfHandlers += methodToCall; } } После этого простого изменения вызывающий код теперь может регистрировать множественные цели для одного и того же обратного вызова. Здесь второй обработчик выводит входное сообщение в верхнем регистре, просто для примера: class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Delegates as event enablers *****\n"); Car cl = new Car ("SlugBug", 100, 10); // Регистрируем несколько обработчиков событий. cl.RegisterWithCarEngine (new Car.CarEngineHandler(OnCarEngineEvent)); cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent2)); // Ускорим (это инициирует события). Console.WriteLine ("***** Speeding up *****"); for (int i = 0; i < 6; i++) cl.AccelerateB0); Console.ReadLine() ; } // Теперь есть ДВА метода, которые будут вызваны Саг // при отправке уведомлений. public static void OnCarEngineEvent(string msg) { Console.WriteLine ("\n***** Message From Car Object *****"); Console.WriteLine("=> {0}", msg); Console. WriteLine (••******************* ****************\n") . }
Глава 11. Делегаты, события и лямбда-выражения 397 public static void OnCarEngineEvent2(string msg) { Console.WriteLine("=> {0}", msg.ToUpper ()); } } В терминах кода CIL операция += разрешает вызывать статический метод Delegate. Combine () (фактически, его можно вызывать и напрямую, но операция += предлагает более простую альтернативу). Ниже показан CIL-код метода RegisterWithCarEngine(). .method public hidebysig instance void RegisterWithCarEngine() (class CarDelegate.Car/AboutToBlow clientMethod) cil managed { // Code size 25 @x19) .maxstack 8 IL_0000: nop IL_0001: ldarg.O IL_0002: dup IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate. Car:rlistOfHandlers IL_0008: ldarg.l IL0009:call class [mscorlib]System.Delegate [mscorlib] System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) IL_000e: castclass CarDelegate.Car/CarEngineHandler IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate. Car::listOfHandlers IL_0018: ret }// end of method Car::RegisterWithCarEngine Удаление целей из списка вызовов делегата В классе Delegate также определен метод Remove(), позволяющий вызывающему коду динамически удалять отдельные члены из списка вызовов объекта делегата. Это позволяет вызывающему коду легко "отписываться" от определенного уведомления во время выполнения. Хотя в коде можно непосредственно вызывать Delegate.Remove(), разработчики С# могут использовать также перегруженную операцию -= в качестве сокращения. Давайте добавим в класс Саг новый метод, который позволяет вызывающему коду исключать метод из списка вызовов: public class Car public void UnRegisterWithCarEngine(CarEngineHandler methodToCall) { listOfHandlers -= methodToCall; } } Опять-таки, синтаксис -- представляет собой просто сокращенную нотацию для ручного вызова метода Delegate.Remove(), что иллюстрирует следующий код CLI для метода UnRegisterWithCarEventO класса Саг: .method public hidebysig instance void UnRegisterWithCarEvent (class CarDelegate.Car/AboutToBlow clientMethod) cil managed { // Code size 25 @x19) .maxstack 8 IL_0000: nop
398 Часть III. Дополнительные конструкции программирования на С# IL_0001: ldarg.O IL_0002: dap IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car:rlistOfHandlers IL_0008: ldarg.l IL_0009: call class [mscorlib]System.Delegate [mscorlib] System. Delegate : .-Remove (class [mscorlib] System. Delegate, class [mscorlib]System.Delegate) IL_000e: castclass CarDelegate.Car/CarEngineHandler IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car:rlistOfHandlers IL_0018: ret } // end of method Car::UnRegisterWithCarEngine В текущей версии класса Car прекратить получение уведомлений от второго обработчика можно за счет изменения метода Main() следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Delegates as event enablers *****\n"); // Сначала создадим объект Car. Car cl = new Car("SlugBug", 100, 10); cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent)); // Сохраним объект делегата для последующей отмены регистрации. Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineEvent2); cl.RegisterWithCarEngine(handler2); // Ускорим (это инициирует события). Console.WriteLine ("***** Speeding up *****"); for (int i = 0; l < 6; i + +) cl.AccelerateB0); // Отменим регистрацию второго обработчика, cl.UnRegisterWithCarEngine(handler2) ; // Сообщения в верхнем регистре больше не выводятся. Console.WriteLine ("***** Speeding up *****"); for (int i = 0; l < 6; i + + ) cl.AccelerateB0); Console.ReadLine(); } Одно отличие Main() состоит в том, что на этот раз создается объект Саг. CarEngineHandler, который сохраняется в локальной переменной, чтобы иметь возможность позднее отменить подписку на получение уведомлений. Тогда при следующем ускорении Саг уже больше не будет выводиться версия входящего сообщения в верхнем регистре, поскольку эта цель исключена из списка вызовов делегата. Исходный код. Проект CarDelegate доступен в подкаталоге Chapter 11. Синтаксис групповых преобразований методов В предыдущем примере CarDelegate явно создавались экземпляры объекта делегата Car.CarEngineHandler, чтобы регистрировать и отменять регистрацию на получение уведомлений: static void Main(string[] args) { Console.WriteLine ("***** Delegates as event enablers *****\n"); Car cl = new Car("SlugBug", 100, 10); cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
Глава 11. Делегаты, события и лямбда-выражения 399 Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineEvent2); cl.RegisterWithCarEngine(handler2); } Если нужно вызывать любые унаследованные члены MulticastDelegate или Delegate, то наиболее простым способом сделать это будет ручное создание переменной делегата. Однако в большинстве случаев обращаться к внутренностям объекта делегата не понадобится. Объект делегата будет нужен только для того, чтобы передать имя метода как параметр конструктора. Для простоты в С# предлагается сокращение, которое называется групповое преобразование методов (method group conversion). Это средство позволяет указывать прямое имя метода, а не объект делегата, когда вызываются методы, принимающие делегаты в качестве аргументов. На заметку! Как будет показано далее в этой главе, синтаксис группового преобразования методов можно также использовать для упрощения регистрации событий С#. Для целей иллюстрации создадим новое консольное приложение по имени CarDelegateMethodGroupConversion и добавим в него файл, содержащий класс Саг, который был определен в проекте CarDelegate. В показанном ниже коде класса Program используется групповое преобразование методов для регистрации и отмены регистрации подписки на уведомления. class Program { static void Main (string [ ] args) { Console.WriteLine (»***** Method Group Conversion *****\n"); Car cl = new Car () ; // Зарегистрировать простое имя метода. cl.RegisterWithCarEngine(CallMeHere); Console.WriteLine("***** Speeding up *****"); for (int l = 0; l < 6; i++) cl.AccelerateB0); // Отменить регистрацию простого имени метода. cl.UnRegisterWithCarEngine(CallMeHere); // Уведомления больше не поступают! for (int 1 = 0; i < 6; i + +) cl.AccelerateB0); Console.ReadLine() ; } static void CallMeHere(string msg) { Console.WriteLine("=> Message from Car: {0}", msg); } } Обратите внимание, что мы не создаем непосредственно объект делегата, а просто указываем метод, который соответствует ожидаемой сигнатуре делегата (в данном случае — метод, возвращающий void и принимающий единственную строку). Имейте в виду, что компилятор С# по-прежнему обеспечивает безопасность типов. Поэтому, если метод CallMeHere() не принимает string и не возвращает void, компилятор сообщит об ошибке.
400 Часть III. Дополнительные конструкции программирования на С# Исходный код. Проект CarDelegateMethodGroupConversion доступен в подкаталоге Chapter 11. Понятие ковариантности делегатов Как вы могли заметить, каждый из делегатов, созданных до сих пор, указывал на методы, возвращающие простые числовые типы данных (или void). Однако предположим, что имеется новое консольное приложение по имени DelegateCovariance, определяющее тип делегата, который может указывать на методы, возвращающие объект пользовательского класса (не забудьте включить в новый проект определение класса Саг): // Определение типа делегата, указывающего на методы, которые возвращают объект Саг. public delegate Car ObtainCarDelegate (); Разумеется, цель для делегата можно определить следующим образом: namespace DelegateCovariance { class Program { // Определение типа делегата, указывающего на методы, // которые возвращают объект Саг. public delegate Car ObtainCarDelegate(); static void Main(string[] args) { Console.WriteLine ("***** Delegate Covariance *****\n"); ObtainCarDelegate targetA = new ObtainCarDelegate (GetBasicCar); Car с = targetA() ; Console.WriteLine("Obtained a {0}", c); Console.ReadLine() ; } public static Car GetBasicCar() { return new Car("Zippy", 100, 55); } } } А теперь пусть необходимо унаследовать новый класс от типа Саг по имени SportsCar и создать тип делегата, который может указывать на методы, возвращающие этот тип класса. До появления .NET 2.0 для этого пришлось бы определять полностью новый делегат, учитывая то, что делегаты настолько безопасны к типам, что не подчиняются базовым законам наследования: // Определение нового типа делегата, указывающего на методы, // которые возвращают объект SportsCar. public delegate SportsCar ObtainSportsCarDelegate(); Поскольку теперь есть два типа делегатов, следует создать экземпляр каждого из них, чтобы получать типы Саг и SportsCar: class Program { public delegate Car ObtainCarDelegate(); public delegate SportsCar ObtainSportsCarDelegate(); public static Car GetBasicCar() { return new Car(); } public static SportsCar GetSportsCar() { return new SportsCar() ; }
Глава 11. Делегаты, события и лямбда-выражения 401 static void Main(string [ ] args) { Console.WriteLine ("***** Delegate Covariance *****\n"); ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar); Car с = targetA () ; Console.WriteLine("Obtained a {0}", c) ; ObtainSportsCarDelegate targetB = new ObtainSportsCarDelegate(GetSportsCar); SportsCar sc = targetB (); Console.WriteLine("Obtained a {0}", sc); Console.ReadLine() ; } } Учитывая законы классического наследования, было бы идеально построить один тип делегата, который мог бы указывать на методы, возвращающие объекты Саг и SportsCar (в конце концов, SportsCar "является" Саг). Ковариантность (которую также называют свободными делегатами (relaxed delegates)) делает это вполне возможным. По сути, ковариантность позволяет построить единственный делегат, который может указывать на методы, возвращающие связанные классическим наследованием типы классов. На заметку! С другой стороны, контравариантность позволяет создать единственный делегат, который может указывать на многочисленные методы, принимающие объекты, связанные классическим наследованием. Дополнительные сведения ищите в документации .NET Framework 4.0 SDK. class Program { // Определение единственного типа делегата, указывающего на методы, // которые возвращают объект Oar или SportsCar. public delegate Car ObtainVehicleDelegate(); public static Car GetBasicCar() { return new Car(); } public static SportsCar GetSportsCar () { return new SportsCar (); } static void Main(string[] args) { Console.WriteLine ("***** Delegate Covariance *****\n"); ObtainVehicleDelegate targetA = new ObtainVehicleDelegate(GetBasicCar); Car с = targetA () ; Console.WriteLine("Obtained a {0}", c) ; // Ковариантность позволяет такое присваивание цели. ObtainVehicleDelegate targetB = new ObtainVehicleDelegate(GetSportsCar) ; SportsCar sc = (SportsCar)targetB (); Console.WriteLine("Obtained a {0}", sc) ; Console.ReadLine(); } } Обратите внимание, что тип делегата ObtainVehicleDelegate определен так, чтобы указывать на методы, возвращающие только строго типизированные объекты типа Саг. Однако с помощью ковариантности можно указывать на методы, которые также возвращают производные типы. Для получения доступа к членам производного типа просто выполните явное приведение. Исходный код. Проект DelegateCovariance доступен в подкаталоге Chapter 11.
402 Часть III. Дополнительные конструкции программирования на С# Понятие обобщенных делегатов Вспомните из предыдущей главы, что язык С# позволяет определять обобщенные типы делегатов. Например, предположим, что необходимо определить делегат, который может вызывать любой метод, возвращающий void и принимающий единственный параметр. Если передаваемый аргумент может изменяться, это моделируется через параметр типа. Для иллюстрации рассмотрим следующий код нового консольного приложения по имени GenericDelegate: namespace GenericDelegate { // Этот обобщенный делегат может вызывать любой метод, который // возвращает void и принимает единственный параметр типа. public delegate void MyGenericDelegate<T> (T arg); class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Generic Delegates *****\n"); // Регистрация целей. MyGenericDelegate<string> strTarget = new MyGenericDelegate<string>(StringTarget); strTarget("Some string data"); MyGenericDelegate<int> intTarget = new MyGenericDelegate<int>(IntTarget); intTarget (9); Console.ReadLine(); static void StringTarget(string arg) Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper ()); static void IntTarget (int arg) Console.WriteLine("++arg is: {0}", ++arg); } } Обратите внимание, что в MyGenericDelegate<T> определен единственный параметр, представляющий аргумент для передачи цели делегата. При создании экземпляра этого типа необходимо указать значение параметра типа вместе с именем метода, который может вызывать делегат. Таким образом, если указать тип string, целевому методу будет отправлено строковое значение: // Создать экземпляр MyGenericDelegate<T> // со string в качестве параметра типа. MyGenericDelegate<string> strTarget = new MyGenericDelegate<string> (StringTarget); strTarget("Some string data"); Имея формат объекта strTarget, метод StringTarget теперь должен принимать в качестве параметра единственную строку: static void StringTarget(string arg) { Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper ()); }
Глава 11. Делегаты, события и лямбда-выражения 403 Эмуляция обобщенных делегатов без обобщений Обобщенные делегаты предоставляют более гибкий способ спецификации метода, подлежащего вызову в безопасной к типам манере. До появления обобщений (в .NET 2.0) тога же конечного результата можно было достичь с использованием параметра System.Object: public delegate void MyDelegate(object arg); Хотя это позволяет посылать любой аргумент цели делегата, это не обеспечивает безопасность типов и не избавляет от бремени упаковки/распаковки. Для примера предположим, что созданы два экземпляра MyDelegate, и оба они указывают на один и тот же метод MyTarget. Обратите внимание на упаковку/распаковку и отсутствие безопасности типов. class Program { static void Main(string[] args) { // Регистрация с "традиционным" синтаксисом делегатов. MyDelegate d = new MyDelegate(MyTarget); d("More string data"); // Синтаксис групповых преобразований методов. MyDelegate d2 = MyTarget; d2 (9); // Дополнительные издержки на упаковку. Console.ReadLine (); } // Из-за отсутствия безопасности типов необходимо // определить лежащий в основе тип перед приведением. static void MyTarget(object arg) { if(arg is int) { int i = (int)arg; // Дополнительные издержки на распаковку. Console.WriteLine("++arg is: {0}", ++i) ; } if(arg is string) { string s = (string)arg; Console.WriteLine("arg in uppercase is: {0}", s.ToUpper()); } } } Когда целевому методу посылается тип значения, это значение упаковывается и снова распаковывается при получении методом. Также, учитывая, что входящий параметр может быть чем угодно, перед приведением должна производиться динамическая проверка лежащего в основе типа. С помощью обобщенных делегатов необходимую гибкость можно получить без упомянутых проблем. Исходный код. Проект GenericDelegate доступен в подкаталоге Chapter 11. На этом первоначальный экскурс в тип делегата .NET завершен. Мы еще вернемся к некоторым дополнительным деталям работы с делегатами в конце этой главы и еще раз — в главе 19, когда будем рассматриваться многопоточность. А теперь переходим к связанной теме — ключевому слову event в С#.
404 Часть III. Дополнительные конструкции программирования на С# Понятие событий С# Делегаты — весьма интересные конструкции в том смысле, что позволяют объектам, находящимся в памяти, участвовать в двустороннем общении. Однако работа с делегатами напрямую может порождать довольно однообразный код (определение делегата, определение необходимых переменных-членов и создание специальных методов регистрации и отмены регистрации для предохранения инкапсуляции). Более того, если делегаты используются в качестве механизма обратного вызова в приложениях напрямую, существует еще одна проблема: если не определить делегат — переменную-член класса как приватную, то вызывающий код получит прямой доступ к объектам делегатов. В этом случае вызывающий код может присвоить переменной новый объект-делегат (фактически удалив текущий список функций, подлежащих вызову), и что еще хуже — вызывающий код сможет напрямую обращаться к списку вызовов делегата. Чтобы проиллюстрировать проблему, рассмотрим следующую переделанную (и упрощенную) версию предыдущего примера CarDelegate: public class Car { public delegate void CarEngineHandler(string msgForCaller); // Теперь этот член public! public CarEngineHandler listOfHandlers; // Просто вызвать уведомление Exploded. public void Accelerate (int delta) { if (listOfHandlers '= null) listOfHandlers("Sorry, this car is dead..."); } } Обратите внимание, что теперь нет делегата — приватной переменной-члена, инкапсулированной с помощью специальных методов регистрации. Поскольку эти члены сделаны общедоступными, вызывающий код может непосредственно обращаться к члену listOfHandlers и переназначить этот тип на новые объекты CarEngineHandler, после чего вызывать делегат, когда вздумается: class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Agh' No Encapsulation' *****\n"); // Создать Car. Car myCar = new Car(); // Есть прямой доступ к делегату! myCar.listOfHandlers = new Car.CarEngineHandler(CallWhenExploded); myCar.Accelerate A0); // Назначаем ему совершенно новый объект... //В лучшем случае получается путаница. myCar.listOfHandlers = new Car.CarEngineHandler(CallHereToo); myCar.Accelerate A0); // Вызывающий код может также напрямую вызвать делегат! myCar.listOfHandlers.Invoke ("nee, nee, nee..."); Console.ReadLine(); } static void CallWhenExploded(string msg) { Console.WriteLine(msg); } static void CallHereToo(string msg) { Console.WriteLine(msg); } }
Глава 11. Делегаты, события и лямбда-выражения 405 Общедоступные члены-делегаты нарушают инкапсуляцию, что не только затруднит сопровождение кода (и отладку), но также сделает приложение уязвимым в смысле безопасности. Ниже показан вывод текущего примера: ***** Agh I No Encapsulation1 ***** Sorry, this car is dead. . . Sorry, this car is dead. . . nee, hee, hee . . . Очевидно, что вряд ли имеет смысл предоставлять другим приложениям право изменять то, на что указывает делегат, или вызывать его члены напрямую. Исходный код. Проект PublicDelegateProblem доступен в подкаталоге Chapter 11. Ключевое слово event В качестве сокращения, избавляющего от необходимости строить специальные методы для добавления и удаления методов в списке вызовов делегата, в С# предусмотрено ключевое слово event. Обработка компилятором ключевого слова event приводит к автоматическому получению методов регистрации и отмены регистрации наряду со всеми необходимыми переменными-членами для типов делегатов. Эта переменная- член делегата всегда объявляется приватной, и потому не доступна напрямую объекту, инициировавшему событие. Точнее говоря, ключевое слово event — это не более чем синтаксическое украшение, позволяющее экономить на наборе кода. Определение события представляет собой двухэтапный процесс. Во-первых, нужно определить делегат, который будет хранить список методов, подлежащих вызову при возникновении события. Во-вторых, необходимо объявить событие (используя ключевое слово event) в терминах связанного типа делегата. Чтобы проиллюстрировать ключевое слово event, создадим новое консольное приложение по имени С a rE vents. В классе Саг будут определены два события под названиями AboutToBlow и Exploded. Эти события ассоциированы с единственным типом делегата по имени CarEngineHandler. Ниже показаны начальные изменения в классе Саг: public class Car { // Этот делегат работает в сочетании с событиями Саг. public delegate void CarEngineHandler(string msg) ; // Car может посылать следующие события. public event CarEngineHandler Exploded; public event CarEngineHandler AboutToBlow; } Отправка события вызывающему коду состоит просто в указании имени события вместе со всеми необходимыми параметрами, определенными в ассоциированном делегате. Чтобы удостовериться, что вызывающий код действительно зарегистрировал событие, его следует проверить на равенство null перед вызовом набора методов делегата. Ниже приведена новая версия метода Accelerate () класса Саг: public void Accelerate(int delta) { // Если автомобиль сломан, инициировать событие Exploded. if (carlsDead) { if (Exploded '= null)
406 Часть III. Дополнительные конструкции программирования на С# Exploded("Sorry, this car is dead..."); } else { CurrentSpeed += delta; // Почти сломан? if A0 == MaxSpeed - CurrentSpeed && AboutToBlow != null) { AboutToBlow("Careful buddy! Gonna blow!"); } // Все в порядке! if (CurrentSpeed >= maxSpeed) carlsDead = true; else Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed); } } Таким образом, объект Car сконфигурирован для отправки двух специальных событий, без необходимости определения специальных функций регистрации или объявления переменных-членов. Чуть ниже будет продемонстрировано использование этого нового объекта, но сначала давайте рассмотрим архитектуру событий немного подробнее. "За кулисами" событий Событие С# в действительности развертывается в два скрытых метода, один из которых имеет префикс add_, а другой — remove. За этим префиксом следует имя события С#. Например, событие Exploded превращается в два скрытых метода CIL с именами add_Exploded () и remove_Exploded (). Если заглянуть в CIL-код метода add_AboutToBlow (), можно обнаружить там вызов метода Delegate.Combine(). Ниже показан частичный код CIL: .method public hidebysig specialname instance void add_AboutToBlow(class CarEvents.Car/CarEngineHandler 'value1) cil managed synchronized { call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine( class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) } Как и следовало ожидать, remove_AboutToBlow() неявно вызывает Delegate. Remove (): .method public hidebysig specialname instance void remove_AboutToBlow(class CarEvents.Car/CarEngineHandler 'value1) cil managed synchronized { call class [mscorlib]System.Delegate [mscorlib] System. Delegate: : Remove ( class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
Глава 11. Делегаты, события и лямбда-выражения 407 И, наконец, в CIL-коде, представляющем само событие, используются директивы .addon и .removeon для отображения имен корректных методов addXXX () и remove_XXX () для вызова: .event CarEvents.Car/EngineHandler AboutToBlow { .addon void CarEvents.Car::add_AboutToBlow (class CarEvents.Car/CarEngineHandler) .removeon void CarEvents.Car::remove_AboutToBlow (class CarEvents.Car/CarEngineHandler) } Теперь, когда вы разобрались, как строить класс, способный отправлять события С# (и уже знаете, что события — это лишь способ сэкономить время на наборе кода), следующий большой вопрос связан с организацией прослушивания входящих событий на стороне вызывающего кода. Прослушивание входящих событий События С# также упрощают акт регистрации обработчиков событий на стороне вызывающего кода. Вместо того чтобы специфицировать специальные вспомогательные методы, вызывающий код просто использует операции += и -= непосредственно (что приводит к внутренним вызовам методов addXXX () или removeXXX ()). Для регистрации события руководствуйтесь следующим шаблоном: // ИмяОбъекта.ИмяСобытия += new СвязанныйДелегат(функцияДляВыэова); // Car.EngineHandler d = new Car.CarEngineHandler(CarExplodedEventHandler) myCar.Exploded += d; Для отключения от источника событий служит операция -= в соответствии со следующим шаблоном: // ИмяОбъекта.ИмяСобытия -= new Свяэ анныйДе легат (функцияДляВыэова) ; // myCar.Exploded -= d; Следуя этому очень простому шаблону, переделаем метод Main(), применив на этот раз синтаксис регистрации методов С#: class Program { static void Main(string [ ] args) { Console.WriteLine("***** Fun with Events *****\n"); Car cl = new Car ("SlugBug", 100, 10); // Зарегистрировать обработчики событий. cl.AboutToBlow += new Car.CarEngineHandler(CarlsAlmostDoomed); cl.AboutToBlow += new Car.CarEngineHandler(CarAboutToBlow); Car.CarEngineHandler d = new Car.CarEngineHandler(CarExploded); cl.Exploded += d; Console.WriteLine ("***** Speeding up *****"); for (int i = 0; i < 6; i++) cl.AccelerateB0); // Удалить метод CarExploded из списка вызовов. cl.Exploded -= d; Console.WriteLine("\n***** Speeding up *****"); for (int i = 0; i < 6; i++) cl.AccelerateB0); Console.ReadLine() ; }
408 Часть III. Дополнительные конструкции программирования на С# public static void CarAboutToBlow(string msg) { Console.WriteLine(msg); } public static void CarlsAlmostDoomed(string msg) { Console.WriteLine("=> Critical Message from Car: {0}", msg); } public static void CarExploded(string msg) { Console.WriteLine(msg); } } Чтобы еще более упростить регистрацию событий, можно применить групповое преобразование методов. Ниже показана очередная модификация Main(). static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Events *****\n"); Car cl = new Car ("SlugBug", 100, 10); // Регистрация обработчиков событий. cl.AboutToBlow += CarlsAlmostDoomed; cl.AboutToBlow += CarAboutToBlow; cl.Exploded += CarExploded; Console.WriteLine ("***** Speeding up *****"); for (int i = 0; l < 6; i++) cl.AccelerateB0); cl.Exploded -= CarExploded;1 Console.WriteLine("\n***** Speeding up *****"); for (int i = 0; l < 6; i++) cl.AccelerateB0); Console.ReadLine() ; } Упрощенная регистрация событий с использованием Visual Studio 2010 Среда Visual Studio 2010 предоставляет помощь в процессе регистрации обработчиков событий. В случае применения синтаксиса += во время регистрации событий открывается окно IntelliSense, приглашающее нажать клавишу <ТаЬ> для автоматического заполнения экземпляра делегата (рис. 11.2). Program.cs* ^fCarEvents.Program vHooklntoEventiQ public static void HookIntoEvents() { Car newCar » new Car(); newCar.AboutToBlow +- } I new Car.CarEngineHandler(newCar_AboutToBlow); (Press TAB to insert) | 100% -j < Рис. 11.2. Выбор делегата с помощью IntelliSense После нажатия клавиши <ТаЬ> появляется возможность ввести имя обработчика событий, который нужно сгенерировать (или просто принять имя по умолчанию), как показано на рис. 11.3. Снова нажав <ТаЬ>, вы получите заготовку кода цели делегата в корректном формате (обратите внимание, что этот метод объявлен статическим, потому что событие было зарегистрировано внутри статического метода MainO): static void newCar_AboutToBlow(string msg) { // Add your code '
Глава 11. Делегаты, события и лямбда-выражения 409 Щ1 Program.cs* X Qyj ^CarEvents-Program »J ^HooldntoEventsQ * 1 ш"^ public static void HookIntoEvents() * \ {.. Car nevrCar » new Car()j newCar.AboutToBlow +-new Car/tarEngineH.andler(MS^rJ^ST^^Ej)J Щ } i^ 1 Press TAB to generate handler 'newCar_At>outToBlow' in this class i 'J } ■ "" ■""■' * *" "*■ " u " ' '■' 'J' u u Рис. 11.3. Формат цели делегата IntelliSense Средство IntelliSense доступно для всех событий .NET из библиотек базовых классов. Это средство интегрированной среды разработки замечательно экономит время, но не избавляет от необходимости поиска в справочной системе .NET правильного делегата для использования с определенным событием, а также формата целевого метода делегата. Исходный код. Проект CarEvents доступен в подкаталоге Chapter 11. Создание специальных аргументов событий По правде говоря, есть еще одно последнее усовершенствование, которое можно внести в текущую итерацию класса Саг и которое отражает рекомендованный Microsoft шаблон событий. Если вы начнете исследовать события, посылаемые определенным типом из библиотек базовых классов, то обнаружите, что первым параметром лежащего в основе делегата будет System.Object, в то время как вторым параметром — тип, являющийся потомком System.EventArgs. Аргумент System.Object представляет ссылку на объект, который отправляет событие (такой как Саг), а второй параметр — информацию, относящуюся к обрабатываемому событию. Базовый класс System. Event Args представляет событие, которое не посылает никакой специальной информации: public class EventArgs public static readonly System.EventArgs Empty; public EventArgs(); } Для простых событий можно передать экземпляр EventArgs непосредственно. Однако чтобы передавать какие-то специальные данные, потребуется построить подходящий класс, унаследованный от EventArgs. Для примера предположим, что есть класс по имени CarEventArgs, поддерживающий строковое представление сообщения, отправленного получателю: public class CarEventArgs : EventArgs { public readonly string msg; public CarEventArgs(string message) { msg = message; } } Теперь понадобится модифицировать делегат CarEngineHandler, как показано ниже (само событие не изменяется):
410 Часть III. Дополнительные конструкции программирования на С# public class Car { public delegate void CarEngineHandler(object sender, CarEventArgs e) ; } Здесь при инициализации события из метода Accelerate() нужно использовать ссылку на текущий Саг (через ключевое слово this) и экземпляр типа CarEventArgs. Например, рассмотрим следующее обновление: public void Accelerate (int delta) { // Если этот автомобиль сломан, инициировать событие Exploded. if (carlsDead) { if (Exploded != null) Exploded(this, new CarEventArgs("Sorry, this car is dead...")); } } Все, что потребуется сделать на вызывающей стороне — это обновить обработчики событий для получения входных параметров и получения сообщения через поле, доступное только для чтения. Например: public static void CarAboutToBlow(object sender, CarEventArgs e) { Console.WriteLine ("{0 } says: {1}", sender, o.msg); } Если получатель желает взаимодействовать с объектом, отправившим событие, можно выполнить явное приведение System.Object. С помощью такой ссылки можно вызвать любой общедоступный метод объекта, который отправил событие: public static void CarAboutToBlow (object sender, CarEventArgs e) { // Чтобы подстраховаться, произведем проверку во время выполнения перед приведением. if (sender is Car) { Car с = (Car)sender; Console.WriteLine("Critical Message from {0}: {1}", c.PetName, e.msg); } } Исходный код. Проект PrimAndProperCarEvents доступен в подкаталоге Chapter 11. Обобщенный делегат EventHandler<T> Учитывая, что очень много специальных делегатов принимают объект в первом параметре и наследников EventArgs — во втором, можно еще более упростить предыдущий пример, используя обобщенный тип EventHandler<T>, где Т — специальный тип-наследник EventArgs. Рассмотрим следующую модификацию типа Саг (обратите внимание, что строить специальный делегат больше не нужно): public class Car { public event EventHandler<CarEventArgs> Exploded; public event EventHandler<CarEventArgs> AboutToBlow; }
Глава 11. Делегаты, события и лямбда-выражения 411 Метод Main() может затем использовать EventHandler<CarEventArgs> везде, где ранее указывался CarEngineHandler: static void Main(string[] args) { Console.WriteLine("***** Prim and Proper Events *****\n"); // Создать Car обычным образом. Car cl = new Car ("SlugBug", 100, 10); // Зарегистрировать обработчики событий. cl.AboutToBlow += CarlsAlmostDoomed; cl.AboutToBlow += CarAboutToBlow; EventHandler<CarEventArgs> d = new EventHandler<CarEventArgs>(CarExploded); cl.Exploded += d; } Итак, вы ознакомились с основными аспектами работы с делегатами и событиями на языке С#. Хотя этого вполне достаточно для решения практически любых задач, связанных с обратными вызовами, в завершение главы рассмотрим ряд финальных упрощений, а именно — анонимные методы и лямбда-выражения. Исходный код. Проект PrimAndProperCarEvents (Generic) доступен в подкаталоге Chapter 11. Понятие анонимных методов С# Как было показано выше, когда вызывающий код желает прослушивать входящие события, он должен определить специальный метод в классе (или структуре), соответствующий сигнатуре ассоциированного делегата. Ниже приведен пример: class Program { static void Main (string[] args) { SomeType t = new SomeTypeO ; // Предположим, что SomeDeletage может указывать на методы, // которые не принимают аргументов и возвращают void. t.SomeEvent += new SomeDelegate(MyEventHandler); } // Обычно вызывается только объектом SomeDelegate. public static void MyEventHandler() { // Что-то делать по возникновении события. } } Однако если подумать, то такие методы, как MyEventHandler (), редко предназначены для обращения из любой другой части программы помимо делегата. Если речь идет о производительности, несложно вручную определить отдельный метод для вызова объектом делегата. Чтобы справиться с этим, можно ассоциировать событие непосредственно с блоком операторов кода во время регистрации события. Формально такой код называется анонимным методом. Для иллюстрации синтаксиса напишем метод Main(), который обрабатывает события, посланные из типа Саг, с использованием анонимных методов вместо специальных именованных обработчиков событий:
412 Часть III. Дополнительные конструкции программирования на С# class Program { static void Main(string[] args) { Console.WriteLine ("***** Anonymous Methods *****\n"); Car cl = new Car ("SlugBug", 100, 10); // Зарегистрировать обработчики событий в виде анонимных методов. cl.AboutToBlow += delegate { Console.WriteLine("Eek! Going too fast!"); }; cl .AboutToBlow += delegate(object sender, CarEventArgs e) { Console.WriteLine("Message from Car: {0}", e.msg); }; cl.Exploded += delegate(object sender, CarEventArgs e) { Console.WriteLine("Fatal Message from Car: {0}", e.msg); }; // Это в конечном итоге инициирует события, for (int i = 0; i < 6; i + + ) cl.Accelerate B0); Console.ReadLine(); } } На заметку! Последняя фигурная скобка анонимного метода должна завершаться точкой с запятой. Если забыть об этом, во время компиляции возникнет ошибка. Обратите внимание, что в классе Program теперь не определяются специальные статические обработчики событий вроде CarAboutToBlowO или CarExploded(). Вместо этого с помощью синтаксиса += определяются встроенные неименованные (анонимные) методы, к которым вызывающий код будет обращаться во время обработки события. Базовый синтаксис анонимного метода соответствует следующему псевдокоду: class Program { static void Main(string [ ] args) { SomeType t = new SomeTypeO ; t.SomeEvent += delegate (optionallySpecifledDelegateArgs) { /* операторы */ }; } } Обратите внимание, что при обработке первого события AboutToBlow внутри предыдущего метода Main() аргументы, передаваемые из делегата, не указываются: cl .AboutToBlow += delegate { Console.WriteLine ("Eek! Going too fast!"); }; Строго говоря, вы не обязаны принимать входные аргументы, посланные определенным событием. Однако если планируется использовать эти входные аргументы, нужно указать параметры, прототипированные типом делегата (как показано во второй обработке событий AboutToBlow и Exploded). Например: cl.AboutToBlow += delegate(object sender, CarEventArgs e) { Console.WriteLine("Critical Message from Car: {0}", e.msg); };
Глава 11. Делегаты, события и лямбда-выражения 413 Доступ к локальным переменным Анонимные методы интересны тем, что могут обращаться к локальным переменным метода, в котором они определены. Формально такие переменные называются внешними (outer) переменными анонимного метода. Ниже отмечены некоторые важные моменты, касающиеся взаимодействия между контекстом анонимного метода и контекстом определяющего их метода. • Анонимный метод не имеет доступа к параметрам ref и out определяющего их метода. • Анонимный метод не может иметь локальных переменных, имена которых совпадают с именами локальных переменных объемлющего метода. • Анонимный метод может обращаться к переменным экземпляра (или статическим переменным) из контекста объемлющего класса. • Анонимный метод может объявлять локальные переменные с теми же именами, что и у членов объемлющего класса (локальные переменные имеют отдельный контекст и скрывают внешние переменные-члены). Предположим, что метод Main() определяет локальную переменную по имени aboutToBlowCounter типа int. Внутри анонимных методов, обрабатывающих событие AboutToBlow, мы увеличим значение этого счетчика на 1 и выведем результат на консоль перед завершением Main(): static void Main(string[] args) { Console.WriteLine ("***** Anonymous Methods *****\n"); int aboutToBlowCounter = 0; // Создать Car обычным образом. Car cl = new Car ("SlugBug", 100, 10); // Зарегистрировать обработчики событий в виде анонимных методов. cl.AboutToBlow += delegate { aboutToBlowCounter++; Console.WriteLine("Eek! Going too fast!"); }; cl.AboutToBlow += (object sender, CarEventArgs e) { aboutToBlowCounter++ ; Console.WriteLine("Critical Message from Car: {0}", msg); }; Console.WriteLine("AboutToBlow event was fired {0} times.", aboutToBlowCounter); Console.ReadLine(); } Запустив этот модифицированный метод Main(), вы обнаружите, что финальный вывод Console.WriteLine() сообщит о двукратном вызове AboutToBlow. Исходный код. Проект AnonymousMethods доступен в подкаталоге Chapter 11. Понятие лямбда-выражений Чтобы завершить знакомство с архитектурой событий .NET, рассмотрим лямбда-выражения. Как объяснялось ранее в этой главе, С# поддерживает способность обрабатывать события "встроенным образом", назначая блок операторов кода непосредственно событию с использованием анонимных методов вместо построения отдельного мето-
414 Часть III. Дополнительные конструкции программирования на С# да, подлежащего вызову лежащим в основе делегатом. Лямбда-выражения — это всего лишь лаконичный способ записи анонимных методов, в конечном итоге упрощающий работу с типами делегатов .NET. Чтобы подготовить фундамент для изучения лямбда-выражений, создадим новое консольное приложение по имени SimpleLambdaExpressions. Теперь займемся методом FindAlK) обобщенного типа List<T>. Этот метод может быть вызван, когда нужно извлечь подмножество элементов из коллекции, и он имеет следующий прототип: // Метод класса System.Collections.Generic.List<T>. public List<T> FindAll(Predicate<T> match) Как видите, этот метод возвращает объект List<T>, представляющий подмножество данных. Также обратите внимание, что единственный параметр FindAll () — обобщенный делегат типа System. Predicate<T>. Этот делегат может указывать на любой метод, возвращающий bool и принимающий единственный параметр: // Этот делегат используется методом FindAll () для извлечения подмножества, public delegate bool Predicate<T>(T obj); Когда вызывается FindAll (), каждый элемент в List<T> передается методу, указанному объектом Predicate<T>. Реализация этого метода будет производить некоторые вычисления для проверки соответствия элемента данных указанному критерию, возвращая true или false. Если метод вернет true, то текущий элемент будет добавлен в List<T>, представляющий искомое подмножество. Прежде чем посмотреть, как лямбда-выражения упрощают работу с FindAll (), давайте решим эту задачу в длинной нотации, используя объекты делегатов непосредственно. Добавим метод (по имени TraditionalDelegateSyntaxO) к типу Program, который взаимодействует с System.Predicate<T> для обнаружения четных чисел в списке List<T> целочисленных значений: class Program { static void Main(string [ ] args) { Console.WriteLine("***** Fun with Lambdas *****\n"); TraditionalDelegateSyntaxO ; Console.ReadLine(); } static void TraditionalDelegateSyntaxO { // Создать список целых чисел. List<int> list = new List<int>(); list. AddRange (new int [ ] { 20, 1, 4, 8, 9, 44 }); // Вызов FindAll() с использованием традиционного синтаксиса делегатов. Predicate<int> callback = new Predicate<int>(IsEvenNumber); List<int> evenNumbers = list.FindAll(callback); Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers) { Console.Write("{0}\t", evenNumber); } Console.WriteLine (); } // Цель для делегата PredicateO. static bool IsEvenNumber(int i) { // Это четное число? return (i % 2) == 0; } }
Глава 11. Делегаты, события и лямбда-выражения 415 Здесь имеется метод (IsEvenNumber ()), отвечающий за проверку входного целочисленного параметра на предмет четности или нечетности через операцию С# взятия модуля % (получения остатка от деления). В результате запуска приложения на консоль выводятся числа .20, 4, 8 и 44. Хотя этот традиционный подход к работе с делегатами функционирует ожидаемым образом, метод IsEvenNumber () вызывается только при очень ограниченных условиях; в частности, когда вызывается FindAllO, который взваливает на нас полный багаж определения метода. Если бы вместо этого применялся анонимный метод, код стал бы существенно яснее. Рассмотрим следующий новый метод в классе Program: static void AnonymousMethodSyntax() { // Создать список целых. List<int> list = new List<int>(); list.AddRange(new int [ ] { 20, 1, 4, 8, 9, 44 }); // Теперь использовать анонимный метод. List<int> evenNumbers = list.FindAll(delegate(int i) { return (i^2) ==0; } ) ; // Вывод четных чисел. Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers) { Console.Write ("{0}\t", evenNumber); } Console.WriteLine(); } В этом случае вместо прямого создания типа делегата Predicate<T> с последующим написанием отдельного метода, метод встраивается как анонимный. Хотя это шаг в правильном направлении, мы все еще обязаны использовать ключевое слово delegate (или строго типизированный Predicate<T>), и должны убедиться в соответствии списка параметров. Также, согласитесь, синтаксис, используемый для определения анонимного метода, выглядит несколько тяжеловесно, что особенно проявляется здесь: List<int> evenNumbers = list.FindAll ( delegate(int i) { return (i % 2) == 0; } ); Для дальнейшего упрощения вызова FindAllO можно применять лямбда-выражения. Используя этот новый синтаксис, вообще не приходится иметь дело с лежащим в основе объектом делегата. Рассмотрим следующий новый метод в классе Program: static void LambdaExpressionSyntax() { // Создать список целых. List<int> list = new List<int>(); list.AddRange (new int[] { 20, 1, 4, 8, 9, 44 }); // Теперь используем лямбда-выражение С#. List<int> evenNumbers = list. FindAll (l => (l % 2) == 0) ; // Вывод четных чисел. Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers) { Console.Write("{0}\t", evenNumber); } Console.WriteLine(); }
416 Часть III. Дополнительные конструкции программирования на С# Здесь обратите внимание на довольно странный оператор кода, передаваемый методу FindAll(), который в действительности и является лямбда-выражением. В этой модификации примера вообще нет никаких следов делегата Predicate<T> (как и ключевого слова delegate). Все, что специфицировано вместо них — это лямбда-выражение: i => (i ць 2) == 0. Прежде чем разбирать синтаксис дальше, пока просто усвойте, что лямбда-выражения могут применяться везде, где использовался анонимный метод или строго типизированный делегат (обычно в более лаконичном виде). "За кулисами" компилятор С# транслирует выражение в стандартный анонимный метод, использующий тип делегата Predicate<T> (в чем можно убедиться с помощью утилиты ildasm.exe или reflector.exe). В частности, следующий оператор кода: // Это лямбда-выражение... List<int> evenNumbers = list.FindAll(i => (i Ъ 2) == 0) ; компилируется в примерно такой код С#: // . . .превращается в следующий анонимный метод. List<int> evenNumbers = list.FindAll(delegate (int l) { return (l na 2) == 0; }); Анализ лямбда-выражения Лямбда-выражение начинается со списка параметров, за которым следует лексема => (лексема С# для лямбда-выражения позаимствована из лямбда-вычислений), а за ней — набор операторов (или единственный оператор), который будет обрабатывать параметры. На самом высоком уровне лямбда-выражение можно представить следующим образом: АргументыДляОбработки => ОбрабатывающиеОператоры То, что находится внутри метода LambdaExpressionSyntaxO, следует понимать так: // i — список параметров. // (i % 2) = 0 - набор операторов для обработки i. List<int> evenNumbers = list. FindAll (i => (l nu 2) == 0) ; Параметры лямбда-выражения могут быть типизированы явно или неявно. В настоящий момент тип данных, представляющий параметр i (целое), определяется неявно. Компилятор способен понять, что i — целое, на основе контекста всего лямбда-выражения, поместив тип данных и имя переменной в пару скобок, как показано ниже: // Теперь установим тип параметров явно. List<int> evenNumbers = list.FindAll ( (int i) => (i % 2) == 0) ; Как было показано, если лямбда-выражение имеет одиночный неявно типизированный параметр, то скобки в списке параметров могут быть опущены. При желании быть последовательным в использовании параметров лямбда-выражений, можно всегда заключать список параметров в скобки, чтобы выражение выглядело так: List<int> evenNumbers = list. FindAll ( (i) => (l qd 2) == 0) ; И, наконец, обратите внимание, что сейчас выражение не заключено в скобки (разумеется, вычисление остатка от деления помещается в скобки, чтобы гарантировать его выполнение перед проверкой равенства). Лямбда-выражение с оператором, заключенным в скобки, выглядит следующим образом: // Теперь заключим в скобки и выражение. List<int> evenNumbers = list.FindAll((i) => ((l % 2) == 0));
Глава 11. Делегаты, события и лямбда-выражения 417 Теперь, когда известны разные способы построения лямбда-выражения, как его представить в понятных человеку терминах? Оставив чистую математику в стороне, можно привести следующее объяснение: // Список параметров (в данном случае — единственное целое по имени i) // будет обработан выражением (i % 2) =0. List<int> evenNumbers = list.FindAll( (i) => (A % 2) == 0) ) ; Обработка аргументов внутри множества операторов Наше первое лямбда-выражение состояло из единственного оператора, который в результате вычисления дает булевское значение. Однако, как должно быть хорошо известно, многие цели делегатов должны выполнять множество операторов кода. По этой причине С# позволяет строить лямбда-выражения, состоящие из нескольких блоков операторов. Когда выражение должно обрабатывать параметры в нескольких строках кода, понадобится выделить контекст этих операторов с помощью фигурных скобок. Рассмотрим следующую модификацию метода LambdaExpressionSyntax(): static void LambdaExpressionSyntax() { // Создать список целых. List<int> list = new List<int>(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 }); // Обработать каждый аргумент в группе операторов кода. List<int> evenNumbers = list.FindAll((i) => { Console.WriteLine("value of l is currently: {0}", i); bool lsEven = ((i пь 2) == 0) ; return lsEven; }); // Вывод четных чисел. Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers) { Console.Write ("{0}\t", evenNumber); } Console.WriteLine(); } В этом случае список параметров (опять состоящий из единственного целого i) обрабатывается набором операторов кода. Помимо вызова Console.WriteLineO, оператор вычисления остатка от деления разбит на два оператора для повышения читабельности. Предположим, что каждый из рассмотренных выше методов вызывается в Main(): static void Main(string [ ] args) { Console.WriteLine("***** Fun with Lambdas *****\n"); TraditionalDelegateSyntax (); AnonymousMethodSyntax(); Console.WriteLine(); LambdaExpressionSyntax(); Console.ReadLine(); } Запуск этого приложения даст следующий вывод: ***** Fun with Lambdas ***** Here are your even numbers: 20 4 8 44
418 Часть III. Дополнительные конструкции программирования на С# Here are your even numbers : 20 4 8 44 value of l is currently: 20 value of l is currently: 1 value of l is currently: 4 value of l is currently: 8 value of l is currently: 9 value of l is currently: 44 Here are your even numbers: 20 4 8 44 Исходный код. Проект SimpleLambdaExpressions доступен в подкаталоге Chapter 11. Лямбда-выражения с несколькими параметрами и без параметров Показанные выше лямбда-выражения обрабатывало единственный параметр. Однако это вовсе не обязательно, поскольку лямбда-выражения могут обрабатывать множество аргументов или вообще не иметь аргументов. Для иллюстрации первого сценария создадим консольное приложение по имени LambdaExpressionsMultipleParams со следующей версией класса SimpleMath: public class SimpleMath { public delegate void MathMessage (string msg, int result); private MathMessage mmDelegate; public void SetMathHandler(MathMessage target) {mmDelegate = target; } public void Add (int x, int y) { if (mmDelegate != null) mmDelegate.Invoke("Adding has completed!", x + y) ; } } Обратите внимание, что делегат MathMessage принимает два параметра. Чтобы представить их в виде лямбда-выражения, метод Main О может быть реализован так: static void Main(string [ ] args) { // Регистрация делегата как лямбда-выражения. SimpleMath m = new SimpleMath(); m.SetMathHandler((msg, result) => {Console.WriteLine("Message: {0}, Result: {1}", msg, result);}); // Это приведет к выполнению лямбда-выражения. m.AddA0, 10) ; Console.ReadLine(); } Здесь используется выведение типа компилятором, поскольку для простоты два параметра не типизированы строго. Однако можно было бы вызвать SetMathHandler () следующим образом: m.SetMathHandler((string msg, int result) => {Console.WriteLine("Message: {0}, Result: {1}", msg, result);}); И, наконец, если лямбда-выражение используется для взаимодействия с делегатом, вообще не принимающим параметров, то это можно сделать, указав в качестве параметра пару пустых скобок. Таким образом, предполагая, что определен следующий тип делегата:
Глава 11. Делегаты, события и лямбда-выражения 419 public delegate string VerySimpleDelegate(); вот как можно обработать результат вызова: // Вывод на консоль строки "Enjoy your string!". VerySimpleDelegate d = new VerySimpleDelegate ( () => {return "Enjoy your string!11;} ); Console.WriteLine(d.Invoke() ) ; Исходный код. Проект LambdaExpressionsMultipleParams доступен в подкаталоге Chapter 11. Усовершенствование примера PrimAndProperCarEvents за счет использования лямбда-выражений Учитывая то, что главное предназначение лямбда-выражений состоит в обеспечении возможности в чистой, сжатой манере определить анонимный метод (и тем самым упростить работу с делегатами), давайте переделаем проект PrimAndProperCarEvents, созданный ранее в этой главе. Ниже приведена упрощенная версия класса Program этого проекта, в которой используется синтаксис лямбда-выражений (вместо простых делегатов) для перехвата всех событий, поступающих от объекта Саг. class Program { static void Main(string[] args) { Console.WriteLine("***** More Fun with Lambdas *****\n") ; // Создание объекта Car обычным образом. Car cl = new Car("SlugBug", 100, 10); // Использование лямбда-выражений. cl.AboutToBlow += (sender, e) => { Console.WriteLine(e.msg); }; cl.Exploded += (sender, e) => { Console.WriteLine(e.msg); }; // Ускорим (это инициирует события). Console.WriteLine("\n***** Speeding up *****"); for (int i = 0; i < 6; i++) cl.AccelerateB0); Console.ReadLine(); } } Теперь общая роль лямбда-выражений должна проясниться, и становится понятно, что они обеспечивают "функциональную манеру" работы с анонимными методами и типами делегатов. К новой лямбда-операции (=>) необходимо привыкнуть, однако помните, что любые лямбда-выражения сводятся к следующему простому уравнению: АргументыДляОбработки => ОбрабатывахлциеИхОператоры Исходный код. Проект CarEventsWithLambdas доступен в подкаталоге Chapter 11. Резюме В этой главе вы ознакомились с несколькими способами двустороннего взаимодействия множества объектов. Во-первых, было рассмотрено ключевое слово delegate, используемое для неявного конструирования класса-наследника System.MuIticastDelegate.
420 Часть III. Дополнительные конструкции программирования на С# Как было показано, объект делегата поддерживает список методов для вызова тогда, когда ему об этом укажут. Такие вызовы могут выполняться синхронно (с использованием метода Invoke()) или асинхронно (через методы BeginlnvokeO и EndlnvokeO). Асинхронная природа типов делегатов .NET будет рассмотрена в главе 19. Во-вторых, вы ознакомились с ключевым словом event, которое в сочетании с типом делегата может упростить процесс отправки уведомлений о событиях ожидающим объектам. Как видно в результирующем коде CIL, модель событий .NET отображается на скрытые обращения к типам System.Delegate/System.MulticastDelegate. В этом свете ключевое слово event является необязательным и просто позволяет сэкономить на наборе кода. В главе также рассматривалось средство языка С#, которое называется анонимными методами. С помощью этой синтаксической конструкции можно явно ассоциировать блок операторов кода с заданным событием. Как было показано, анонимные методы могут игнорировать параметры, переданные событием, и имеют доступ к "внешним переменным" определяющего их метода. Вы также ознакомились с упрощенным способом регистрации событий с применением групповых преобразований методов. И, наконец, в завершение главы было дано описание лямбда-операции => в С#. Как было показано, этот синтаксис значительно сокращает нотацию написания анонимных методов, когда набор аргументов может быть передан на обработку группе операторов.
ГЛАВА 12 Расширенные средства языка С# В этой главе рассматриваются некоторые более сложные синтаксические конструкции языка программирования С#. Сначала будет показано, как реализуется и используется метод-индексатор. Этот механизм С# позволяет строить специальные типы, обеспечивающие доступ к внутренним подтипам с применением синтаксиса, похожего на синтаксис массивов. Затем вы узнаете о том, как перегружать различные операции (+, -, <, > и т.д.) и как создавать специальные процедуры явного и неявного преобразования типов (а также причины, по которым это может понадобиться). Далее рассматриваются три темы, которые особенно полезны при работе с API- интерфейсами LINQ (хотя это применимо и вне контекста LINQ), а именно: расширяющие методы, частичные методы и анонимные типы. И в завершение вы узнаете, как создавать контекст "небезопасного" кода, чтобы напрямую манипулировать неуправляемыми указателями. Хотя использовать указатели в приложениях С# приходится исключительно редко, понимание того, как это делается, может пригодиться в определенных ситуациях со сложными сценариями взаимодействия. Понятие методов-индексаторов Программисты хорошо знакомы с процессом доступа к индивидуальным элементам, содержащимся в стандартных массивах, через операцию индекса ([ ]). Например: static void Main(string[] args) { // Цикл по аргументам командной строки с использованием операции индекса. for(int i = 0; i < args.Length; i++) Console.WriteLine("Args: {0}", args[i]); // Объявление массива локальных целых. int[] mylnts = { 10, 9, 100, 432, 9874}; // Использование операции индекса для доступа к элементам. for(int D = 0; ] < mylnts.Length; j++) Console.WriteLine("Index {0} = {1} ", j, mylnts[j]); Console.ReadLine(); } Приведенный код не должен быть для вас чем-то новым. В С# имеется возможность проектировать специальные классы и структуры, которые могут быть индексированы подобно стандартному массиву, посредством определения метода-индексатора. Это
422 Часть III. Дополнительные конструкции программирования на С# конкретное языковое средство наиболее полезно при создании специальных типов коллекций (обобщенных и необобщенных). Прежде чем ознакомиться с реализацией специального индексатора, начнем с рассмотрения его в действии. Предположим, что вы добавили поддержку метода-индексатора к пользовательскому типу PeopleCollection, разработанному в главе 10 (в проекте CustomNonGenericCollection). Рассмотрим следующее его применение в новом консольном приложении по имени Simplelndexer: // Индексаторы позволяют обращаться к элементам в стиле массива. class Program { static void Main(string[] args) { Console.WriteLine ("***** Fun with Indexers *****\n"); PeopleCollection myPeople = new PeopleCollection (); // Добавление объектов с помощью синтаксиса индексатора. myPeople[0] = new Person("Homer", "Simpson", 40); myPeople [1] = new Person("Marge", "Simpson", 38); myPeople[2] = new Person ("Lisa", "Simpson", 9); myPeople[3] = new Person ("Bart", "Simpson", 7) ; myPeople[4] = new Person("Maggie", "Simpson", 2) ; // Получение и вывод на консоль элементов с использованием индексатора. for (int i = 0; i < myPeople.Count; i++) { Console.WriteLine("Person number: {0}", i); Console.WriteLine("Name: {0} {1}", myPeople [i] .FirstName, myPeople[i] .LastName); Console.WriteLine("Age: {0}", myPeople[i].Age); Console.WriteLine(); } } } Как видите, в отношении доступа к подэлементам контейнера индексаторы ведут себя подобно специальным коллекциям, поддерживающим интерфейсы IEnumerator и IE numerable (либо их обобщенные версии). Главное отличие, конечно, в том, что вместо доступа к содержимому с использованием конструкции f о reach можно манипулировать внутренней коллекцией подобъектов подобно стандартному массиву. Но тут возникает серьезный вопрос: как сконфигурировать класс PeopleCollection (или любой другой класс либо структуру) для поддержки этой функциональности? Индексатор представляет собой несколько видоизмененное определение свойства. В его простейшей форме индексатор создается с использованием синтаксиса this [ ]. Ниже показано необходимое изменение класса PeopleCollection из главы 10: // Добавим индексатор к существующему определению класса. public class PeopleCollection : IEnumerable { private ArrayList arPeople = new ArrayList(); // Специальный индексатор для этого класса. public Person this[int index] { get { return (Person)arPeople[index]; } set { arPeople.Insert(index, value); } } }
Глава 12. Расширенные средства языка С# 423 Помимо использования ключевого слова this, индексатор выглядит как объявление любого другого свойства С#. Например, роль конструкции get состоит в возврате корректного объекта вызывающему коду. Здесь мы фактически и делаем это, делегируя запрос к индексатору объекта ArrayList. В противоположность этому, конструкция set отвечает за размещение входящего объекта в контейнере по определенному индексу; в данном примере это достигается вызовом метода Insert () объекта ArrayList. Как видите, индексаторы — это просто еще одна форма синтаксиса, учитывая, что та же функциональность может быть обеспечена с использованием "нормальных" общедоступных методов вроде AddPerson () или GetPerson (). Тем не менее, поддержка методов-индексаторов в специальных типах коллекций позволяет их легко интегрировать с библиотеками базовых классов .NET. Хотя создание методов-индексаторов — обычное дело при построении специальных коллекций, следует помнить, что обобщенные типы предлагают эту функциональность в готовом виде. В следующем методе используется обобщенный список List<T> объектов Person. Обратите внимание, что индексатор List<T> можно просто применять непосредственно. static void UseGenericListOfPeople () { List<Person> myPeople = new List<Person> (); myPeople.Add(new Person("Lisa", "Simpson", 9)); myPeople.Add(new Person("Bart", "Simpson", 7)); // Заменим первую персону с помощью индексатора. myPeople[0] = new Person("Maggie", "Simpson", 2); // Теперь получим и отобразим каждый элемент через индексатор. for (int i = 0; i < myPeople .Count; i++) { Console.WriteLine("Person number: {0}", l) ; Console.WriteLine("Name: {0} {1}", myPeople[i].FirstName, myPeople[i].LastName); Console.WriteLine("Age: {0}", myPeople[l].Age); Console.WriteLine(); } } Исходный код. Проект Simplelndexer доступен в подкаталоге Chapter 12. Индексация данных с использованием строковых значений В текущем классе PeopleCollection определен индексатор, позволяющий вызывающему коду идентифицировать подэлементы с применением числовых значений. Однако надо понимать, что это не обязательное требование метода-индексатора. Предположим, что решено хранить объекты Person, используя System. Collections . Generic . Dictionary<TKey, TValue> вместо ArrayList. Учитывая, что типы ListDictionary позволяют производить доступ к содержащимся в них типам с использованием строкового маркера (такого как фамилия персоны), индексатор можно было бы определить следующим образом: public class PeopleCollection : IEnumerable { private Dictionary<string, Person> listPeople = new Dictionary<string, Person>();
424 Часть III. Дополнительные конструкции программирования на С# // Этот индексатор возвращает персону по строковому индексу. public Person this[string name] { get { return (Person)listPeople[name]; } set { listPeople[name] = value; } } public void ClearPeople () { listPeople.Clear(); } public int Count { get { return listPeople.Count; } } IEnumerator IEnumerable.GetEnumerator() { return listPeople.GetEnumerator(); } } Теперь вызывающий код может взаимодействовать с содержащимися внутри объектами Person, как показано ниже: static void Main(string[] args) { Console.WriteLine ("***** Fun with Indexers *****\n"); PeopleCollection myPeople = new PeopleCollection (); myPeople["Homer"] = new Person("Homer", "Simpson", 40); myPeople["Marge"] = new Person("Marge", "Simpson", 38); // Получит "Homer" и вывести данные на консоль. Person homer = myPeople["Homer"]; Console.WriteLine(homer.ToString() ) ; Console.ReadLine(); } Опять-таки, если использовать обобщенный тип Dictionary<TKey, TValue> напрямую, получится функциональность метода-индексатора в готовом виде, без построения специального необобщенного класса, поддерживающего строковый индексатор. Исходный код. Проект Stringlndexer доступен в подкаталоге Chapter 12. Перегрузка методов-индексаторов Имейте в виду, что методы-индексаторы могут быть перегружены в отдельном классе или структуре. То есть если имеет смысл позволить вызывающему коду обращаться к подэлементам с использованием числового индекса или строкового значения, в одном и том же типе можно определить несколько индексаторов. Например, если вы когда-либо программировали с применением ADO.NET (встроенный API-интерфейс .NET для доступа к базам данных), то вспомните, что тип Data Set поддерживает свойство по имени Tables, которое возвращает строго типизированную коллекцию DataTableCollection. В свою очередь, в DataTableCollection определены три индексатора для получения объектов DataTable — по порядковому номеру, по дружественным строковым именам и необязательному пространству имен: public sealed class DataTableCollection : InternalDataCollectionBase { // Перегруженные индексаторы. public DataTable this[string name] { get; } public DataTable this[string name, string tableNamespace] { get; } public DataTable this[int index] { get; }
Глава 12. Расширенные средства языка С# 425 Следует отметить, что множество типов из библиотек базовых классов поддерживают методы-индексаторы. Поэтому даже если текущий проект не требует построения специальных индексаторов для классов и структур, помните, что многие типы уже поддерживают этот, синтаксис. Многомерные индексаторы Можно также создавать метод-индексатор, принимающий несколько параметров. Предположим, что имеется специальная коллекция, хранящая подэлементы двумерного массива. В этом случае метод-индексатор можно сконфигурировать следующим образом: public class SomeContainer { private int[,] my2DintArray = new int[10, 10]; public int this[int row, int column] { /* установить или получить значение из двумерного массива */ } } Если только не строится очень специализированный класс коллекций, то вряд ли понадобится создавать многомерные индексаторы. Здесь снова пример ADO.NET показывает, насколько полезной может быть эта конструкция. Класс DataTable в ADO.NET — это, по сути, коллекция строк и столбцов, похожая на распечатанную таблицу или электронную таблицу Microsoft Excel. Хотя объекты DataTable обычно наполняются без вашего участия, посредством связанных с ними "адаптеров данных", в приведенном ниже коде показано, как вручную создать находящийся в памяти объект DataTable, содержащий три столбца (для имени, фамилии и возраста). Обратите внимание, что после добавления одной строки в DataTable с помощью многомерного индексатора производится обращение к всем столбцам первой (и единственной) строки. (Чтобы реализовать это, в файл кода понадобится импортировать пространство имен System.Data.) static void MultilndexerWithDataTable () { // Создать простой объект DataTable с тремя столбцами. DataTable myTable = new DataTable (); myTable.Columns.Add(new DataColumn("FirstName")); myTable.Columns.Add(new DataColumn("LastName")); myTable.Columns.Add(new DataColumn("Age")); // Добавить строку к таблице. myTable.Rows.Add("Mel", "Appleby", 60); // Использовать многомерный индексатор для вывода деталей первой строки. Console.WriteLine("First Name: {0}", myTable.Rows[0][0]); Console.WriteLine("Last Name: {0}", myTable.Rows[0][1]); Console.WriteLine("Age : {0}", myTable.Rows[0][2]); } Начиная с главы 21, мы продолжим рассмотрение ADO.NET, так что не пугайтесь, если что что-то в приведенном выше коде покажется незнакомым. Этот пример просто иллюстрирует, что методы-индексаторы могут поддерживать множество измерений, а при правильном использовании могут упростить взаимодействие с подобъектами, содержащимися в специальных коллекциях. Определения индексаторов в интерфейсных типах Индексаторы могут определяться в типе интерфейса, позволяя поддерживающим типам предоставлять их специальные реализации.
426 Часть III. Дополнительные конструкции программирования на С# Ниже показан пример такого интерфейса, который определяет протокол для получения строковых объектов с использованием числового индексатора: public interface IStringContainer { // Этот интерфейс определяет индексатор, возвращающий // строки по числовому индексу. string this[int index] { get; set; } } При таком определении интерфейса любой класс или структура, реализующие его, должны поддерживать индексатор чтения/записи, манипулирующий подэлементами через числовое значение. На этом первая главная тема настоящей главы завершена. Хотя понимание синтаксиса индексаторов С# важно, как объяснялось в главе 10, обычно единственным случаем, когда программисту нужно строить специальный обобщенный класс коллекции, является ситуация, когда необходимо добавить ограничения к параметрам-типам. Если придется строить такой класс, добавление специального индексатора может заставить класс коллекции выглядеть и вести себя подобно стандартному классу коллекции из библиотеки базовых классов .NET. А теперь давайте рассмотрим языковое средство, позволяющее строить специальные классы и структуры, которые уникальным образом реагируют на встроенные операции С# — перегрузку операций. Понятие перегрузки операций В С#, подобно любому языку программирования, имеется готовый набор лексем, используемых для выполнения базовых операций над встроенными типами. Например, известно, что операция + может применяться к двум целым, чтобы дать их сумму: // Операция + с целыми. int a = 10 0; int b = 240; int с = а + Ь; //с теперь равно 340 Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных С#? Например, рассмотрим такой код: // Операция + со строками. string si = "Hello"; string s2 = " world!"; string s3 = si + s2; // s3 теперь содержит "Hello world!" По сути, функциональность операции + уникальным образом базируются на представленных типах данных (строках или целых в данном случае). Когда операция + применяется к числовым типам, мы получаем арифметическую сумму операндов. Однако когда та же операция применяется к строковым типам, получается конкатенация строк. Язык С# предоставляет возможность строить специальные классы и структуры, которые также уникально реагируют на один и тот же набор базовых лексем (вроде операции +). Имейте в виду, что абсолютно каждую встроенную операцию С# перегружать нельзя. В табл. 12.1 описаны возможности перегрузки основных операций.
Глава 12. Расширенные средства языка С# 427 Таблица 12.1. Возможности перегрузки операций С# Операция С# Возможность перегрузки true, Этот набор унарных операций может быть перегружен +, -, fal. +i -i ==, i se * i = ~, /, <, ++I "■ %, &, >i <=. -, 1 I, ' , > :, » Эти бинарные операции могут быть перегружены Эти операции сравнения могут быть перегружены. С# требует совместной перегрузки "подобных" операций (т.е. < и >, <= и >=, ==и !=) [ ] Операция [ ] не может быть перегружена. Как было показано ранее в этой главе, однако, аналогичную функциональность предлагают индексаторы () Операция () не может быть перегружена. Однако, как будет показано далее в этой главе, ту же функциональность предоставляют специальные методы преобразования +=, -=, *=, /=, %=, &=, | =, Сокращенные операции присваивания не могут перегружаться; А=, «=, »= однако вы получаете их автоматически, перегружая соответствующую бинарную операцию Перегрузка бинарных операций Чтобы проиллюстрировать процесс перегрузки бинарных операций, представим следующий простой класс Point, определенный в новом консольном приложении по имени OverloadedOps: // Простой класс С# для повседневного пользования. public class Point { public int X {get; set;} public int Y {get; set;} public Point(int xPos, int yPos) { X = xPos; Y = yPos; } public override string ToStringO return string.Format("[{0}, {1}]", this.X, this.Y); } Теперь, логически рассуждая, имеет смысл складывать экземпляры Point вместе. Например, если сложить вместе две переменных Point, получится новая Point с суммарными значениями х и у. Кстати, также может быть полезно иметь возможность вычитать одну Point из другой. В идеале пригодилась бы возможность написать такой код: // Сложение и вычитание двух точек? static void Main(string [ ] args) { Console.WriteLine("***** Fun with Overloaded Operators *****\n"); // Создать две точки. Point ptOne = new Point A00, 100); Point ptTwo = new Point D0, 40); Console.WriteLine("ptOne = {0}", ptOne); Console.WriteLine("ptTwo = {0}", ptTwo);
428 Часть III. Дополнительные конструкции программирования на С# // Сложить две точки, чтобы получить большую? Console.WriteLine("ptOne + ptTwo: {0} ", ptOne + ptTwo); // Вычесть одну точку из другой, чтобы получить меньшую? Console.WriteLine("ptOne - ptTwo: {0} ", ptOne - ptTwo); Console.ReadLine(); } Однако в том виде, как он есть, класс Point приведет к ошибкам этапа компиляции, поскольку типу Point не известно, как реагировать на операции + и -. Чтобы оснастить специальный тип возможностью уникально реагировать на встроенные операции, в С# служит ключевое слово operator, которое может использоваться только в сочетании со статическими методами. При перегрузке бинарной операции (вроде + и -) чаще всего передаются два аргумента того же типа, что и определяющий их класс (в данном примере — Point); это иллюстрируется в следующей модификации кода: // Более интеллектуальный тип Point. public class Point { // Перегруженная операция + public static Point operator + (Point pi, Point p2) { return new Point(pi.X + p2.X, pi. Y + p2.Y); } // Перегруженная операция - public static Point operator - (Point pi, Point p2) { return new Point(pl.X - p2.X, pl.Y - p2.Y); } } Логика, положенная в основу операции +, состоит просто в возврате нового экземпляра Point на основе сложения соответствующих полей входных параметров Point. Поэтому, когда вы напишете ptl + pt2, "за кулисами" произойдет следующий скрытый вызов статического метода operator+: // Псевдокод: Point рЗ = Point.operator+ (pi, p2) Point рЗ = pi + р2; Аналогично, pi - р2 отображается на следующее: // Псевдокод: Point р4 = Point. operator- (pi, p2) Point р4 = pi - р2; После этого дополнения программа скомпилируется, и мы получим возможность складывать и вычитать объекты Point: ptOne = [100, 100] ptTwo = [40, 40] ptOne + ptTwo: [140, 140] ptOne - ptTwo: [60, 60] При перегрузке бинарной операции вы не обязаны передавать ей два параметра одинакового типа. Если это имеет смысл, один из аргументов может отличаться. Например, ниже показана перегруженная операция +, которая позволяет вызывающему коду получить новый объект Point на основе числового смещения: public class Point { public static Point operator + (Point pi, int change) { return new Point(pi.X + change, pl.Y + change); }
Глава 12. Расширенные средства языка С# 429 public static Point operator + (int change, Point pi) { return new Point(pi.X + change, pl.Y + change); Обратите внимание, что если нужно передавать аргументы в любом порядке, потребуются обе версии метода (т.е. нельзя просто определить один из методов и рассчитывать, что компилятор автоматически будет поддерживать другой). Теперь можно использовать эти новые версии операции + следующим образом: // Выводит [110, 110] Point biggerPoint = ptOne + 10; Console.WriteLine("ptOne + 10 = {0}", biggerPoint); // Выводит [120, 120] Console.WriteLine(0 + biggerPoint = {0}", 10 + biggerPoint); Console.WriteLine () ; А как насчет операций += и -=? Перешедших на С# с языка C++ может удивить отсутствие возможности перегрузки операций сокращенного присваивания (+=, -+ и т.д.). Не беспокойтесь. В терминах С# операции сокращенного присваивания автоматически эмулируются при перегрузке соответствующих бинарных операций. Таким образом, если в классе Point уже перегружены операции + и -, можно написать следующий код: // Перегрузка бинарных операций автоматически обеспечивает // перегрузку сокращенных операций, static void Main (string[] args) { // Операция += автоматически перегружена Point ptThree = new Point (90, 5) ; Console.WriteLine("ptThree = {0}", ptThree); Console.WriteLine("ptThree += ptTwo: {0}", ptThree += ptTwo); // Операция -= автоматически перегружена Point ptFour = new Point @, 500); Console.WriteLine("ptFour = {0}", ptFour); Console.WriteLine ("ptFour -= ptThree: {0}", ptFour -= ptThree); Console.ReadLine (); } Перегрузка унарных операций В С# также допускается перегружать и унарные операции, такие как ++ и —. При перегрузке унарной операции также определяется статический метод через ключевое слово operator, однако в этом случае просто передается единственный параметр того же типа, что и определяющий его класс/структура. Например, дополните Point следующими перегруженными операциями: public struct Point { // Прибавить 1 к значениям X/Y входного объекта Point. public static Point operator ++(Point pi) { return new Point(pi.X+l, pl.Y+1); } // Вычесть 1 из значений X/Y входного объекта Point. public static Point operator —(Point pi) { return new Point(pi.X-l, pl.Y-1); }
430 Часть III. Дополнительные конструкции программирования на С# В результате появляется возможность выполнять инкремент и декремент значений X и Y класса Point, как показано ниже: static void Main(string [ ] args) { // Применение унарных операций ++ и — к Point. Point ptFive = new Point A, 1) ; Console.WriteLine("++ptFive = {0}", ++ptFive); // [2, 2] Console.WriteLine("—ptFive = {0}", —ptFive); // [1, 1] // Применение тех же операций для постфиксного инкремента/декремента. Point ptSix = new Point B0, 20); Console.WriteLine("ptSix++ = {0}", ptSix++); // [20, 20] Console.WriteLine ("ptSix-- = {0}", ptSix—) ; // [21, 21] Console.ReadLine(); } В предыдущем примере кода обратите внимание, что специальные операции ++ и — применяются двумя разными способами. В C++ допускается перегружать операции префиксного и постфиксного инкремента/декремента по отдельности. В С# это невозможно; тем не менее, возвращаемое значение инкремента/декремента автоматически обрабатывается правильно (т.е., для перегруженной операции ++ выражение pt++ имеет значение ^модифицированного объекта, в то время как ++pt имеет новое значение, примененное перед использованием выражения). Перегрузка операций эквивалентности Как вы должны помнить из главы 6, метод System.Object .Equals () может быть перегружен для выполнения сравнений объектов на основе значений (а не ссылок). Если вы решите переопределить Equals () (часто вместе со связанным методом System. Object. GetHashCode ()), это позволит легко переопределить и операции проверки эквивалентности (== и ! =). Для иллюстрации рассмотрим модифицированное определение типа Point: // Этот вариант Point также перегружает операции == и '=. public class Point { public override bool Equals(object o) { return o.ToStringO == this.ToString(); } public override int GetHashCode () { return this.ToString () .GetHashCode(); } // Теперь перегрузим операции == и !=. public static bool operator ==(Point pi, Point p2) { return pi.Equals(p2); } public static bool operator !=(Point pi, Point p2) { return 'pi.Equals (p2); } } Обратите внимание, что для выполнения нужной работы реализации операций == и ! = просто вызывают перегруженный метод Equals (). Теперь класс Point можно использовать следующим образом: // Использование перегруженных операций эквивалентности. static void Main(string [ ] args) { Console.WriteLine("ptOne == ptTwo : {0}", ptOne == ptTwo);
Глава 12. Расширенные средства языка С# 431 Console.WriteLine("ptOne !=ptTwo : {0}", ptOne != ptTwo); Console.ReadLine(); } Как видите, сравнение двух объектов с применением хорошо знакомых операций == и ! = выглядит намного интуитивно понятней, чем вызов Object .Equals (). При перегрузке операций эквивалентности для определенного класса помните, что С# требует, чтобы в случае перегрузки операции == обязательно перегружалась также и операция ! = (компилятор напомнит, если вы забудете это сделать). Перегрузка операций сравнения В главе 9 было показано, как реализовать интерфейс I Comparable для выполнения сравнений двух сходных объектов. Вдобавок для того же класса можно перегрузить операции сравнения (<,>,<= и >=). Подобно операциям эквивалентности, С# и здесь требует, чтобы в случае перегрузки операции < обязательно перегружалась также и операция >. После перегрузки в классе Point этих операций сравнения пользователь объекта сможет сравнивать объекты Point следующим образом: // Использование перегруженных операций < и >. static void Main(string[] args) { Console.WriteLine ("ptOne < ptTwo : {0}", ptOne < ptTwo); Console.WriteLine ("ptOne > ptTwo : {0}", ptOne > ptTwo); Console.ReadLine(); } Предполагая, что интерфейс IComparable уже реализован, перегрузка операций сравнения становится тривиальной. Ниже показано модифицированное определение класса. // Объекты Point также можно сравнивать с помощью операций сравнения. public class Point : IComparable { public int CompareTo(object obj) { if (obj is Point) { Point p = (Point)obj; if (this.X > p.X && this.Y > p.Y) return 1; if (this.X < p.X && this.Y < p.Y) return -1; else return 0; } else throw new ArgumentException (); } public static bool operator <(Point pi, Point p2) { return (pi.CompareTo(p2) < 0) ; } public static bool operator >(Point pi, Point p2) { return (pi.CompareTo(p2) > 0) ; } public static bool operator <= (Point pi, Point p2) { return (pi.CompareTo (p2) <= 0) ; } public static bool operator >=(Point pi, Point p2) { return (pi.CompareTo (p2) >= 0) ; } }
432 Часть III. Дополнительные конструкции программирования на С# Внутреннее представление перегруженных операций Подобно любому программному элементу С#, перегруженные операции имеют специальное представление в синтаксисе CIL. Чтобы начать исследование того, что происходит "за кулисами", откройте сборку OverloadedOps .exe в утилите ildasm.exe. Как видно на рис. 12.1, перегруженные операции внутренне представлены в виде скрытых методов (op_Addition (), op_Subtraction (), op_Equality () и т.д.). ft НЛМу File View Help ■ ToString : stringQ ■ get_X : int32() ■ get_Y : rt32() ; a| Q op_Addbon : class OverloadedOps.Port(int32,class OverloadedOps.Port) О op.AddJbon : dass OverloadedOps.Port(dass OverioadedOps.Port,class OverloadedOps.Port) U op_Decrement: class OverloadedOps.Port(class OverloadedOps.Port) ; Q op_Equalty : booKdass OverloadedOps.Port,class OverloadedOps.Port) ! В op_GreaterThan : booKclass OverloadedOps.Port,dass OverloadedOps.Port) В op.GreaterThanOrEqual: booKdass OverloadedOps.Port,class OverloadedOps.Port) В opjncrement: dass OverbadedOps.Port(dass OverloadedOps.Port) В opjnequafcty : booKdass OverloadedOps.Port,dass OverloadedOps.Port) В op_LessThan : booKdass OverloadedOps.PoW,dass OverloadedOps.Port) В op_LessThanOrEqual: booKdass OverloadedOps.Port,class OverloadedOps.Port) В op.Subtracoon : dass OverloadedOps.Port(dass OverloadedOps.Port,class OverloadedOps.Port) Hiiiiriii assembly OverloadedOps Рис. 12.1. В терминах CIL перегруженные операции отображаются на скрытые методы Если посмотреть на инструкции CIL для метода opAddition (того, что принимает два параметра Point), легко заметить, что компилятор также вставил модификатор метода specialname: .method public hidebysig specialname static class OverloadedOps.Point op_Addition (class OverloadedsOps.Point pi, class OverloadedOps.Point p2) cil managed { } В действительности любая операция, которую можно перегрузить, превращается в специально именованный метод в коде CIL. В табл. 12.2 приведены отображения на CIL для наиболее распространенных операций С#. Таблица 12.2. Отображение операций С# на специально именованные методы CIL Встроенная операция С# Представление CIL ++ + op_Decrement() op_Increment() op_Addition() op_Subtraction() op_Multiply() op_Division() op_Equality() op_GreaterThan() op_LessThan()
Глава 12. Расширенные средства языка С# 433 Окончание табл. 12.2 Встроенная операция С# Представление CIL i = >= <= op_Inequality() op_GreaterThanOrEqual() op_LessThanOrEqual() op_SubtractionAssignment() op_AdditionAssignment() Финальные соображения относительно перегрузки операций Как уже было показано, С# предлагает возможность строить типы, которые могут уникальным образом реагировать на различные встроенные, хорошо известные операции. Теперь перед добавлением поддержки этого поведения в классы необходимо убедиться в том, что операции, которые вы собираетесь перегружать, имеют хоть какой-то смысл в реальном мире. Например, предположим, что перегружена операция умножения для класса Mini Van (минивэн). Что вообще должно означать перемножение двух объектов MiniVan? He слишком много. Фактически, если коллеги по команде увидят следующее использование объектов MiniVan, то будут весьма озадачены: // Это не слишком понятно.. . MiniVan newVan = myVan * yourVan; Перегрузка операций обычно полезна только при построении служебных типов. Строки, точки, прямоугольники, функции и шестиугольники — подходящие кандидаты на перегрузку операций. Люди, менеджеры, автомобили, подключения к базе данных и веб-страницы — нет. В качестве эмпирического правила: если перегруженная операция затрудняет пользователю понимание функциональности типа, не делайте этого. Используйте это средство с умом. Также имейте в виду, что даже если вы не хотите перегружать операции в специальных классах, это уже сделано в многочисленных типах из библиотек базовых классов. Например, сборка System.Drawing.dll предлагает определение Point, применяемое в Windows Forms, в котором перегружено множество операций. Обратите внимание на значок операции в браузере объектов Visual Studio 2010 (рис. 12.2). А *fj ImageAnimator ^$ ImageConverter *{$ ImageFormatConverter jtf- KnownColor ^Pen ^Pens + Point > C3 Base Types !> СД Derived Types -ty PointCorrverter + PointF ■■у Rectangle L -if, explicit operator(System.Drawing.Point) Щ implicit operator(System.Drawing.Point) Щ operator !=(System.Drawing.Point, System.Drawing.Point) УЩ operator -(System.Drawing.Poirrt, System.Drawing.Size) H7, operator = = (System.Drawing.Point System.Drawing.Pointj Jf IsEmpty J public static System.Drawing.Point operator + (System.Drawing.Point pt, System.Drawing.Size sz) Member of System Drawing.Point I Summery. rJ Translates a System.Drawing.Poirrt by a given System.Drawing.Size. ■ Рис. 12.2. Множество типов в библиотеках базовых классов включают уже перегруженные операции
434 Часть III. Дополнительные конструкции программирования на С# Исходный код. Проект OverloadedOps доступен в подкаталоге Chapter 12. Понятие преобразований пользовательских типов Теперь обратимся к теме, близкой к перегрузке операций — преобразованию пользовательских типов. Чтобы заложить фундамент для последующей дискуссии, давайте кратко опишем нотацию явного и неявного преобразования между числовыми данными и связанными с ними типами классов. Числовые преобразования В терминах встроенных числовых типов (sbyte, int, float и т.п.) явное преобразование требуется при попытке сохранить большее значение в меньшем контейнере, поскольку это может привести к потере данных. По сути, это означает, что вы говорите компилятору: "Я знаю, что делаю". В противоположность этому неявное преобразование происходит автоматически, когда вы пытаетесь поместить меньший тип в целевой тип, и в результате этой операции не происходит потеря данных: static void Main() { int a = 123; long b = a; // Неявное преобразование int в long int с = (int) b; // Явное преобразование long в int } Преобразования между связанными типами классов Как было показано в главе 6, типы классов могут быть связаны классическим наследованием (отношение "является" ("is а")). В этом случае процесс преобразования С# позволяет выполнять приведение вверх и вниз по иерархии классов. Например, класс- наследник всегда может быть неявно приведен к базовому типу. Однако если необходимо хранить тип базового класса в переменной типа класса-наследника, понадобится явное приведение: // Два связанных типа классов, class Basel} class Derived : Basel} class Program I static void Main(string [] args) I // Неявное приведение наследника к предку. Base myBaseType; myBaseType = new Derived(); // Для хранения базовой ссылке в ссылке //на наследника нужно явное преобразование. Derived myDerivedType = (Derived)myBaseType; } } Это явное приведение работает благодаря тому факту, что классы Base и Derived связаны отношением классического наследования. Однако что если есть два типа классов из разных иерархий без общего предка (кроме System.Object), которые требуют преобразования друг в друга? Если они не связаны классическим наследованием, явное приведение здесь не поможет.
Глава 12. Расширенные средства языка С# 435 Рассмотрим типы значений, такие как структуры. Предположим, что определены две структуры .NET с именами Square и Rectangle. Учитывая, что они не могут полагаться на классическое наследование (поскольку всегда запечатаны), нет естественного способа выполнить приведение между этими, на первый взгляд, связанными типами. Наряду с возможностью создания в структурах вспомогательных методов (вроде Rectangle.ToSquare ()), язык С# позволяет строить специальные процедуры преобразования, которые позволят типам реагировать на операцию приведения (). Таким образом, если корректно сконфигурировать эти структуры, можно будет использовать следующий синтаксис для явного преобразования между ними: // Преобразовать Rectangle в Square! Rectangle rect; rect.Width = 3; rect.Height =10; Square sq = (Square)rect; Создание специальных процедур преобразования Начнем с создания нового консольного приложения по имени CustomCconversions. В С# предусмотрены два ключевых слова — explicit и implicit, которые можно применять для управления реакцией на попытки выполнить преобразования. Предположим, что имеются следующие определения классов: public class Rectangle { public int Width {get; set;} public int Height {get; set;} public Rectangle (int w, int h) { Width = w; Height = h; } public Rectangle(){} public void Draw() { for (int i = 0; l < Height; i++) { for (int j = 0; j < Width; j++) { Console.Write("*"); } Console.WriteLine(); } } public override string ToStringO { return string.Format(" [Width = {0}; Height = {1}]", Width, Height); } } public class Square { public int Length {get; set;} public Square(int 1) { Length = 1; } public Square () {}
436 Часть III. Дополнительные конструкции программирования на С# public void Draw() { for (int i = 0; l < Length; i++) { for (int j = 0; j < Length; j++) { Console.Write("*"); } Console.WriteLine(); } } public override string ToStringO { return string.Format("[Length = {0}]", Length); } // Rectangle можно явно преобразовать в Square, public static explicit operator Square(Rectangle r) { Square s = new Square(); s.Length = r.Height; return s; } } Обратите внимание, что эта итерация типа Squire определяет явную операцию преобразования. Подобно процессу перегрузки операций, процедуры преобразования используют ключевое слово operator в сочетании с ключевым словом explicit или implicit и должны быть статическими. Входным параметром является сущность, из которой выполняется преобразование, в то время как тип операции — сущность, к которой оно производится. В этом случае предполагается, что квадрат (геометрическая фигура с четырьмя равными сторонами) может быть получен из высоты прямоугольника. Таким образом, преобразовать Rectangle в Square можно следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Conversions *****\nM); // Создать Rectangle. Rectangle r = new RectangleA5, 4); Console.WriteLine(r.ToString()); r.Draw(); Console.WriteLine(); // Преобразовать г в Square на основе высоты Rectangle. Square s = (Square)r; Console.WriteLine(s.ToString()); s.Draw (); Console.ReadLine(); } Вывод этой программы показан на рис. 12.3. Хотя, может быть, не слишком полезно преобразовывать Rectangle в Square в пределах одного контекста, предположим, что есть функция, спроектированная так, чтобы принимать параметров Square: // Этот метод требует параметр типа Square. static void DrawSquare(Square sq) { Console.WriteLine(sq.ToString()); sq.Draw();
Глава 12. Расширенные средства языка С# 437 Имея операцию явного преобразования в тип Square, можно передавать типы Rectangle для обработки этому методу, используя явное приведение: static void Main(string[] args) { // Преобразовать Rectangle в Square для вызова метода. Rectangle rect = new Rectangle A0, 5) ; DrawSquare((Square)rect); Console.ReadLine(); } as C\Windo*s\system32\cmd.exe 1 i***** рип w-jth Conversions [Width = 15; Height = 4] *************** *************** I*************** *************** ■[Length = 4] ■**** ■**** **** I**** ■Press any key to continue . in ■ Ь^ЁУи^яГ ***** - - - _^» - j Рис. 12.3. Преобразование Rectangle в Square Дополнительные явные преобразования типа Square Теперь, когда можно явно преобразовывать объекты Rectangle в объекты Square, давайте рассмотрим несколько дополнительных явных преобразований. Учитывая, что квадрат симметричен по всем сторонам, может быть полезно предусмотреть процедуру преобразования, которая позволит вызывающему коду привести целочисленный тип к типу Square (который, разумеется, будет иметь длину стороны, равную переданному целому). Аналогично, что если вы захотите модифицировать Square так, чтобы вызывающий код мог выполнять приведение из Square в System. Int32? Логика вызова выглядит следующим образом. static void Main(string[] args) { // Преобразование int в Square. Square sq2 = (Square)90; Console.WriteLine(Msq2 = {0}", sq2); // Преобразование Square в int. int side = (int)sq2; Console.WriteLine("Side length of sq2 = {0}", side); Console.ReadLine(); } Ниже показаны необходимые изменения в классе Square: public class Square { public static explicit operator Square(int sideLength) {
438 Часть III. Дополнительные конструкции программирования на С# Square newSq = new Square(); newSq.Length = sideLength; return newSq; } public static explicit operator int (Square s) {return s.Length;} } По правде говоря, преобразование Square в int может показаться не слишком интуитивно понятной (или полезной) операцией. Однако это указывает на один очень важный факт, касающийся процедур пользовательских преобразований: компилятор не волнует, что и куда преобразуется, до тех пор, пока пишется синтаксически корректный код. Таким образом, как и с перегруженными операциями, возможность создания операций явного приведения еще не означает, что вы обязаны их создавать. Обычно эта техника наиболее полезна при создании типов структур .NET, учитывая, что они не могут участвовать в отношениях классического наследования (где приведение достается бесплатно). Определение процедур неявного преобразования До сих пор вы создавали различные пользовательские операции явного преобразования. Однако что, если понадобится неявное преобразование? static void Main(string[] args) { // Попытка выполнить неявное приведение? Square s3 = new Square (); s3.Length =83; Rectangle rect2 = s3; Console.ReadLine(); } Этот код не скомпилируется, если для типа Rectangle не будет предусмотрена процедура неявного преобразования. Ловушка здесь вот в чем: не допускается иметь одновременно функции явного и неявного преобразования, если они не отличаются по типу возвращаемого значения или списку параметров. Это может показаться ограничением, однако вторая ловушка состоит в том, что когда тип определяет процедуру неявного преобразования, никто не запретит вызывающему коду использовать синтаксис явного приведения! Запутались? Для того чтобы прояснить ситуацию, давайте добавим к классу Rectangle процедуру неявного преобразования, используя для этого ключевое слово implicit (обратите внимание, что в следующем коде предполагается, что ширина результирующего Rectangle вычисляется умножением стороны Square на 2): public class Rectangle { public static implicit operator Rectangle(Square s) { Rectangle r = new Rectangle (); r.Height = s.Length; // Предположим, что длина нового Rectangle // будет равна (Length x 2) г.Width = s.Length * 2; return r; } }
Глава 12. Расширенные средства языка С# 439 После такой модификации можно будет выполнять преобразование между типами: static void Main(string [ ] args) { // Неявное преобразование работает! Square s3 = new Square (); s3.Length = 7; Rectangle rect2 = s3; Console.WriteLine("rect2 = {0}", rect2); DrawSquare(s3); // Синтаксис явного преобразования также работает! Square s4 = new Square (); s4.Length =3; ■ Rectangle rect3 = (Rectangle)s4; Console.WriteLine("rect3 = {0}", rect3); Console.ReadLine() ; } Внутреннее представление процедур пользовательских преобразований Подобно перегруженным операциям, методы, квалифицированные ключевыми словами implicit или explicit, имеют специальные имена в терминах CIL: oplmplicit и opExplicit, соответственно (рис. 12.4). р НЛМу Ex>ki\C* Вох£* and the ,NF Г atform 5th ed'circt Dr»«SChapter_12\Code\CustomC.. File i/iew Help й Щ CustomConverstons Ш £ CustomConverstons.Program * Щ. CustomConverstons.Rectangle й Щ- CustomConverstons.Square ► .class public auto ansi beforefieldinit v <Length>k_BacttngFtoW : private W32 ■ ,ctor : votoX) ■ .ctor : votd(nt32) ■ Draw: votoX) ■ ToString: strinoX) Щ get Length : jnt32Q аГ ^ (J op_Explclt: dass CustomConverstons.Square(int32) Ё1 op_Expkit: mt32(class CustomConverstons.Square) ■ set .Length : void(Jnt32) A Length : instance int32() мэдгштшитта ■?1Й|1М1 •assembly CustomConverstons Рис. 12.4. Представление CIL определяемых пользователем процедур преобразования На заметку! В браузере объектов Visual Studio 2010 операции пользовательских преобразований отображаются с использованием значков "явная операция" и " неявная операция". На этом рассмотрение определений операций пользовательского преобразования завершено. Как и с перегруженными операциями, здесь следует помнить, что данный фрагмент синтаксиса представляет собой просто сокращенное обозначение "нормальных" функций-членов, и в этом смысле является необязательным. Однако в случае правильного применения пользовательские структуры могут использоваться более естественно, поскольку трактуются как настоящие типы классов, связанные наследованием. Исходный код. Проект CustomConversions доступен в подкаталоге Chapter 12.
440 Часть III. Дополнительные конструкции программирования на С# Понятие расширяющих методов В .NET 3.5 появилась концепция расширяющих методов (extension method), которая позволила добавлять новую функциональность к предварительно скомпилированным типам "на лету". Известно, что как только тип определен и скомпилирован в сборку .NET, его определение становится более-менее окончательным. Единственный способ добавления новых членов, обновления или удаления членов состоит в перекодировании и перекомпиляции кодовой базы в обновленную сборку (или же можно прибегнуть к более радикальным мерам, таким как использование пространства имен System. Reflection.Emit для динамического изменения скомпилированного типа в памяти). Теперь в С# можно определять расширяющие методы. Суть расширяющих методов в том, что они позволяют существующим скомпилированным типам (а именно — классам, структурам или реализациям интерфейсов), а также типам, которые в данный момент компилируются (такие как типы в проекте, содержащем расширяющие методы), получать новую функциональность без необходимости в непосредственном изменении расширяемого типа. Эта техника может оказаться полезной, когда нужно внедрить новую функциональность в типы, исходный код которых не доступен. Также она может пригодиться, когда необходимо заставить тип поддерживать набор членов (в интересах полиморфизма), но вы не можете модифицировать его исходное объявление. Механизм расширяющих методов позволяет добавлять функциональность к предварительно скомпилированным типам, создавая иллюзию, что она была у него всегда. На заметку! Имейте в виду, что расширяющие методы на самом деле не изменяют скомпилированную кодовую базу! Эта техника лишь добавляет члены к типу в контексте текущего приложения. При определении расширяющих методов первое ограничение состоит в том, что они должны быть определены внутри статического класса (см. главу 5), и потому каждый расширяющий метод должен быть объявлен с ключевым словом static. Второй момент состоит в том, что все расширяющие методы помечаются таковыми посредством ключевого слова this в виде модификатора первого (и только первого) параметра данного метода. Третий момент — каждый расширяющий метод может быть вызван либо от текущего экземпляра в памяти, либо статически, через определенный статический класс! Звучит странно? Давайте рассмотрим полный пример, чтобы прояснить картину. Определение расширяющих методов Создадим новое консольное приложение по имени ExtensionMethods. Теперь предположим, что строится новый служебный класс по имени MyExtensions, в котором определены два расширяющих метода. Первый позволяет любому объекту из библиотек базовых классов .NET получить новый метод по имени DisplayDef iningAssembly (), который использует типы из пространства имен System.Reflection для отображения сборки указанного типа. На заметку! API-интерфейс рефлексии формально рассматривается в главе 15. Если эта тема является новой, просто знайте, что рефлексия позволяет исследовать структуру сборок, типов и членов типов во время выполнения. Второй расширяющий метод по имени ReverseDigits () позволяет любому экземпляру System. Int32 получить новую версию себя, но с обратным порядком следования
Глава 12. Расширенные средства языка С# 441 цифр. Например, если на целом значении 1234 вызвать ReverseDigits (), возвращенное целое значение будет равно 4 321. Взгляните на следующую реализацию класса (не забудьте импортировать пространство имен System.Reflection): static class MyExtensions { // Этот метод позволяет любому объекту отобразить // сборку, в которой он определен. public static void DisplayDefiningAssembly(this object obj) { Console.WriteLine("{0} lives here: => {l}\n", ob].GetType().Name, Assembly.GetAssembly(ob].GetType()).GetName().Name); } // Этот метод позволяет любому целому изменить порядок следования // десятичных цифр на обратный. Например, 56 превратится в 65. public static int ReverseDigits(this int i) { // Транслировать int в string и затем получить все его символы. char[] digits = i.ToString() .ToCharArray (); // Изменить порядок элементов массива. Array.Reverse(digits); // Вставить обратно в строку. string newDigits = new string(digits); // Вернуть модифицированную строку как int. return int.Parse(newDigits) ; } } Обратите внимание, что первый параметр каждого расширяющего метода квалифицирован ключевым словом this, перед определением типа параметра. Первый параметр расширяющего метода всегда представляет расширяемый тип. Учитывая, что DisplayDef iningAssembly () прототипирован расширять System. Ob j ect, любой тип в любой сборке теперь получает этот новый член. Однако ReverseDigits () прототипирован только для расширения целочисленных типов, и потому если что-то другое попытается вызвать этот метод, возникнет ошибка времени компиляции. Знайте, что каждый расширяющий метод может иметь множество параметров, но только первый параметр может быть квалифицирован как this. Например, вот как выглядит перегруженный расширяющий метод, определенный в другом служебном классе по имени TestUtilClass: static class TesterUtilClass { // Каждый Int32 теперь имеет метод Foo() . . . public static void Foo(this int i) { Console.WriteLine ("{0} called the Foo() method.", i); } // ...который перегружен для приема параметра string! public static void Foo(this int i, string msg) { Console.WriteLine ("{0} called Foo() and told me: {1}", i, msg); } } Вызов расширяющих методов на уровне экземпляра После определения этих расширяющих методов теперь все объекты (в том числе, конечно же, все содержимое библиотек базовых классов .NET) имеют метод по имени DisplayDef iningAssembly () , в то время как типы System. Int32 (и только целые) — методы ReverseDigits () и Foo ():
442 Часть III. Дополнительные конструкции программирования на С# static void Main(string[] args) { Console.WriteLine ("***** Fun with Extension Methods *****\n"); // В int появилась новая идентичность1 int mylnt = 12345678; mylnt.DisplayDefiningAssembly(); // To же и у DataSet! System.Data.DataSet d = new System.Data.DataSet (); d.DisplayDefiningAssembly(); // И у SoundPlayerl System.Media.SoundPlayer sp = new System.Media.SoundPlayer(); sp.DisplayDefiningAssembly(); // Использовать новую функциональность int. Console.WriteLine("Value of mylnt: {0}", mylnt); Console.WriteLine("Reversed digits of mylnt: {0}", mylnt.ReverseDigits()) ; mylnt.Foo(); mylnt.Foo("Ints that Foo? Who would have thought it!"); bool b2 = true; // Ошибка I Booleans не имеет метода Foo() I // b2.Foo() ; Console.ReadLine(); } Ниже показан вывод этой программы: ••••• Fun with Extension Methods ***** Int32 lives here: => mscorlib DataSet lives here: => System.Data SoundPlayer lives here: => System Value of mylnt: 12345678 Reversed digits of mylnt: 87654321 12345678 called the Foo() method. 12345678 called Foo () and told me: Ints that Foo? Who would have thought it! Вызов расширяющих методов статически Вспомните, что первый параметр расширяющего метода помечен ключевым словом this, а за ним следует тип элемента, к которому метод применяется. Если вы посмотрите, что происходит "за кулисами" (с помощью инструмента вроде ildasm.exe), то обнаружите, что компилятор просто вызывает "нормальный" статический метод, передавая переменную, на которой вызывается метод, в первом параметре (т.е. в качестве значения this). Ниже показаны примерные подстановки кода. private static void Main(string[] args) { Console.WriteLine("***** Fun with Extension Methods *****\n"); int mylnt = 12345678; MyExtensions.DisplayDefiningAssembly(mylnt); System.Data.DataSet d = new DataSet(); MyExtensions.DisplayDefiningAssembly(d); System.Media.SoundPlayer sp = new SoundPlayer(); MyExtensions.DisplayDefiningAssembly(sp); Console.WriteLine("Value of mylnt: {0}", mylnt); Console.WriteLine("Reversed digits of mylnt: {0}", MyExtensions.ReverseDigits(mylnt)); TesterUtilClass.Foo(mylnt); TesterUtilClass.Foo(mylnt, "Ints that Foo? Who would have thought it!"); Console.ReadLine(); }
Глава 12. Расширенные средства языка С# 443 Учитывая, что вызов расширяющего метода из объекта (что похоже на вызов метода уровня экземпляра) — это просто эффект "дымовой завесы", создаваемый компилятором, расширяющие методы всегда можно вызвать как нормальные статические методы, используя привычный синтаксис С# (как показано выше). Контекст расширяющего метода Как только что объяснялось, расширяющие методы — это, по сути, статические методы, которые могут быть вызваны от экземпляра расширяемого типа. Поскольку это — разновидность синтаксического "украшения", важно понимать, что в отличие от "нормального" метода, расширяющий метод не имеет прямого доступа к членам типа, который он расширяет. Иначе говоря, расширение — это не наследование. Взгляните на следующий простой тип Саг: public class Car { public int Speed; public int SpeedUp () { return ++Speed; } } Построив расширяющий метод для типа Car по имени SlowDown (), вы не получите прямого доступа к членам Саг внутри контекста расширяющего метода, поскольку это не является классическим наследованием. Таким образом, следующий код вызовет ошибку компиляции: public static class CarExtensions { public static int SlowDown(this Car c) { // Ошибка! Этот метод не унаследован от Саг! return --Speed/ } } Проблема в том, что расширяющий метод SlowDown () пытается обратиться к полю Speed типа Саг. Однако поскольку SlowDown () — статический член класса CarExtension, в его контексте отсутствует Speed! Тем не менее, допустимо использовать параметр, квалифицированный словом this, для обращения к общедоступным (и только общедоступным) членам расширяемого типа. Таким образом, следующий код успешно скомпилируется, как и следовало ожидать: public static class CarExtensions { public static int SlowDown(this Car c) { // Скомпилируется успешно! return --с.Speed; } } Теперь можно создать объект Car и вызывать методы SpeedUp () и SlowDown (), как показано ниже: static void UseCarO { Car с = new Car(); Console.WriteLine("Speed: {0}", с.SpeedUp()); Console.WriteLine("Speed: {0}", с.SlowDown ()); }
444 Часть III. Дополнительные конструкции программирования на С# Импорт типов, которые определяют расширяющие методы В случае выделения набора статических классов, содержащих расширяющие методы, в уникальное пространство имен другие пространства имен в этой сборке используют стандартное ключевое слово using для импорта не только самих статических классов, но также и каждого из поддерживаемых расширяющих методов. Об этом следует помнить, поскольку если не импортировать явно корректное пространство имен, то расширяющие методы будут недоступны в таком файле кода С#. Хотя на первый взгляд может показаться, что расширяющие методы глобальны по своей природе, на самом деле они ограничены пространствами имен, в которых они определены, или пространствами имен, которые их импортируют. Таким образом, если поместить определения рассматриваемых статических классов (MyExtensions, TesterUtilClass и CarExtensions) в пространство имен MyExtensionMethods, как показано ниже: namespace MyExtensionMethods { static class MyExtensions static class TesterUtilClass static class CarExtensions } то другие пространства имен в проекте должны явно импортировать пространство MyExtensionMethods для получения расширяющих методов, определенных этими типами. Поэтому следующий код вызовет ошибку во время компиляции: // Единственная директива using, using System; namespace MyNewApp { class JustATest { void SomeMethod() { // Ошибка! Для расширения int методом Foo() необходимо // импортировать пространство имен MyExtensionMethods! int i=0; i.Foo (); } } } Поддержка расширяющих методов средством IntelliSense Учитывая тот факт, что расширяющие методы не определены буквально на расширяемом типе, при чтении кода есть шансы запутаться. Например, предположим, что имеется импортированное пространство имен, в котором определено несколько расширяющих методов, написанных кем-то из команды разработчиков. При написании своего кода вы создаете переменную расширенного типа, применяете операцию точки и обнаруживаете десятки новых методов, которые не являются членами исходного определения класса!
Глава 12. Расширенные средства языка С# 445 К счастью, средство IntelliSense в Visual Studio маркирует все расширяющие методы уникальным значком с изображением синей стрелки вниз (рис. 12.5). ..;ExtensionMethods. Program ^UseCerQ 1 } static void UseCar() { Car с ■ new Car()j 100% '] 4 ♦ Equals ♦ GetHashCode ♦ GetType ♦^ SlowDown V Speed V SpeedUp ♦ ToString (extension) void objectDisplayDef iningAssemblyO I Рис. 12.5. Отображение расширяющих методов в IntelliSense Если метод помечен этим значком, это означает, что он определен вне исходного определения класса, через механизм расширяющих методов. Исходный код. Проект ExtensionMethods доступен в подкаталоге Chapter 12. Построение и использование библиотек расширений В предыдущем примере производилось расширение функциональности различных типов (таких как System. Int32) для использования в текущем консольном приложении. Представьте, насколько было бы полезно построить библиотеку кода .NET, определяющую расширения, на которые могли бы ссылаться многие приложения. К счастью, сделать это очень легко. Подробности создания и конфигурирования специальных библиотек будут рассматриваться в главе 14; а пока, если хотите реализовать самостоятельно приведенный здесь пример, создайте проект библиотеки классов по имени MyExtensionLibrary. Затем переименуйте начальный файл кода С# на MyExtensions . cs и скопируйте определение класса MyExtensions в новое пространство имен: namespace MyExtensionsLibrary { // Не забудьте импортировать System.Reflection! public static class MyExtensions { // Та же реализация, что и раньше. public static void DisplayDefiningAssembly(this object obj) {...} // Та же реализация, что и раньше. public static int ReverseDigits(this int 1) } На заметку! Чтобы можно было экспортировать расширяющие методы из библиотеки кода .NET, определяющий их тип должен быть объявлен с ключевым словом public (вспомните, что по умолчанию действует модификатор доступа internal).
446 Часть III. Дополнительные конструкции программирования на С# После этого можно скомпилировать библиотеку и ссылаться на сборку MyExtensionsLibrary.dll внутри новых проектов .NET. Это позволит использовать новую функциональность System.Object и System. Int32 в любом приложении, которое ссылается на библиотеку. Чтобы проверить сказанное, создадим новый проект консольного приложения (по имени MyExtensionsLibraryClient) и добавим к нему ссылку на сборку MyExtensionsLibrary.dll. В начальном файле кода укажем на использование пространства имен MyExtensionsLibrary и напишем простой код, который вызывает эти новые методы на локальном значении int: using System; // Импортируем наше специальное пространство имен, using MyExtensionsLibrary; namespace MyExtensionsLibraryClient { class Program { static void Main(string[] args) { Console.WriteLine("***** Using Library with Extensions *****\n"); // Теперь эти расширяющие методы определены внутри внешней // библиотеки классов .NET. int mylnt = 987; mylnt.DisplayDefiningAssembly() ; Console.WriteLine("{0} is reversed to {1}", mylnt, mylnt.ReverseDigits()) ; Console.ReadLine(); } } } В Microsoft рекомендуют размещать типы, которые имеют расширяющие методы, в отдельной сборке (внутри выделенного пространства имен). Причина проста — сокращение сложности программной среды. Например, если вы напишете базовую библиотеку для внутреннего использования в компании, причем в корневом пространстве имен этой библиотеки определено 30 расширяющих методов, то в конечном итоге все приложения будут видеть эти методы в списках IntelliSense (даже если они и не нужны). Исходный код. Проекты MyExtensionsLibrary и MyExtensionsLibraryClient доступны в подкаталоге Chapter 12. Расширение интерфейсных типов через расширяющие методы Итак, было показано, каким образом расширять классы (а также структуры, которые следуют тому же синтаксису) новой функциональностью через расширяющие методы. Чтобы завершить исследование расширяющих методов С#, следует отметить, что новыми методами можно также расширять и интерфейсные типы; однако семантика такого действия определенно несколько отличается от того, что можно было бы ожидать. Создадим новое консольное приложение по имени Interf aceExtensions, а в нем — простой интерфейсный тип (IBasicMath), включающий единственный метод по имени Add (). Затем подходящим образом реализуем этот интерфейс в каком-нибудь типе класса (MyCalc). Например: // Определение обычного интерфейса на С#. interface IBasicMath {
Глава 12. Расширенные средства языка С# 447 int Add(int x, int у) ; } // Реализация IBasicMath. class MyCalc : IBasicMath { public int "Add (int x, int y) { return x + y; } } Теперь предположим, что доступ к коду с определением IBasicMath отсутствует, но к нему нужно добавить новый член (например, метод вычитания), чтобы расширить его поведение. Можно попробовать написать следующий распшряющий класс: static class MathExtensions { // Расширить IBasicMath методом вычитания? public static int Subtract(this IBasicMath ltf, int x, int y); } Однако такой код вызовет ошибку во время компиляции. В случае расширения интерфейса новыми членами должна также предоставляться реализация этих членов! Это кажется противоречащим самой идее интерфейсных типов, поскольку интерфейсы не включают реализации, а только определения. Тем не менее, класс MathExtensions должен быть определен следующим образом: static class MathExtensions { // Расширить IBasicMath этим методом с этой реализацией. public static int Subtract (this IBasicMath itf, int x, int y) { return x - y; Теперь может показаться, что допустимо создать переменную типу IBasicMath и непосредственно вызвать Substract (). Опять-таки, если бы такое было возможно (а на самом деле нет), то это нарушило бы природу интерфейсных типов .NET. На самом деле приведенный код говорит вот что: "Любой класс в моем проекте, реализующий интерфейс IBasicMath, теперь имеет метод Substract (), реализованный представленным образом". Как и раньше, все базовые правила соблюдаются, а потому пространство имен, определяющее MyCalc, должно иметь доступ к пространству имен, определяющему MathExtensions. Рассмотрим следующий метод Main (): static void Main(string[] args) { Console.WriteLine("***** Extending an interface *****\n"); // Вызов членов IBasicMath из объекта MyCalc. MyCalc с = new MyCalc (); Console.WriteLine ( + 2 = {0}", c.Addd, 2) ) ; Console.WriteLine( - 2 = {0}", с.SubtractA, 2) ) ; // Для вызова расширения можно выполнить приведение к IBasicMath. Console.WriteLine(0 - 9 = {0}", ((IBasicMath)с) .Subtract C0, 9)); // Это не будет работать! // IBasicMath ltfBM = new IBasicMath(); // ltfBM.SubtractA0, 10); Console.ReadLine();
448 Часть III. Дополнительные конструкции программирования на С# На этом исследование расширяющих методов С# завершено. Помните, что это конкретное языковое средство может быть очень полезным, когда нужно расширить функциональность типа, даже если нет доступа к первоначальному исходному коду (или если тип запечатан), в целях поддержания полиморфизма. Во многом подобно неявно типизированным локальным переменным, расширяющие методы являются ключевым элементом работы с API-интерфейсом LINQ. Как будет показано в следующей главе, множество существующих типов в библиотеках базовых классов расширены новой функциональностью через расширяющие методы, что позволяет им интегрироваться в программную модель LINQ. Исходный код. Проект InterfaceExtension доступен в подкаталоге Chapter 12. Понятие частичных методов Начиная с версии .NET 2.0, строить определения частичных классов стало возможно с использованием ключевого слова partial (см. главу 5). Вспомните, что эта деталь синтаксиса позволяет разбивать полную реализацию типа на несколько файлов кода (или других мест, таких как память). До тех пор, пока каждый аспект частичного типа имеет одно полностью квалифицированное имя, конечным результатом будет "нормальный" скомпилированный класс, находящийся в созданной компилятором сборке. Язык С# расширяет роль ключевого слова partial, позволяя его применять на уровне метода. По сути, это дает возможность прототипировать метод в одном файле, а реализовать в другом. При наличии опыта работы в C++, это может напомнить отношения между файлами заголовков и реализаций C++. Тем не менее, частичные методы С# обладают рядом важных ограничений. • Частичные методы могут определяться только внутри частичного класса. • Частичные методы должны возвращать void. • Частичные методы могут быть статическими или методами экземпляра. • Частичные методы могут иметь аргументы (включая параметры с модификаторами this, ref или params, но не с out). • Частичные методы всегда неявно приватные (private). Еще более странным является тот факт, что частичный метод может быть как помещен, так и не помещен в скомпилированную сборку! Для прояснения картины давайте рассмотрим пример. Первый взгляд на частичные методы Для оценки влияния определения частичного метода создадим проект консольного приложения по имени PartialMethods. Затем определим новый класс CarLocator внутри файла С# по имени CarLocator. cs: // CarLocator.es partial class CarLocator { // Этот член всегда будет частью класса CarLocator. public bool CarAvailablelnZipCode (string zipCode) { // Этот вызов *может* быть частью реализации данного метода. VerifyDuplicates(zipCode); // Некоторая интересная логика взаимодействия с базой данных... return true; }
Глава 12. Расширенные средства языка С# 449 // Этот член *может* быть частью класса CarLocator! partial void VerifyDuplicates(string make); } Обратите внимание, что метод Verif yDuplicates () определен с модификатором partial и не имеет определения тела внутри этого файла. Кроме того, метод CarAvailablelnZipCode () содержит вызов Verif yDuplicates () внутри своей реализации. Скомпилировав это приложение в таком, как оно есть виде, и открыв скомпилированную сборку в утилите ildasm.exe или refatcor.exe, вы не обнаружите там никаких следов Verif yDuplicates () в классе CarLocator, равно как никаких вызовов Verif yDuplicates () внутри CarAvailablelnZipCode ()! С точки зрения компилятора в данном проекте класс CarLocator определен в следующем виде: internal class CarLocator { public bool CarAvailablelnZipCode(string zipCode) { return true; } } Причина столь странного усечения кода связана с тем, что частичный метод Verif yDuplicates () не имеет реальной реализации. Добавив в проект новый файл (например, CarLocatorlmpl .cs) с определением остальной порции частичного метода: // CarLocatorlmpl.cs partial class CarLocator { partial void VerifyDuplicates(string make) { // Assume some expensive data validation // takes place here.. . } } вы обнаружите, что во время компиляции будет принят во внимание полный комплект класса CarLocator, как показано в следующем примерном коде С#: internal class CarLocator { public bool CarAvailablelnZipCode(string zipCode) { this.VerifyDuplicates(zipCode); return true; } private void VerifyDuplicates(string make) { } } Как видите, когда метод определен с ключевым словом partial, то компилятор принимает решение о том, нужно ли его включить в сборку, в зависимости от наличия у этого метода тела или же просто пустой сигнатуры. Если у метода нет тела, все его упоминания (вызовы, описания метаданных, прототипы) на этапе компиляции отбрасываются. В некоторых отношениях частичные методы С# — это строго типизированная версия условной компиляции кода (через директивы препроцессора #if, #elif и #endif). Однако основное отличие состоит в том, что частичный метод будет полностью проиг-
450 Часть III. Дополнительные конструкции программирования на С# норирован во время компиляции (независимо от настроек сборки), если отсутствует его соответствующая реализация. Использование частичных методов Учитывая ограничения, присущие частичным методам, самое важное из которых связано с тем, что они должны быть неявно private и всегда возвращать void, сразу представить множество полезных применений этого средства языка может быть трудно. По правде говоря, из всех языковых средств С# частичные методы кажутся наименее востребованными. В текущем примере метод Verif yDuplicates () помечен как частичный в демонстрационных целях. Однако предположим, что этот метод, будучи реализованным, выполняет некоторые очень интенсивные вычисления. Снабжение этого метода модификатором partial дает возможность другим разработчикам классов создавать детали реализации по своему усмотрению. В данном случае частичные методы предоставляют более ясное решение, чем применение директив препроцессора, поддерживая "фиктивные" реализации виртуальных методов либо генерируя объекты исключений NotlmplementedException. Наиболее распространенным применением этого синтаксиса является определение так называемых легковесных событий. Эта техника позволяет проектировщикам классов представлять привязки для методов, подобно обработчикам событий, относительно которых разработчики могут самостоятельно решать — реализовывать их или нет. В соответствии с принятым соглашением, имена таких методов-обработчиков легковесных событий имеют префикс On. Например: // CarLocator.EventHandler.cs partial class CarLocator { public bool CarAvailablelnZipCode(string zipCode) { OnZipCodeLookup(zipCode); return true; > // Обработчик "легковесного" события, partial void OnZipCodeLookup(string make); } Если разработчик класса пожелает получать уведомления о вызове метода CarAvailablelnZipCode (), он может предоставить реализацию метода OnZipCodeLookup (). В противном случае ничего делать не потребуется. Исходный код. Проект PartialMethods доступен в подкаталоге Chapter 12. Понятие анонимных типов Как объектно-ориентированный программист, вы знаете преимущества классов в отношении представления состояния и функциональности заданной программной сущности. То есть, когда нужно определить класс, который предполагает многократное использование и предоставляет обширную функциональность через набор методов, событий, свойств и специальных конструкторов, то разработка нового класса С# является общепринятой и зачастую обязательной практикой.
Глава 12. Расширенные средства языка С# 451 Однако есть и другие случаи, когда может понадобиться определить класс просто для моделирования набора инкапсулированных (и каким-то образом связанных) элементов данных, без ассоциированных с ними методов, событий или другой специальной функциональности. Более того, что если этот тип будет использоваться только внутри текущего приложения, и не предназначен для многократного применения? Для такого "временного" типа ранние версии С# все равно требовали построения вручную нового определения класса: internal class SomeClass { // Определить набор приватных переменных-членов... // Создать свойство для каждой приватной переменной. . . // Переопределить ToStringO для учета каждой переменной-члена... // Переопределить GetHashCode() и Equals () для работы // с эквивалентностью на основе значений.. . } Хотя само по себе построение такого класса — не сверхсложная задача, но инкапсуляция изрядного количества членов часто оказывается довольно трудоемкой (хотя здесь в некоторой степени помогают автоматические свойства). Теперь доступно замечательное сокращение для подобных ситуаций (анонимные типы), которое во многих отношениях является естественным расширением синтаксиса анонимных методов С# (см. главу 11). Анонимный тип определяется с использованием ключевого слова var (см. главу 3) в сочетании с синтаксисом инициализации объекта (см. главу 5). Для иллюстрации создадим новое консольное приложение по имени AnonymousTypes. Модифицируем метод Main (), добавив следующий анонимный класс, который моделирует простой тип автомобиля: static void Main(string[] args) { Console.WriteLine ("***** Fun with Anonymous Types *****\n"); // Создать анонимный тип, представляющий автомобиль. var myCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed =55 }; // Вывести на консоль цвет и производителя. Console.WriteLine("My car is a {0} {1}.", myCar.Color, myCar.Make); Console.ReadLine(); } Обратите внимание, что переменная myCar должна быть типизирована неявно, что вполне разумно, поскольку мы не моделируем концепцию автомобиля с использованием строго типизированного определения класса. Во время компиляции компилятор С# автоматически сгенерирует уникально именованный класс. Учитывая тот факт, что это имя класса невидимо в коде С#, применение неявной типизации посредством ключевого слова var обязательно. Кроме того, также нужно указать (с помощью синтаксиса инициализации объектов) набор свойств, моделирующих данные, которые должны быть инкапсулированы. Однажды определенные, эти значения могут быть получены с применением стандартного синтаксиса вызова свойств С#. Внутреннее представление анонимных типов Все анонимные типы автоматически наследуются от System.Object и потому поддерживают все члены, предоставленные этим базовым классом. Учитывая это, можно вызывать ToString () , GetHashCode () , Equals () или GetType () на неявно типизированном объекте myCar. Предположим, что в классе Program определена следующая статическая вспомогательная функция:
452 Часть III. Дополнительные конструкции программирования на С# static void ReflectOverAnonymousType(object obj) { Console.WriteLine("obj is an instance of: {0}", obj.GetType().Name); Console.WriteLine("Base class of {0} is {1}", Ob] . GetType () . Name, ob;j . GetType () . BaseType) ; Console.WriteLine("obj.ToStringO = {0}", ob].ToString()); Console.WriteLine("obj.GetHashCode() = {0}", obj.GetHashCode() ) ; Console.WriteLine(); } Теперь вызовем этот метод в Main(), передав ему объект myCar в качестве параметра: static void Main(string[] args) { Console.WriteLine ("***** Fun with Anonymous Types *****\n"); // Создать анонимный тип, представляющий автомобиль. var myCar = new {Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55}; // Отобразить то, что сгенерировал компилятор. ReflectOverAnonymousType(myCar); Console.ReadLine(); } Вывод будет выглядеть примерно так: ••••• pun W1th Anonymous Types ***** obj is an instance of: of AnonymousTypeOv3 Base class of of AnonymousTypeO ч 3 is System. Object ob] .ToString () = { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 } obj.GetHashCode () =2038548792 Прежде всего, обратите внимание, что объект myCar имеет тип of AnonymousTypeO ч 3 (конкретное имя типа может отличаться). Помните, что назначаемое типу имя полностью определяется компилятором и напрямую в коде С# недоступно. Возможно, наиболее важно здесь то, что каждая пара "имя/значение", определенная с использованием синтаксиса инициализации объектов, отображается на идентично именованное свойство, доступное только для чтения, и соответствующее приватное поле заднего плана, также предназначенное только для чтения. Следующий код С# примерно отражает сгенерированный компилятором класс, используемый для представления объекта myCar (который можно увидеть с помощью утилиты reflector. ехе или ildasm. exe): internal sealed class of AnonymousTypeO<<Color>] TPar, <Make>j TPar, <CurrentSpeedy TPar> { // Поля только для чтения private readonly <Color>] TPar <Color>i Field; private readonly <CurrentSpeed>] TPar <CurrentSpeed>i Field; private readonly <Make>j TPar <Make>i Field; // Конструктор по умолчанию public of AnonymousTypeO (<Color>] TPar Color, <Make>] TPar Make, <CurrentSpeed>j TPar CurrentSpeed); // Переопределенные методы public override bool Equals(object value); public override int GetHashCode(); public override string ToStringO; // Свойства только для чтения public <Color>] TPar Color { get; } public <CurrentSpeed>j TPar CurrentSpeed { get; } public <Make>j TPar Make { get; }
Глава 12. Расширенные средства языка С# 453 Реализация методов ToStr±ng() и GetHashCode () Все анонимные типы автоматически наследуются от System. Ob j ect и предоставляют переопределенные версии методов Equals (), GetHashCode () и ToString (). Реализация ToString () просто строит строку из каждой пары "имя/значение". Например: public override string ToString() { StringBuilder builder = new StringBuilder (); builder.Append("{ Color = ") ; builder.Append(this.<Color>i Field); builder.Append(", Make = ") ; builder.Append(this.<Make>i Field); builder.Append(", CurrentSpeed = ") ; builder.Append(this.<CurrentSpeed>i Field); builder.Append(" }"); return builder.ToString (); } Реализация GetHashCode () вычисляет хеш-значение, используя каждую переменную- член анонимного типа в качестве входной для типа System.Collections . Generic . EqualityComparer<T>. Используя эту реализацию GetHashCode (), два анонимных типа породят одинаковое хеш-значение тогда (и только тогда), когда они имеют одинаковый набор свойств, которым присвоены одинаковые значения. При такой реализации анонимные типы хорошо подходят для помещения в контейнер Hashtable. Семантика эквивалентности анонимных типов Хотя реализация переопределенных методов ToString () и GetHashCode () достаточно проста, реализация метода Equals () может вызвать вопросы. Например, если определено две переменных "анонимных автомобилей" с одинаковым набором пар "имя/ значение", должны ли эти переменные трактоваться как эквивалентные? Чтобы увидеть результат такого сравнения, дополним класс Program следующим новым методом: static void EqualityTest () { // Создать два анонимных класса с идентичным набором пар "имя/значение". var firstCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55 }; var secondCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed =55 }; // Считать ли их эквивалентными на основе использования Equals()? if (firstCar.Equals(secondCar) ) Console.WriteLine("Same anonymous object!"); // один и тот же объект else Console.WriteLine("Not the same anonymous object!"); // разные объекты // Можно ли проверить их эквивалентность с помощью ==? if (firstCar == secondCar) Console.WriteLine("Same anonymous object1"); else Console.WriteLine ("Not the same anonymous object1"); // Имеют ли эти объекты в основе одинаковый тип? if (firstCar.GetType() .Name == secondCar.GetType () .Name) Console. WriteLine ("We are both the same type1"); // один и тот же тип else Console.WriteLine("We are different types!"); // разные типы // Отобразить все детали. Console.WriteLine(); ReflectOverAnonymousType(firstCar); ReflectOverAnonymousType(secondCar); }
454 Часть III. Дополнительные конструкции программирования на С# На рис. 12.6 показан (несколько неожиданный) вывод, полученный в результате вызова этого метода в Main (). ■В C:\Windo*s\$ystem32\cmd.e« Same anonymous obje Not the same anonym We are both the sain obj is an instance of: of__AnonymousTypeO*3 ■Base class of of AnonymousTypeO is System.Object lobi.ToStringO = { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 } ■obj.GetHashCodeO = 2038548792 bobj is an instance of: of AnonymousTypeO*3 ■Base class of of AnonymousTypeO*3 is System.Object lobj.ToStringO = { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 } ■obj.GetHashCodeO = 2Q38548792 ■Press any key to continue . . . Рис. 12.6. Эквивалентность анонимных типов После запуска этого тестового кода вы увидите, что первая проверка, при которой вызывается Equals (), возвращает true, и потому на консоль выводится сообщение "Same anonymous object! ". Причина в том, что сгенерированный компилятором метод Equals () при проверке эквивалентности использует семантику на основе значений (т.е. проверяет значения каждого поля сравниваемого объекта). Однако вторая проверка (в которой используется операция ==) приводит к выводу на консоль строки "Not the same anonymous object! ", что на первый взгляд выглядит несколько нелогично. Такой результат объясняется тем, что анонимные типы не получают перегруженной версии операций проверки равенства (== и ! =). Поэтому при проверке эквивалентности объектов анонимных типов с использованием операций равенства С# (вместо метода Equals ()), то проверяются ссылки, а не значения, поддерживаемые объектами. И последнее (по порядку, но не значению): финальная проверка (где проверяется имя лежащего в основе типа) показывает, что экземпляры анонимных типов относятся к одному и тому же сгенерированному компилятором типу класса (в данном примере — of AnonymousTypeO ч 3), потому что f irstCar и secondCar имеют одинаковый набор свойств (Color, Make и CurrentSpeed). Это иллюстрирует важный, но тонкий момент: компилятор генерирует определение нового класса тогда, когда анонимный тип имеет уникальные имена свойств. Поэтому в случае объявления идентичных анонимных типов (в смысле — с одинаковыми именами) в одной сборке компилятор сгенерирует только одно определение анонимного типа. Анонимные типы, содержащие другие анонимные типы Можно создавать анонимные типы, состоящие из других анонимных типов. Например, предположим, что необходимо смоделировать заказ на покупку, который состоит из временной метки, цены и приобретаемого автомобиля. Ниже показан новый (несколько более сложный) анонимный тип, представляющий эту сущность: // Создать анонимный тип, состоящий из другого анонимного типа, var purchaseltem = new { TimeBought = DateTime.Now, ItemBought = new {Color = "Red", Make = "Saab", CurrentSpeed =55}, Price = 34.000} ; ReflectOverAnonymousType(purchaseltem);
Глава 12. Расширенные средства языка С# 455 К этому моменту синтаксис, используемый для определения анонимных типов, должен быть понятным, но, скорее всего, остался главный вопрос: где и когда может понадобиться это новое языковое средство? Если кратко, то объявления анонимных типов следует применять сдержанно, обычно только в сочетании с набором технологий LINQ (см. главу 14). Никогда не отказывайтесь от использования строго типизированных классов и структур просто потому, что это возможно, учитывая многочисленные ограничения анонимных типов, которые перечислены ниже. 1. Контроль над именами анонимных типов отсутствует. 2. Анонимные типы всегда расширяют System.Object. 3. Поля и свойства анонимного типа всегда доступны только для чтения. 4. Анонимные типы не могут поддерживать события, специальные методы, специальные операции или специальные переопределения. 5. Анонимные типы всегда неявно запечатаны. 6. Анонимные типы всегда создаются с использованием конструктора по умолчанию. Тем не менее, при программировании с использованием набора технологий LINQ вы обнаружите, что во многих случаях этот синтаксис оказывается очень полезным, когда нужно быстро смоделировать общую форму сущности, а не ее функциональность. Исходный код. Проект AnonymousTypes доступен в подкаталоге Chapter 12. Работа с типами указателей Последняя тема настоящей главы касается средства С#, которое наименее часто используется в подавляющем большинстве проектов. В главе 4 было указано, что в .NET определены две основных категории типов данных: типы значения и ссылочные типы. На самом деле существует еще и третья категория: типы указателей. Чтобы можно было работать с типами указателей, в табл. 12.3 описаны специфические ключевые слова и операции, которые позволят обойти схему управления памятью CLR и взять бразды правления в собственные руки. Таблица 12.3. Операции и ключевые слова С#, связанные с указателями Операция/ назначение ключевое слово * Эта операция используется для создания переменной-указателя (те. переменной, представляющей непосредственное расположение в памяти). Как и в С (C++), та же самая операция служит для разыменования указателя. & Эта операция используется для получения адреса переменной в памяти. -> Эта операция используется для доступа к полям типа, представленного указателем (небезопасной версией операции точки С#). [ ] Операция [ ] (в небезопасном контексте) позволяет индексировать слот, заданный переменной-указателем (вспомните взаимосвязь между переменной- указателем и операцией [ ] в С (C++)). ++, — В небезопасном контексте операции инкремента и декремента могут применяться к типам указателей.
456 Часть III. Дополнительные конструкции программирования на С# Окончание табл. 12.3 Операция/ ключевое слово Назначение +, - В небезопасном контексте операции сложения и вычитания могут применяться к типам указателей. ==, ! =, <, >, В небезопасном контексте операции сравнения и эквивалентности могут при- <=, >= меняться к типам указателей. stackalloc В небезопасном контексте ключевое слово stackalloc может быть использовано для размещения массивов С# непосредственно в стеке. fixed В небезопасном контексте ключевое слово fixed может быть использовано для временной фиксации переменной, чтобы можно было определить ее адрес. Перед погружением в детали следует учесть, что вам очень редко, если вообще понадобится использовать типы указателей. Хотя С# позволяет перейти на уровень прямых манипуляций указателями, знайте, что исполняющая система .NET не имеет абсолютно никакого представления о ваших намерениях. Поэтому, если вы произведете неверное действие с указателем, то сами будете отвечать за последствия. С учетом этого предупреждения возникает вопрос: когда вообще может понадобиться работа с типами указателей? Есть только две такие ситуации. • Нужно оптимизировать некоторые части приложения, напрямую обращаясь к его членам вне управления CLR. • Требуется вызывать методы из динамической библиотеки (* . dll), написанной на С, или же из сервера СОМ, который требует в качестве параметров типы указателей. Но даже в этом случае часто можно обойтись без использования типов указателей, предпочтя им тип System. IntPtr и члены типа System.Runtime . InteropServices.Marshal. В случае если все же решено применять это средство языка С#, понадобится информировать компилятор С# (csc.exe) о своих намерениях, позволив проекту поддерживать "небезопасный" (unsafe) код. Чтобы сделать это в командной строке, добавьте при вызове компилятора флаг /unsafe: esc /unsafe *.cs В среде Visual Studio 2010 нужно будет перейти на страницу Properties (Свойства) проекта и на вкладке Build (Сборка) отметить флажок Allow Unsafe Code (Разрешить небезопасный код), как показано на рис. 12.7. Чтобы поэкспериментировать с типами указателей, создадим новое консольное приложений по имени Unsaf eCode и разрешим небезопасный код. На заметку! В последующих примерах предполагается наличие некоторого опыта манипуляций указателями в С (C++). Если это не так, можете просто полностью пропустить этот раздел. Написание небезопасного кода не является распространенной практикой в большинстве приложений С#. Ключевое слово unsafe Если необходимо работать с указателями в С#, следует специально объявить блок "небезопасного" кода с помощью ключевого слова unsafe (любой код, который не помечен ключевым словом unsafe, автоматически считается "безопасным").
Глава 12. Расширенные средства языка С# 457 UnsafeCode* X Application j Build* Build Events Debug Resources Services Settings Reference Paths Configuration: [Active(Debug) ▼[ Platform: Active 1x86) У Define TRACE constant Platform target ;V) Allow unsafe code L] Optimiiecode Errors and warnings Warning level: Рис. 12.7. Включение поддержки небезопасного кода в Visual Studio 2010 Например, в следующем классе Program объявляется область небезопасного кода внутри метода Main (): class Program { static void Main(string[] args) { unsafe { // Здесь работаем с указателями! } // Здесь нельзя работать с указателями! } } В дополнение к объявлению области небезопасного кода внутри метода, можно строить "небезопасные" структуры, классы, члены типов и параметры. Ниже приведено несколько примеров (эти типы в текущем проекте определять не нужно): // Вся эта структура "небезопасна" и может // использоваться только в небезопасном контексте. public unsafe struct Node { public int Value; public Node* Left; public Node* Right; } // Структура безопасна, но члены Node2* - нет. // Технически можно обратиться к Value извне // небезопасного контекста, но не к Left и Right. public struct Node2 { public int Value; // Это доступно только из небезопасного контекста! public unsafe Node2* Left; public unsafe Node2* Right; } Методы (статические и уровня экземпляра) также могут быть помечены как небезопасные. Предположим, известно, что определенный статический метод будет использовать логику указателей. Чтобы обеспечить возможность вызова этого метода только из небезопасного контекста, этот метод можно определить следующим образом:
458 Часть III. Дополнительные конструкции программирования на С# unsafe static void SquarelntPointer (int* mylntPointer) { // Возведем значение в квадрат — просто для целей тестирования. *myIntPointer *= *myIntPointer; } Конфигурация метода требует, чтобы вызывающий код обращался к методу SquarelntPointer (), как показано ниже: static void Main(string[] args) { unsafe { int mylnt = 10; // Нормально, мы в небезопасном контексте. SquarelntPointer(&mylnt); Console.WriteLine("mylnt: {0}" , mylnt); } int mylnt2 = 5; // Ошибка компиляции! Должен быть небезопасный контекст! SquarelntPointer(&mylnt2); Console.WriteLine("mylnt: {0}", mylnt2); } Если вы не хотите заставлять вызывающий код помещать этот вызов в оболочку небезопасного контекста, можете пометить весь метод Main () ключевым словом unsafe. В этом случае следующий код скомпилируется: unsafe static void Main(string [ ] args) { int mylnt2 = 5; SquarelntPointer(&mylnt2); Console.WriteLine ("mylnt: {0}", mylnt2); Работа с операциями * и & Установив небезопасный контекст, можно строить указатели и типы данных, использующие операцию *, а также получать адрес указателя с помощью операции &. В отличие от С или C++, в языке С# операция * применяется только к лежащему в основе типу, а не является префиксом имени каждой переменной указа геля. Например, рассмотрим следующий код, который иллюстрирует правильный и неправильный способы объявления указателей на целочисленные переменные: // Нет1 В С# это неправильно1 int *pi, *pj; // Да! Так правильно в С#. int* pi, pj; Рассмотрим следующий небезопасный метод: unsafe static void PrintValueAndAddress () { int mylnt; // Определить указатель на int и присвоить ему адрес mylnt. int* ptrToMylnt = &mylnt; // Присвоить значение mylnt, используя обращение через указатель. *ptrToMyInt = 123; // Вывести на консоль некоторые значения. Console.WriteLine("Value of mylnt {0}", mylnt); Console.WriteLine("Address of mylnt {0:X}", (int)&ptrToMy!nt);
Глава 12. Расширенные средства языка С# 459 Небезопасная и безопасная функция обмена значений Разумеется, объявление указателей на локальные переменные только для того, чтобы присвоить им значения (как в предыдущем примере), никогда не понадобится и вообще неудобно. Чтобы проиллюстрировать более практичный пример небезопасного кода, предположим, что нужно построить функцию обмена с использованием арифметики указателей: unsafe public static void UnsafeSwap(int* 1, int* j) { int temp = *i; *i = *j; *j = temp; } Очень похоже на язык С, не правда ли? Однако если вы читали главу 4, то в состоянии написать следующую безопасную версию алгоритма обмена с применением ключевого слова ref: public static void SafeSwap(ref int 1, ref int j) { int temp = i; i = ц; j = temp; } Функциональность обеих версий метода идентична, а это доказывает, что прямые манипуляции указателями в С# вовсе не обязательны. Ниже показана логика вызова с использованием безопасного Main (), но с небезопасным контекстом: static void Main(string[] args) { Console.WriteLine ("***** Calling method with unsafe code *****"); // Значения, подлежащие обмену. int l = 10, j = 20; // "Безопасный" обмен значений. Console.WriteLine("\n***** Safe swap *****"); Console .WriteLine ("Values before safe swap: 1 = {0}, j = {1}", i, j) ; SafeSwap(ref 1, ref j); Console .WriteLine ("Values after safe swap: 1 = {0}, j = {1}", i, j); // "Небезопасный" обмен значений. Console.WriteLine("\n***** Unsafe swap *****"); Console.WriteLine ("Values before unsafe swap: i = {0}, j = {1}", l, j); unsafe { UnsafeSwap (&i, &]); } Console.WriteLine("Values after unsafe swap: l = {0}, j = {1}", l, j) ; Console.ReadLine() ; } Доступ к полям через указатели (операция ->) Теперь предположим, что определена простая безопасная структура Point: struct Point { public int x; public int y; public override string ToStringO { return string.Format ( " ({0}, {1})", x, y) ; } }
460 Часть III. Дополнительные конструкции программирования на С# При объявлении указателя на тип Point для доступа к общедоступным членам структуры понадобится применять операцию доступа к полю (в виде ->). Как показано в табл. 12.3, это — небезопасная версия стандартной (безопасной) операции точки (.). Фактически, используя операцию обращения к указателю (*), можно разыменовать указатель и вновь применить операцию точки. Рассмотрим следующий небезопасный метод: unsafe static void UsePointerToPoint () { // Доступ к членам через указатель. Point point; Point* p = &point; р->х = 100; р->у = 200; Console.WriteLine(p->ToString() ) ; // Доступ к членам через разыменованный указатель. Point point2; Point* p2 = &point2; (*р2).х = 100; (*р2) .у = 200; Console.WriteLine ( (*p2) .ToStringO ) ; } Ключевое слово stackalloc В небезопасном контексте может понадобиться объявить локальную переменную, выделяющую память непосредственно в стеке вызовов (и потому недоступной для системы сборки мусора .NET). Для этого в С# предусмотрено ключевое слово stackalloc, которое является С#-эквивалентом функции all оса библиотеки времени выполнения С. Ниже приведен простой пример: unsafe static void UnsafeStackAlloc () { char* p = stackalloc char[256]; for (int k = 0; k < 256; k++) p[k] = (char)k; } Закрепление типа ключевым словом fixed Как было показано в предыдущем примере, выделение фрагмента памяти в пределах небезопасного контекста, может быть осуществлено с помощью ключевого слова stackalloc. Ввиду природы этой операции, выделенная память очищается, как только метод, который ее выделил, возвращает управление (поскольку память распределена в стеке). Однако рассмотрим более сложный пример. Во время экспериментов с операцией -> был создан тип значения по имени Point. Подобно всем типам значений, выделенная его экземплярам память выталкивается из стека, как только завершается контекст выполнения. Предположим, что вместо этого структура Point определена как ссылочный тип: class PointRef // Переименовано и переписано. { public int х; public int у; public override string ToStringO { return string.Format (" ({0}, {1})", x, y) ; } }
Глава 12. Расширенные средства языка С# 461 Как вам хорошо известно, если теперь в вызывающем коде объявить переменную типа Point, то память будет выделена в куче, подверженной сборке мусора. И тут возникает животрепещущий вопрос: а что если небезопасный контекст пожелает взаимодействовать, с этим объектом (или любым другим объектом из кучи)? Учитывая, что сборка мусора может произойти в любой момент, представьте проблему, с которой вы столкнетесь при обращении к членам Point в тот момент, когда происходит реорганизация кучи. Теоретически может случиться так, что небезопасный контекст попытается взаимодействовать с членом, который уже недоступен или был перемещен в куче после ее реорганизации (что является очевидной проблемой). Для фиксации переменной ссылочного типа в памяти из небезопасного контекста в С# предусмотрено ключевое слово fixed. Оператор fixed устанавливает указатель на управляемый тип и закрепляет эту переменную на время выполнения оператора. Без fixed от указателей на управляемые переменные было бы мало пользы, поскольку сборка мусора может перемещать переменные в памяти непредсказуемым образом. (Фактически компилятор С# не позволит установить указатель на управляемую переменную без оператора fixed.) Таким образом, если вы создадите тип Point (теперь в виде класса) и захотите взаимодействовать с его членами, то должны будете написать следующий код (или, в противном случае, получить ошибку этапа компиляции): unsafe public static void UseAndPinPoint () { PointRef pt = new PointRef (); pt.x = 5; pt.y = 6; // Закрепить указатель pt на месте, чтобы он не мог быть // перемещен или собран сборщиком мусора. fixed (int* p = &pt.x) { // Использовать здесь переменную int*1 } // Указатель pt теперь не закреплен и готов к сборке мусора. Console.WriteLine ("Point is: {O}11, pt) ; } По сути, ключевое слово fixed позволяет строить оператор, блокирующий ссылочную переменную в памяти, чтобы ее адрес оставался постоянным на протяжении работы оператора. Поэтому, взаимодействуя со ссылочным типом из контекста небезопасного кода, нужно обязательно фиксировать ссылку. Ключевое слово sizeof И последнее ключевое слово С#, связанное с небезопасным кодом — это sizeof. Как и в языке С (C++), ключевое слово sizeof в С# служит для получения размера в байтах типа значения (но никогда — ссылочного типа), и может применяться только внутри небезопасного контекста. Как и можно было представить, эта возможность может пригодиться при взаимодействии с неуправляемыми API-интерфейсами на базе С. Его применение очевидно: unsafe static void UseSizeOfOperator () { Console.WriteLine("The size of short is {0 } . ", sizeof (short)); // размер short Console.WriteLine("The size of int is {0}.", sizeof (int)); // размер int Console.WriteLine("The size of long is {0}.", sizeof (long)); // размер long }
462 Часть III. Дополнительные конструкции программирования на С# Поскольку sizeof вычисляет количество байт для любой сущности, унаследованной от System. ValueType, также можно получить размер пользовательских структур. Например, передать структуру Point в sizeof можно следующим образом: unsafe static void UseSizeOfOperator () { Console.WriteLine("The size of Point is {0}.", sizeof (Point)); } Исходный код. Проект UnsafeCode доступен в подкаталоге Chapter 12. На этом обзор наиболее мощных средств языка программирования С# завершен. В следующей главе будет продемонстрировано применение некоторых из этих концепций (а именно — расширяющих методов и анонимных типов) в контексте LINQ to Objects. Резюме Целью этой главы было углубление знаний языка программирования С#. Мы начали с исследования различных применения развитых конструкций (методов-индексаторов, перегруженных операций и процедур специального преобразования типов). Затем была рассмотрена роль расширяющих методов, анонимных типов и частичных методов. Как будет показано в следующей главе, эти средства очень полезны при работе с API-интерфейсами LINQ (хотя их можно использовать повсюду в коде, если это покажется удобным). Вспомните, что анонимные методы позволяют быстро моделировать "форму" типа, в то время как расширяющие методы позволяют добавлять новую функциональность к типам, без необходимости определения подклассов. Финальная часть главы была посвящена рассмотрению небольшого набора малоизвестных ключевых слов (sizeof, checked, unsafe и т.п.), а попутно была продемонстрирована работа с низкоуровневыми типами указателей. Как было установлено в процессе рассмотрения этих типов, в большинстве приложений С# никогда не понадобится их использовать.
ГЛАВА 13 LINQ to Objects Независимо от типа приложения, которое вы создаете с использованием платформы .NET, ваша программа определенно нуждается в доступе к некоторой форме данных в процессе выполнения. Данные могут находиться в самых разных местах, включая файлы XML, реляционные базы данных, коллекции в памяти, элементарные массивы. Исторически сложилось, что в зависимости от места хранения данных программистам приходилось использовать очень разные и никак не связанные API- интерфейсы. Набор технологий LINQ (Language Integrated Query — язык интегрированных запросов), появившийся в .NET 3.5, предоставил краткий, симметричный и строго типизированный способ доступа к широкому разнообразию хранилищ данных. В этой главе начинается изучение LINQ с рассмотрения LINQ to Objects. Прежде чем погрузиться в LINQ to Objects, в первой части этой главы мы кратко просмотрим ключевые программные конструкции С#, которые обеспечили возможность существования LINQ. По мере чтения главы вы убедитесь, насколько полезны такие средства, как неявно типизированные переменные, синтаксис инициализации объектов, лямбда-выражения, расширяющие методы и анонимные типы. После ознакомления с поддерживающей инфраструктурой в оставшемся материале главы будет представлена модель LINQ и роль, которую она играет в платформе .NET. Вы узнаете назначение операций и выражений запросов, которые позволяют определять операторы, опрашивающие источник данных для выдачи запрошенного результирующего набора. Попутно рассматриваются многочисленные примеры применения LINQ, которые иллюстрируют взаимодействие с данными в массивах и коллекциях различного типа (как обобщенных, так и необобщенных), а также сборки, пространства имен и типы, представляющие API-интерфейс LINQ to Objects. На заметку! Предлагаемые в этой главе сведения послужат фундаментов для освоения последующих глав книги, в которых описаны дополнительные технологии LINQ, включая LINQ to XML (глава 25), Parallel LINQ (глава 19) и LINQ to Entities (глава 23). Программные конструкции, специфичные для LINQ На самом высоком уровне LINQ можно воспринимать как строго типизированный язык запросов, встроенный непосредственно в грамматику самого языка С#. Используя LINQ, можно строить любое количество выражений, которые выглядят и ведут себя подобно SQL-запросам к базе данных. Однако запрос LINQ может применяться к любому числу хранилищ данных, включая хранилища, которые не имеют ничего общего с истинными реляционными базами данных.
464 Часть III. Дополнительные конструкции программирования на С# На заметку! Хотя запросы LINQ внешне похожи на запросы SQL, их синтаксис не идентичен. Фактически многие запросы LINQ выглядят прямой противоположностью формата запроса к базе данных! Если вы попытаетесь отобразить LINQ непосредственно на SQL, то определенно будете разочарованы. Чтобы это не случилось, рекомендуется воспринимать запросы LINQ как уникальную сущность, которая лишь случайно похожа на SQL. Когда LINQ впервые был представлен в составе платформы .NET 3.5, языки С# (и VB) уже были снабжены огромным количеством программных конструкций для поддержки набора технологий LINQ. В частности, язык С# использует следующие связанные с LINQ средства: • неявно типизированные локальные переменные; • синтаксис инициализации объектов и коллекций; • лямбда-выражения; • расширяющие методы; • анонимные типы. Эти средства уже детально рассматривались в различных главах настоящей книги. Однако чтобы освежить все в памяти, давайте быстро рассмотрим каждое из средств по очереди, просто чтобы удостоверится в правильном их понимании. Неявная типизация локальных переменных В главе 3 вы узнали о ключевом слове var в языке С#. Это ключевое слово позволяет определять локальную переменную без явной спецификации лежащего в основе типа данных. Тем не менее, такая переменная будет строго типизированной, поскольку компилятор определит ее корректный тип данных исходя из начального присваивания. Вспомните следующий код примера из главы 3: static void DeclarelmplicitVars () { // Неявно типизированные локальные переменные. var mylnt = 0; var myBool = true; var myString = "Time, marches on..."; // Вывод имен типов, лежащих в основе этих переменных. Console.WriteLine("mylnt is a: {0}", mylnt.GetType().Name); Console.WriteLine("myBool is a: {0}", myBool.GetType().Name); Console.WriteLine("myString is a: {0}", myString.GetType().Name); } Это средство языка очень удобно и зачастую обязательно, когда используется LINQ. Как будет показано на протяжении главы, многие запросы LINQ будут возвращать последовательности типов данных, которые неизвестны к моменту компиляции. Учитывая, что лежащий в основе тип данных до компиляции приложения не известен, очевидно, что объявить такую переменную явно не удастся! Синтаксис инициализации объектов и коллекций В главе 5 объясняется роль синтаксиса инициализации объектов, который позволяет создать переменную типа класса или структуры и установить любое количество ее общедоступных свойств за один прием. В результате получается очень компактный (и легко читаемый) синтаксис, который может применяться для подготовки объектов к использованию. Также вспомните из главы 10, что язык С# поддерживает очень похожий синтаксис инициализации коллекций объектов. Взгляните на следующий фрагмент
Глава 13. LINQ to Objects 465 кода, где синтаксис инициализации коллекций используется для наполнения List<T> объектами Rectangle, каждый из которых состоит из пары объектов Point, представляющих точку с координатами (х, у): List<Rectangle> myListOfRects = new List<Rectangle> { new Rectangle {TopLeft = new Point { X = 10, Y = 10 }, BottomRight = new Point { X = 200, Y = 200}}, new Rectangle {TopLeft = new Point { X = 2, Y = 2 }, BottomRight = new Point { X = 100, Y = 100}}, new Rectangle {TopLeft = new Point { X = 5, Y = 5 }, BottomRight = new Point { X = 90, Y = 75}} }; Хотя ничто не заставляет применять синтаксис инициализации коллекции или объекта, с его помощью можно получить более компактную кодовую базу. Более того, этот синтаксис в сочетании с неявной типизацией локальных переменных позволяет объявлять анонимный тип, что очень удобно для создании проекций LINQ. О проекциях LINQ речь пойдет далее в этой главе. Лямбда-выражения Лямбда-операция С# (=>) была уже полностью описана в главе 11. Вспомните, что эта операция позволяет построить лямбда-выражение, которое может быть использовано в любой момент, когда вызывается метод, который требует строго типизированного делегата в качестве аргумента. Лямбда-выражения значительно упрощают работу с делегатами .NET, поскольку сокращают объем кода, который должен быть написан вручную. Лямбда-выражения могут быть описаны следующим образом: АргументыДляОбработки => ОператорыДляИхОбработки В главе 11 рассматривались способы взаимодействия с методом FindAllO обобщенного класса List<T> с применением различных подходов. После работы с простым делегатом Predicate<T> и анонимным методом С# мы пришли к следующей (исключительно краткой) итерации, в которой применялось следующее лямбда-выражение: static void LambdaExpressionSyntax () { // Создать список целых. List<int> list = new List<int>(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 }); // Лямбда-выражение С#. List<int> evenNumbers = list.FindAll (l => (l " 2) == 0) ; // Вывод на консоль четных чисел. Console .WriteLine ("Here are your even numbers:11); foreach (int evenNumber in evenNumbers) { Console.Write("{0}\t", evenNumber); } Console.WriteLine (); } Лямбда-выражения очень полезны при работе с объектной моделью, лежащей в основе LINQ. Как вы вскоре убедитесь, операции запросов С# LINQ — это просто сокращенная нотация вызова методов класса по имени System.Linq.Enumerable. Эти методы обычно всегда требуют передачи в качестве параметров делегатов (в частности, делегата Func<>), которые используются для обработки данных с целью получения корректного результирующего набора. За счет использования лямбда-выражений можно упростить код и позволить компилятору вывести необходимый делегат.
466 Часть III. Дополнительные конструкции программирования на С# Расширяющие методы Расширяющие методы С# позволяют добавлять новую функциональность к существующим классам без необходимости применять наследование. Кроме того, расширяющие методы позволяют добавлять новую функциональность к запечатанным классам и структурам, от которых просто невозможно наследовать производные классы. Вспомните из главы 12, где создавался расширяющий метод, что первый параметр такого метода квалифицируется операцией this и помечает расширяемый тип. Кроме того, расширяющие методы должны всегда определяться внутри статического класса, а потому объявляться с использованием ключевого слова static. Для примера взгляните на следующий код: namespace MyExtensions { static class ObjectExtensions { // Определение расширяющего метода для System.Object. public static void DisplayDefimngAssembly (this object obj ) { Console.WriteLine ("{0 } lives here:\n\t->{1}\n", obj.GetType() .Name, Assembly.GetAssembly(obj.GetType())); } } } Чтобы использовать это расширение, в приложении должна быть сначала установлена ссылка на внешнюю сборку, содержащую реализацию расширяющего метода, с использованием диалогового окна Add Reference (Добавить ссылку) среды Visual Studio 2010. После этого можно просто импортировать определенное пространство имен и написать следующий код: static void Main(string [ ] args) { // Поскольку все расширяет System.Object, все классы и структуры // могут использовать это расширение. int mylnt = 12345678; mylnt.DisplayDefiningAssembly(); System.Data.DataSet d = new System.Data.DataSet() ; d.DisplayDefiningAssembly(); Console.ReadLine(); } При работе с LINQ вам редко придется строить собственные расширяющие методы, если вообще придется. Однако при создании выражений запросов LINQ на самом деле применяются многочисленные расширяющие методы, уже определенные Microsoft. Фактически каждая операция запроса С# LINQ — это сокращенная нотация ручного вызова лежащего в основе расширяющего метода, обычно определенного служебным классом System.Linq.Enumerable. Анонимные типы И последнее средство языка С#, которое мы здесь кратко пересмотрим — анонимные типы, которые были подробно описаны в главе 12. Это средство может быть использовано для быстрого моделирования "формы" данных, позволяя компилятору генерировать новое определение класса во время компиляции на основе указанного набора пар "имя/значение". Вспомните, что этот тип будет составлен с использованием семантики, основанной на значениях, и каждый виртуальный метод System.Obj ect будет coot-
Глава 13. LINQ to Objects 467 ветствующим образом переопределен. Чтобы определить анонимный тип, необходимо объявить неявно типизированную переменную и указать форму данных с применением синтаксиса инициализации объекта: // Создать анонимный тип, состоящий из другого анонимного типа, var purchaseltem = new { TimeBought = DateTime.Now, ItemBought = new {Color = "Red", Make = "Saab", CurrentSpeed =55}, Price = 34.000}; В LINQ анонимные типы часто используются при проектировании новых форм данных "на лету". Например, может существовать коллекция объектов Person, и нужно с помощью LINQ получить информацию о возрасте и номере карточки социального страхования для каждого объекта. Используя проекцию LINQ, можно позволить компилятору сгенерировать новый анонимный тип, содержащий необходимую информацию. Роль LINQ На этом краткий обзор средств языка С#, позволяющих LINQ выполнять свою работу, завершен. Однако важно понять, зачем вообще нужен язык LINQ? Любой разработчик программного обеспечения согласится с утверждением, что значительная часть времени при программировании тратится на получение и манипуляции данными. Когда говорят о "данных", немедленно приходит на ум информация, хранящаяся внутри реляционных баз данных. Тем не менее, другими популярными местами нахождения данных являются документы XML (файлы *. с on fig, локально сохраненные наборы DataSet, или данные в памяти, возвращенные службами WCF). Данные могут быть найдены в различных местах и помимо этих двух общепринятых хранилищ информации. Например, предположим, что имеется массив или обобщенный тип List<T>, содержащий 300 целых чисел, и нужно получить подмножество, которое отвечает заданному критерию (например, только четные или только нечетные числа, только простые числа, только неповторяющиеся числа больше 50). Или, возможно, при использовании API-интерфейсов рефлексии требуется получить в массиве Туре только метаданные для каждого класса, унаследованного от определенного родительского класса. В действительности данные находятся повсюду. До появления .NET 3.5 взаимодействие с определенной разновидностью данных требовало от программистов использования очень разных API-интерфейсов. Например, в табл. 13.1 описаны некоторые распространенные API-интерфейсы, применяемые для доступа к различным типам данных. Таблица 13.1. Способы манипуляции различными типами данных Необходимые данные Как получить Реляционные данные System.Data.dll, System.Data.SqlClient.dll и т.п. Данные документов XML System.Xml.dll Таблицы метаданных Пространство имен System.Reflection Коллекции объектов Пространства имен System.Array и System. Collections/System.Collections.Generic Разумеется, это вполне нормальные подходы к манипулированию данными. Фактически при программировании с помощью .NET 4.0/C# 2010 вы можете (и будете) непосредственно использовать ADO.NET, пространства имен XML, службы рефлексии и различные типы коллекций. Однако основная проблема состоит в том, что каждый из этих
468 Часть III. Дополнительные конструкции программирования на С# API-интерфейсов представляет собой "изолированный островок", слабо интегрируемый с другими. Правда, можно (например) сохранить DataSet из ADO.NET в документ XML и затем манипулировать им через пространства имен System.Xml, но все равно манипуляции данными остаются довольно асимметричными. В рамках API-интерфейса LINQ была предпринята попытка предложить программистам согласованный, симметричный способ получения и манипуляций "данными" в самом широком смысле этого понятия. Используя LINQ, можно создавать непосредственно внутри синтаксиса языка С# конструкции, называемые выражениями запросов. Эти выражения запросов основаны на множестве операций запросов, которые намеренно сделаны похожими — внешне и по поведению (но не идентичными) — на выражения SQL. Фокус, однако, в том, что выражение запроса может применяться для взаимодействия с многочисленными типами данных, даже данными, которые никак не связаны с реляционными базами. Строго говоря, "LINQ" — это термин, описывающий общий подход к доступу к данным. В зависимости от места использования запросов LINQ, существуют разновидности LINQ, которые перечислены ниже. • UNQ to Objects. Эта разновидность позволяет применять запросы LINQ к массивам и коллекциям. • LLNQ to XML. Эта разновидность позволяет применять LINQ для манипулирования и опроса документов XML. • UNQ to DataSet. Эта разновидность позволяет применять запросы LINQ к объектам DataSet из ADO.NET. • UNQ to Entities. Эта разновидность позволяет применять запросы LINQ внутри API-интерфейса ADO.NET Entity Framework (EF). • Parallel UNQ (он же PUNQ). Эта разновидность позволяет выполнять параллельную обработку данных, возвращенных запросом LINQ. Похоже, в Microsoft намерены глубоко интегрировать поддержку LINQ в среду программирования .NET. Вполне можно ожидать, что с течением времени LINQ станет неотъемлемой частью библиотек базовых классов .NET, языков и самой среды разработки Visual Studio. Выражения LINQ строго типизированы Очень важно отметить, что выражение запроса LINQ (в отличие от традиционных операторов SQL) является строго типизированным. Поэтому компилятор С# следит за этим и гарантирует, что выражения синтаксически оформлены корректно. Попутно следует отметить, что выражения запросов имеют представление метаданных внутри использующей их сборки, поскольку операции запросов LINQ всегда обладают развитой объектной моделью. Такие инструменты, как Visual Studio 2010, могут пользоваться этими метаданными для обеспечения работы полезных средств вроде IntelliSense, автозавершения и тому подобного. Основные сборки LINQ Как упоминалось в главе 2, в диалоговом окне New Project (Новый, проект) в Visual Studio 2010 доступна возможность выбора версии платформы .NET, для которой нужно производить компиляцию. При компиляции для .NET 3.5 и последующих версий каждый из шаблонов проектов автоматически ссылается на ключевые сборки LINQ, что легко заметить в Solution Explorer. В табл. 13.2 описана роль ключевых сборок LINQ. В остальной части книги вы встретите и другие дополнительные библиотеки LINQ.
Глава 13. LINQ to Objects 469 Таблица 13.2. Основные сборки, связанные с LINQ Сборка Назначение System.Core.dll Определяет типы, представляющие основной API- интерфейс LINQ. Это единственная сборка, к которой нужно иметь доступ, чтобы пользоваться любым API- интерфейсом LINQ, включая LINQ to Objects stem.Data.DataSetExtensions.dll Определяет набор типов для интеграции типов ADO.NET в программную парадигму LINQ (LINQ to DataSet) System.Xml.Linq.dll Предоставляет функциональность для использования LINQ с данными документов XML (LINQ to XML) Для того чтобы работать с LINQ to Objects, потребуется обеспечить, чтобы в каждом файле кода С#, содержащем запросы LINQ, импортировалось пространство имен System.Linq (определенное внутри сборки System.Core.dll). В противном случае возникнет множество проблем. Если вы столкнулись с примерно таким сообщением об ошибке во время компиляции: Error 1 Could not find an implementation of the query pattern for source type ' int[] ' . 'Where' not found. Are you missing a reference to 'System.Core.dll' or a using directive for 'System.Linq'? Ошибка 1 He удается обнаружить реализацию шаблона запроса для исходного типа 'mt[] '. 'Where' не найдено. Возможно, пропущена ссылка на 'System.Core.dll' или директива using для 'System.Linq'? то очень высока вероятность, что в файле С# отсутствует следующая директива: using System.Linq; Применение запросов LINQ к элементарным массивам Чтобы приступить к изучению LINQ to Objects, давайте построим приложение, которое будет применять запросы LINQ к различным объектам типа массива. Создадим консольное приложение по имени LinqOverArray и определим внутри класса Program статический вспомогательный метод под названием QueryOverStrings (). В этом методе создадим массив строк, содержащий несколько элементов по своему усмотрению (например, названия видеоигр). Пусть хотя бы два элемента содержат числовые значения, и еще несколько — внутренние пробелы. static void QueryOverStrings () { // Предположим, что имеется массив строк. string[] currentVideoGames = { "Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; } Теперь модифицируем Main() для вызова QueryOverStrings (): static void Main(string [ ] args) { Console.WriteLine("***** Fun with LINQ to Objects *****\n"); QueryOverStrings (); Console.ReadLine(); }
470 Часть III. Дополнительные конструкции программирования на С# Имея дело с любым массивом данных, очень часто приходится извлекать из него подмножество элементов на основе определенного критерия. Скажем, требуется получить только элементы, содержащие какое-нибудь число (т.е. "System Shock 2", "Uncharted 2" и "Fallout 3"), либо имеющие больше или меньше определенного количества символов, либо не содержащие внутренних пробелов (т.е. "Morrowind" или "Daxter") и т.п. Хотя такие задачи определенно можно решать с использованием членов типа System.Array и приложением приличных усилий, выражения запросов LINQ значительно упрощают ситуацию. Исходя из предположения, что требуется получить из массива только элементы, содержащие внутри себя пробел, причем в алфавитном порядке, можно построить следующее выражение запроса LINQ: static void QueryOverStrings () { // Предположим, что имеется массив строк. string [] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; // Выражение запроса для нахождения элементов массива, включающих пробелы. IEnumerable<string> subset = from g in currentVideoGames where g.Contains(" ") orderby g select g; // Вывести на консоль результаты, foreach (string s in subset) Console.WriteLine("Item: {0}", s); } Обратите внимание, что созданное здесь выражение запроса использует операции LINQ from, in, where, orderby и select. Формальности синтаксиса выражений запросов будут изложены далее в этой главе. Однако даже сейчас можно прочесть этот оператор примерно как "предоставить элементы из currentVideoGames, содержащие пробелы, в алфавитном порядке". Здесь каждый элемент, соответствующий критерию поиска, получает имя g (от "game"); однако можно было бы использовать любое допустимое имя переменной С#: IEnumerable<string> subset = from game in currentVideoGames where game.Contains(" ") orderby game select game; Возвращенная последовательность хранится в переменной по имени subset, тип которой реализует обобщенную версию интерфейса IEnumerable<T>, где Т — тип System.String (в конце концов, опрашивается массив элементов string). Получив результирующий набор, затем можно просто вывести на консоль его элементы, используя стандартную конструкцию foreach. В результате запуска приложения на выполнение будет получен следующий вывод: ***** Fun with LINQ to Objects ***** Item: Fallout 3 Item: System Shock 2 Item: Uncharted 2 Решение без использования LINQ Строго говоря, применение LINQ никогда не бывает обязательным. При желании тот же результирующий набор можно получить, отказавшись от LINQ и воспользовавшись такими программными конструкциями, как операторы if и циклы for. Ниже приведен метод, который генерирует тот же результат, что и QueryOverStrings(), но в намного более многословной манере:
Глава 13. LINQ to Objects 471 static void QueryOverStringsLongHand() { // Предположим, что имеется массив строк, string [] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout Э", "Daxter", "System Shock 2"}; string[] gamesWithSpaces = new string[5]; for (int i=0; i < currentVideoGames.Length; i++) { if (currentVideoGames[i] .Contains (" ")) gamesWithSpaces [i] = currentVideoGames[i]; } // Отсортировать набор. Array.Sort(gamesWithSpaces); // Вывести на консоль результат, foreach (string s in gamesWithSpaces) { if ( s != null) Console.WriteLine ("Item: {0}", s) ; } Console.WriteLine(); } Несмотря на возможные пути улучшения этого метода, факт остается фактом — запросы LINQ могут радикально упростить процесс извлечения новых подмножеств данных из источника. Вместо построения вложенных циклов, сложной логики if/else, временных типов данных и тому подобного, компилятор С# выполнит всю черновую работу за вас, как только будет создан подходящий запрос LINQ. Рефлексия результирующего набора LINQ Теперь предположим, что в классе Program определена дополнительная вспомогательная функция по имени ReflectOverQueryResultsO, которая выводит на консоль разнообразные детали о результирующем наборе LINQ (обратите внимание на тип параметра — System.Object, который позволяет принимать различные типы результирующих наборов): static void ReflectOverQueryResults(object resultSet) { Console.WriteLine ("***** info about your query *****"); Console.WriteLine("resultSet is of type: {0}", resultSet.GetType().Name); // тип Console.WriteLine("resultSet location: {0}", resultSet.GetType() .Assembly.GetName () .Name); // расположение } Поместив вызов этого метода в Query Over St rings () непосредственно после вывода полученного подмножества и запустив приложение, можно увидеть, что это подмножество на самом деле представляет собой экземпляр обобщенного типа OrderedEnumerable<TElement, ТКеу> (представленного в CIL-коде как OrderedEnumerable2), который является абстрактным внутренним типом, находящимся в сборке System.Core.dll: ***** jnfo about your query ***** resultSet is of type: OrderedEnumerableч2 resultSet location: System.Core На заметку! Многие из типов, представляющих результат LINQ, скрыты в браузере объектов Visual Studio 2010. Чтобы увидеть внутренние скрытые типы, воспользуйтесь утилитой ildasm.exe или reflector.exe.
472 Часть III. Дополнительные конструкции программирования на С# LINQ и неявно типизированные локальные переменные Хотя в данном примере программы относительно легко догадаться, что результирующий набор может быть интерпретирован как перечисление объектов string (т.е. IEnumerable<string>), менее очевидно, что типом подмножества на самом деле является OrderedEnumerable<TElement, TKey>. Поскольку результирующие наборы LINQ могут быть представлены с использованием изрядного числа типов из различных пространств имен LINQ, было бы утомительно определять подходящий тип для хранения результирующего набора, потому что во многих случаях лежащий в основе тип не очевиден и даже напрямую недоступен в коде (и как будет показано, иногда тип генерируется во время компиляции). Чтобы еще более подчеркнуть это обстоятельство, ниже показан дополнительный вспомогательный метод, определенный внутри класса Program (который должен быть вызван из метода Main()): static void QueryOverlnts () { int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8}; // Вывести только элементы меньше 10. IEnumerable<int> subset = from 1 in numbers where l < 10 select i; foreach (int l in subset) Console.WriteLine("Item: {0}", i); ReflectOverQueryResults(subset) ; } В данном случае переменная subset будет иметь совершенно иной внутренний тип. На этот раз тип, реализующий интерфейс IEnumerable<int> — это низкоуровневый класс по имени WhereArrayIterator<T>: Item: l Item: 2 Item: 3 Item: 8 ***** Info about your query ***** resultSet is of type: WhereArraylterator% 1 resultSet location: System.Core Поскольку точный тип запроса LINQ определенно неизвестен, в первом примере результат запроса был представлен как IEnumerable<T>, где Т — тип данных возвращенной последовательности (string, int и т.п.). Это все еще довольно запутано. Еще более усложняет картину то, что поскольку IEnumerable<T> расширяет необобщенный интерфейс IEnumerable, получить результат запроса LINQ допускается следующим образом: System.Collections.IEnumerable subset = from i in numbers where i < 10 select i; К счастью, неявная типизация существенно проясняет картину при работе с запросами LINQ: static void QueryOverlnts () { int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8}; // Здесь используется неявная типизация... var subset = from i in numbers where i < 10 select i; // . . .и здесь тоже. foreach (var i m subset) Console.WriteLine("Item: {0} ", l); ReflectOverQueryResults(subset); }
Глава 13. LINQ to Objects 473 В качестве эмпирического правила: при захвате результатов запроса LINQ всегда следует применять неявную типизацию. Однако помните, что (в большинстве случаев) реальное возвращенное значение имеет тип, реализующий интерфейс IEnumerable<T>. Какой именно тип кроется за этим (OrderedEnumerable<TElement, TKey>, WhereArrayIterator<T> и т.п.) — не важно, и определять его не обязательно. Как было показано в предыдущем примере кода, можно просто воспользоваться ключевым словом var с конструкцией f oreach для проведения итерации по полученным данным. LINQ и расширяющие методы Хотя в рассматриваемом примере не требуется явно писать какие-то расширяющие методы, на самом деле они используются на заднем плане. Выражения запросов LINQ могут применяться для итерации по содержимому контейнеров данных, которые реализуют обобщенный интерфейс IEnumerable<T>. Однако класс .NET System.Array (используемый для представления массива строк и массива целых чисел) не реализует этот контракт: // Тип System.Array, похоже, не реализует корректную // инфраструктуру для выражений запросов! public abstract class Array : ICloneable, IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable { } Несмотря на то что System. Array не реализует напрямую интерфейс IEnumerable<T>, он опосредовано получает необходимую функциональность этого типа (а также многие другие члены, связанные с LINQ) через статический тип класса System.Linq. Enumerable. В этом служебном классе определено множество обобщенных расширяющих методов (таких как Aggregate<T>(), First<T>, Max<T> и т.д.), которые System.Array (и другие типы) получают в свое распоряжение на заднем плане. Таким образом, после ввода операции точки за текущей локальной переменной currentVideoGames обнаружится значительное количество членов, которые отсутствуют в формальном определении System.Array (рис. 13.1). .;;LinqOverArr«). Program ^•QueiyOverttnngsO stringf] currentVideoGames ■ {"Могrewind", "Uncharted 2", "Fallout 3", "Dexter", "System Shock 2"}; // 4uild a query expression to find the items in the arra; // that have an embedded space. var subset ■ from g in currentVideoGames where g.Contains(" ") orderby g select g; currentVi^eoGamesJ ** Aggregate- stat: ; /,' Print out the п ф ЩШ/Ш foreach (var s in ; *. . Cwuolfi.WriteLia* . ' U i _i^ .л ,'** AsEnumerableo Console.WriteLine(; * ReflectOverQueryR* *♦ AsParallel \ AsParallelo ; *# AsQueryable ic void QueryOverS *^ AsQueryableo ;•«, Average<> f] A«um* we have | «ь Cest<> string[ ] currentvt ф c|one "Fallout 3". -%СопсЛ<> string g.«esKith^°"'-S<> i *• СоруТо щ (ейетюп) boolKmirn*fable<TSotirce>jyi<TSoufce>{Func<TSoufce,booJ> predicate) 1 Determines whether all elements ol a sequence satisfy a condition. Exceptions; System-ArgumeniNullExceptiofl ted 2", i < currentVideoGames.Length; i++) Рис. 13.1. Тип System.Array был расширен членами System.Linq.Enumerable
474 Часть III. Дополнительные конструкции программирования на С# Роль отложенного выполнения Другой важный момент, касающийся выражений запросов LINQ, состоит в том, что на самом деле они не выполняются до тех пор, пока не будет начата итерация по последовательности. Формально это называется отложенным выполнением. Преимущество такого подхода заключается в возможности применения одного и того же запроса LINQ многократно к одному и тому же контейнеру, с полной гарантией получения самых актуальных результатов. Рассмотрим следующую модификацию метода Query0verlnts(): static void QueryOverlnts () { int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 }; // Получить числа меньше 10. var subset = from i in numbers where i < 10 select i; // Оператор LINQ здесь выполняется! foreach (var i in subset) Console.WriteLine ("{0} < 10", i) ; Console.WriteLine (); // Изменить некоторые данные в массиве. numbers[0] = 4; // Оператор LINQ снова выполняется! foreach (var j in subset) Console.WriteLine ("{0} < 10", j); Console.WriteLine (); ReflectOverQueryResults(subset); } Ниже показан вывод после запуска этой программы. Обратите внимание, что во второй итерации по запрошенной последовательности появился дополнительный член, поскольку для первого элемента массива было установлено значение меньше 10. 1 2 3 8 4 1 2 3 8 < < < < < < < < < 10 10 10 10 10 10 10 10 10 Среда Visual Studio 2010 обладает одним очень полезным аспектом: поместив точку останова перед выполнением запроса LINQ, можно просматривать содержимое в сеансе отладки. Переместите курсор мыши на переменную результирующего набора LINQ (subst на рис. 13.2). После этого появляется возможность выполнить запрос в этот момент, раскрыв узел Results View (Представление результатов). Pfogrem.cs X I ^LinqOvwArray.Program * j ^#QueryOverStrings0 /J Build a querj /7 that have an У/ Print out the foreach (var s in sul Consolfi.WriteLii Consolo.WriteLine(); ibl ;5»-stem.(.inq.<>dere4E' ■;sMng,si *.;' Рис. 13.2. Отладка выражений LINQ в Visual Studio 2010
Глава 13. LINQ to Objects 475 Роль немедленного выполнения Чтобы выполнить выражение LINQ за пределами логики итерации for each, можно вызвать любое количество расширяющих методов, определенных типом Enumerable, таких как ToArry<T>, ToDictionary<TSource,TKey>() и ToList<T>(). Все эти методы заставляют запрос LINQ выполняться в момент их вызова, чтобы получить снимок данных. После этого полученным снимком можно манипулировать независимо. static void ImmediateExecution () { int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 }; // Получить данные НЕМЕДЛЕННО как int[]. int[] subsetAsIntArray = (from l in numbers where l < 10 select l) .ToArray<int> (); // Получить данные НЕМЕДЛЕННО как List<int>. List<int> subsetAsListOfInts = (from l in numbers where i < 10 select i) .ToList<int> () ; } Обратите внимание, что все выражение LINQ заключено в скобки для приведения к корректному лежащему в основе типу (каким бы он ни был); это позволяет вызывать методы Enumerable. Также вспомните из главы 10, что когда компилятор С# может однозначно определить параметр типа обобщенного элемента, то вы не обязаны указывать этот параметр. Таким образом, ТоАггау<Т> (или ToList<T>) можно было бы вызвать следующим образом: int[] subsetAsIntArray = (from i in numbers where l < 10 select i) . ToArrayO; Польза от немедленного выполнения очевидна, когда нужно вернуть результат запроса LINQ внешнему вызывающему коду. Это будет темой следующего раздела главы. Исходный код. Проект LinqOverArray доступен в подкаталоге Chapter 13. Возврат результата запроса LINQ В классе (или структуре) можно определить поле, значением которого будет результат запроса LINQ. Однако для этого нельзя использовать неявную типизацию (т.е. ключевое слово var для таких полей применяться не может), и целью запроса LINQ не могут быть данные уровня экземпляра, а потому он должен быть статическим. Учитывая эти ограничения, писать код вроде показанного ниже придется редко: class LINQBasedFieldsAreClunky { private static string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; // Здесь нельзя использовать неявную типизацию1 Нужно знать тип subset1 private IEnumerable<string> subset = from g in currentVideoGames where g.Contains(" ") orderby g select g; public void PrintGamesO { foreach (var item in subset) { Console.WriteLine(item); } } }
476 Часть III. Дополнительные конструкции программирования на С# Довольно часто запросы LINQ определяются в контексте метода или свойства. Более того, для упрощения переменная, предназначенная для хранения результирующего набора, будет храниться в неявно типизированной локальной переменной с использованием ключевого слова var. Вспомните из главы 3, что неявно типизированные переменные не могут применяться для определения параметров, возвращаемых значений или полей класса либо структуры. С учетом всего этого может возникнуть вопрос: как вернуть результат запроса внешнему коду? Все зависит от обстоятельств. Если есть результирующий набор, состоящий из строго типизированных данных, такой как массив строк или список List<T> объектов Саг, можно отказаться от ключевого слова var и использовать правильный тип IEnumerable<T> либо IEnumerable (поскольку IEnumerable<T> расширяет IEnumerable). Ниже показан пример нового консольного приложения под названием LinqRetValues. class Program { static void Main(string[] args) { Console.WriteLine("***** LINQ Transformations *****\n"); IEnumerable<string> subset = GetStringSubset(); foreach (string item in subset) { Console.WriteLine(item); } Console.ReadLine()/ } static IEnumerable<string> GetStringSubset () { string[] colors = {"Light Red11, "Green", "Yellow", "Dark Red", "Red", "Purple"}; // Обратите внимание, что subset представляет собой // объект, совместимый с IEnumerable<string>. IEnumerable<string> theRedColors = from c in colors where с.Contains("Red") select c; return theRedColors; } } Результат выглядит следующим образом: LINQ Transformations Light Red Dark Red Red Возврат результатов LINQ через немедленное выполнение Этот пример работает ожидаемым образом только потому, что возвращаемое значение GetStringSubset() и запрос LINQ внутри этого метода строго типизированы. Если воспользоваться ключевым словом var для определения переменной subset, то вернуть значение можно будет, только если метод по-прежнему возвращает IEnumerable<string> (и если неявно типизированная локальная переменная на самом деле совместима с указанным типом возврата). Поскольку оперировать IEnumerable<T> было бы несколько неудобно, можно использовать немедленное выполнение. Например, вместо возврата IEnumerable<string> возможно просто вернуть string[], при условии трансформирования последовательности в строго типизированный массив.
Глава 13. LINQ to Objects 477 Ниже показан новый метод класса Program, который именно это и делает: static string[] GetStringSubsetAsArray () { string[] colors = {"Light Red", "Green", "Yellow", -"Dark Red", "Red", "Purple"}; var theRedColors = from с in colors where с.Contains("Red") select c; // Отобразить результаты в массив, return theRedColors.ToArray (); } При этом вызывающему коду не известно, что полученный им результат поступил от запроса LINQ, и он просто работает с массивом строк, как ожидалось. Например: foreach (string item in GetStringSubsetAsArray()) { Console.WriteLine(item); } Немедленное выполнение также важно при попытке вернуть вызывающему коду результат проекции LINQ. Чуть позже в этой главе мы еще вернемся к этой теме. Однако сначала давайте посмотрим, как применять запросы LINQ к обобщенным и необобщенным объектам коллекций. Исходный код. Проект LinqRetValues доступен в подкаталоге Chapter 13. Применение запросов LINQ к объектам коллекций Помимо извлечения результатов из простого массива данных, выражения запросов LINQ также манипулируют данными внутри членов коллекций из пространства имен System.Collections.Generic, таких как List<T>. Создадим новое консольное приложение по имени ListOverCollections и определим базовый класс Саг, поддерживающий текущую скорость, цвет, производителя и дружественное имя, как показано ниже: class Car { public string PetName {get; set;} public string Color {get; set;} public int Speed {get; set;} public string Make {get; set;} } Теперь определим в методе Main() переменную List<T> для хранения элементов типа Саг и воспользуемся синтаксисом инициализации объектов для заполнения списка несколькими новыми объектами Саг: static void Main(string [ ] args) { Console.WriteLine (••***** LINQ over Generic Collections *****\n"); // Создать список Listo объектов Car. List<Car> myCars = new List<Car>() { new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"}, new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW" }, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo" }, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"} }; Console.PeadLine(); }
478 Часть III. Дополнительные конструкции программирования на С# Доступ к содержащимся в контейнере подобъектам Применение запроса LINQ к обобщенному контейнеру ничем не отличается от применения к простому запросу, поскольку LINQ to Objects может использоваться с любым типом, реализующим IEnumerable<T>. На этот раз цель заключается в построении выражения запроса для получения объектов Саг из списка myCars, у которых значение скорости больше 55. После получения подмножества на консоль будет выводиться имя каждого объекта Саг за счет обращения к его свойству Pet Name. Предположим, что определен следующий вспомогательный метод (принимающий параметр List<Car>), который вызывается в Main(): static void GetFastCars(List<Car> myCars) { // Найти в контейнере Listo объекты Car, у которых Speed больше 55. var fastCars = from с in myCars where c. Speed > 55 select c; foreach (var car in fastCars) { Console.WriteLine ("{0 } is going too fast!", car.PetName); } } Обратите внимание, что выражение запроса выбирает только те элементы из List<T>, у которых Speed больше 55. Запустив приложение, можно увидеть, что критерию поиска отвечают только два элемента — "Henry" и "Daisy". Чтобы построить более сложный критерий, можно запросить только автомобили марки BMW со значением Speed больше 90. Для этого понадобится создать составной булевский оператор, в котором используется С#-операция &&: static void GetFastBMWs(List<Car> myCars) { // Найти быстрые автомобили BMW! var fastCars = from с in myCars where c. Speed > 90 && c.Make == "BMW" select c; foreach (var car in fastCars) { Console.WriteLine ("{ 0 } is going too fast!", car.PetName); } } В этом случае на консоль выводится только одно имя — "Henry". Применение запросов LINQ к необобщенным коллекциям Вспомните, что операции запросов LINQ предназначены для работы с любым типом, реализующим IEnumerable<T> (как непосредственно, так и через расширяющие методы). Учитывая то, что класс System.Array оснащен всей необходимой инфраструктурой, может оказаться сюрпризом, что унаследованные (необобщенные) контейнеры из System.Collections такой поддержки не имеют. К счастью, итерацию по данным, содержащимся внутри необобщенных коллекций, все равно можно выполнять с использованием обобщенного расширяющего метода Enumerable.OfType<T>(). Метод OfType<T>() — один из нескольких членов Enumerable, которые не расширяют обобщенные типы. При вызове этого члена на необобщенном контейнере, реализующем интерфейс IEnumerable (вроде Array List), просто укажите тип элемента в контейнере для извлечения совместимого с IEnumerable<T> объекта. Сохранить эти данные в коде можно в неявно типизированной переменной.
Глава 13. LINQ to Objects 479 Ниже показан новый метод, который заполняет ArrayList множеством объектов Саг (не забудьте импортировать пространство имен System.Collections в файл Program.cs). static void LINQOverArrayList () { Console. WriteLine (••***** LINQ no ArrayList *****"); // Необобщенная коллекция автомобилей. ArrayList myCars = new ArrayList () { new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"}, new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo" }, new Car{ PetName = "Melvin11, Color = "White", Speed = 43, Make = "Ford"} }; // Трансформировать ArrayList в тип, совместимый с IEnumerable<T>. var myCarsEnum = myCars.OfType<Car>(); // Создать выражение запроса, нацеленное на совместимый с IEnumerable<T> тип. var fastCars = from с in myCarsEnum where с. Speed > 55 select c; foreach (var car in fastCars) { Console.WriteLine ("{0} is going too fast!", car.PetName); } } Аналогично предыдущим примерам, этот метод, вызванный в Main(), отобразит только имена "Henry" и "Daisy" на основе формата нашего запроса LINQ. Фильтрация данных с использованием Of Туре<т>() Как уже известно, необобщенные типы способны содержать комбинации любых элементоЁ в качестве членов контейнеров (таких как ArrayList), поскольку они про- тотипированы для приема экземпляров System.Object. Например, предположим, что ArrayList содержит различные элементы, часть из которых являются числами. Получить подмножество, состоящее только из числовых данных, можно с помощью метода OfType<T>(), поскольку во время итераций он отфильтрует все элементы, тип которых отличается от заданного. static void OfTypeAsFilter () { // Извлечение целых из ArrayList. ArrayList myStuff = new ArrayList (); myStuff .AddRange (new object [] { 10, 400, 8, false, newCarO, "string data" }); var mylnts = myStuff.OfType<int>(); // Выведет на консоль 10, 400 и 8. foreach (int i in mylnts) { Console.WriteLine("Int value: {0}", l) ; } } Отлично! Теперь есть возможность применять запросы LINQ к массивам, обобщенным и необобщенным коллекциям. Эти контейнеры содержат как элементарные типы С# (целочисленные, строковые данные), так и пользовательские специальные классы. Следующая задача — изучить множество дополнительных операций LINQ, которые можно использовать для построения сложных и полезных запросов. Исходный код. Проект LinqOverCollectons доступен в подкаталоге Chapter 13.
480 Часть III. Дополнительные конструкции программирования на С# Исследование операций запросов LINQ В языке С# определено множество операций запросов в готовом виде. Некоторые наиболее часто используемые из них перечислены в табл. 13.3. На заметку! Документация .NET Framework 4.0 SDK содержит подробные сведения по каждой операции С# LINQ (см. раздел "LINQ General Programming Guide" ("Общее руководство программирования на LINQ")). Таблица 13.3. Различные операции запросов LINQ Операции запросов Назначение from, in Используется для определения основы любого выражения LINQ, позволяющей извлечь подмножество данных из нужного контейнера where Используется для определения ограничений о том, т.е. какие элементы должны извлекаться из контейнера select Используется для выбора последовательности из контейнера join, on, equals, into Выполняет соединения на основе указанного ключа. Помните, что эти "соединения" ничего не делают с данными в реляционных базах orderby, ascending, Позволяет упорядочить результирующий набор в порядке возраста- descending ния или убывания group, by Порождает подмножество с данными, сгруппированными по указанному значению В дополнение к частичному списку операций, приведенному в табл. 13.3, класс System.Linq.Enumerable предлагает набор методов, которые не имеют прямой сокращенной нотации в форме операций запроса С#, а представлены в виде расширяющих методов. Эти обобщенные методы могут вызываться для трансформации результирующего набора в различной манере (Reverse<>(), ToArray<>(), ToList<>() и т.п.). Некоторые из них используются для извлечения одиночных элементов из результирующего набора, другие выполняют различные операции над множествами (Distinct<>(), Union<>(), Intersect<>() и т.п.), третьи агрегируют результаты (Count<>(), Sum<>(), Min<>(), Max<>() и т.п.). Чтобы приступить к исследованию более сложных запросов LINQ, создадим новое консольное приложение под названием FunWithLinqExpressions. Затем определим массив или коллекцию некоторых простых данных. Для данного проекта создается массив объектов ProductInfo, определенный в следующем коде: class ProductInfo { public string Name {get; set;} public string Description {get; set;} public int NumberlnStock {get; set; } public override string ToStringO { return string.Format("Name={0}, Description={l}, Number in Stock={2}", Name, Description, NumberlnStock); } } Теперь внутри метода Main() заполним массив объектами ProductInfo:
Глава 13. LINQ to Objects 481 static void Main(string[] args) { Console.WriteLine("***** Fun with Query Expressions *****\nM), // Этот массив будет основой для тестирования... ProductInfo[] itemsInStock = new[] { new ProductInfo! Name = "Mac's Coffee", Description = "Coffee with TEETH", NumberlnStock =24}, new ProductInfo! Name = "Milk Maid Milk", Description = "Milk cow's love", NumberlnStock = 100}, new ProductInfo! Name = "Pure Silk Tofu", Description = "Bland as Possible", NumberlnStock = 120}, new ProductInfo! Name = "Cruchy Pops", Description = "Cheezy, peppery goodness", NumberlnStock =2}, new ProductInfo! Name = "RipOff Water", Description = "From the tap to your wallet", NumberlnStock = 100}, new ProductInfo! Name = "Classic Valpo Pizza", Description = "Everyone loves pizza!", NumberlnStock = 73} }; // Здесь будем вызывать разнообразные методы! Console.ReadLine (); } Базовый синтаксис выборки Поскольку синтаксическая корректность выражения запроса LINQ проверяется во время компиляции, следует помнить, что порядок этих операций важен. В простейшем виде каждый запрос LINQ строится из операций from, in и select. Ниже показан базовый шаблон, которому нужно следовать: var результат = from сопоставляемыйЭлемент in контейнер select сопоставляемыйЭлемент; Элемент, следующий за операцией from, представляет элемент, соответствующий критерию запроса LINQ, и назвать его можно по своему усмотрению. Элемент после операции in представляет контейнер данных для поиска (массив, коллекция или документ XML). Рассмотрим очень простой запрос, который не делает ничего помимо извлечения каждого элемента контейнера (это похоже на поведение SQL-оператора SELECT * в базе данных): static void SelectEverything(ProductInfo [ ] products) { // Получить все! Console .WriteLine ("All product details:11); var allProducts = from p in products select p; foreach (var prod in allProducts) { Console.WriteLine(prod.ToString()); } } По правде говоря, это выражение запроса не слишком полезно, поскольку выдаст подмножество, идентичное содержимому входного параметра. При желании из входящего параметра можно извлечь только значения Name каждого товара, используя следующий синтаксис выборки:
482 Часть III. Дополнительные конструкции программирования на С# static void ListProductNames(ProductInfo[] products) { // Теперь получить только наименования товаров. Console.WriteLine ("Only product names:"); var names = from p in products select p.Name; foreach (var n in names) { Console.WriteLine("Name: {0}", n) ; } } Получение подмножества данных Чтобы получить определенное подмножество из контейнера, можно воспользоваться операцией where. При этом общий шаблон запроса становится таким: var результат = from элемент in контейнер where булевскоеВыражение select элемент; Обратите внимание, что операция where ожидает выражения, которое возвращает булевское значение. Например, чтобы получить из массива-аргумента ProductInfo[] только те позиции, которых на складе есть более 25 единиц, можно написать следующий код: static void GetOverstock(ProductInfo[] products) { Console.WriteLine("The overstock items!"); // Получить только те товары, которых на складе более 25. var overstock = from p in products where p.NumberlnStock > 25 select p; foreach (ProductInfo с in overstock) { Console.WriteLine(c.ToString() ) ; } } Как уже было показано ранее в этой главе, при построении конструкции where допускается применять любые операции С# для получения сложных выражений. Например, вспомним запрос, который извлекал только автомобили BMW, которые едут со скоростью выше 100 миль в час: // Получить машины BMW, движущиеся со скоростью свыше 100 миль в час. var onlyFastBMWs = from с in myCars where с.Make == "BMW" && с Speed >= 100 select c; foreach (Car с in onlyFastBMWs) { Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed); } Проекция новых типов данных Новые формы данных можно также проектировать на основе существующих источников. Предположим, что для входящего параметра ProductInfo[] необходимо получить результирующий набор, который содержит только имя и описание каждого элемента. Для этого понадобится определить оператор select, который динамически породит новый анонимный тип: static void GetNamesAndDescriptions (ProductInfo [ ] products) { Console.WriteLine("Names and Descriptions:"); var nameDesc = from p in products select new { p.Name, p.Description };
Глава 13. LINQ to Objects 483 foreach (var item in nameDesc) { // Можно также использовать свойства Name и Description напрямую. Console.WriteLine(item.ToString()); } } Всегда помните, что когда имеете дело с запросом LINQ, выполняющим проекцию, нет никакой возможности узнать лежащий в ее основе тип данных, поскольку он определяется во время компиляции. В этих случаях обязательным является ключевое слово var. Кроме того, нельзя создавать методы с неявно типизированными возвращаемыми значениями. Поэтому следующий код не скомпилируется: static var GetProjectedSubset () { ProductInfo[] products = new[] { new ProductInfo! Name = "Mac's Coffee", Description = "Coffee with TEETH", NumberlnStock =24}, new ProductInfo! Name = "Milk Maid Milk", Description = "Milk cow's love", NumberlnStock = 100}, new ProductInfo! Name = "Pure Silk Tofu", Description = "Bland as Possible", NumberlnStock = 120}, new ProductInfo! Name = "Cruchy Pops", Description = "Cheezy, peppery goodness", NumberlnStock =2}, new ProductInfo! Name = "RipOff Water", Description = "From the tap to your wallet", NumberlnStock = 100}, new ProductInfo! Name = "Classic Valpo Pizza", Description = "Everyone loves pizza!", NumberlnStock =73} }; var nameDesc = from p in products select new ! p.Name, p.Description }; return nameDesc; // Ошибка! } Когда требуется вернуть проекцию данных вызывающему коду, один из подходов предусматривает трансформацию результата запроса в .NET-объект System.Array за счет применения расширяющего метода ТоАггауО. Таким образом, если модифицировать выражение запроса следующим образом: // Теперь возвращаемое значение - Array. static Array GetProjectedSubset () ! // Отобразить набор анонимных объектов на объект Array, return nameDesc.ToArray (); } то его можно вызвать и обработать данные в методе Main() следующим образом: Array objs = GetProjectedSubset (); foreach (object o in objs) ! Console.WriteLine(о); // Вызывает ToStringO на каждом анонимном объекте. }
484 Часть III. Дополнительные конструкции программирования на С# Обратите внимание, что должен использоваться объект System.Array, при этом нельзя применять синтаксис объявления массива С#. Это связано с тем, что лежащий в основе проекции тип не известен, поскольку речь идет об анонимном классе, который сгенерирован компилятором. Кроме того, не указывается параметр типа для обобщенного метода ТоАггу<>Т(), поскольку он тоже не известен до времени компиляции. Очевидная проблема здесь состоит в утере строгой типизации, поскольку каждый элемент в объекте Array считается относящимся к типу Object. Тем не 'менее, когда нужно вернуть результирующий набор LINQ, полученный в результате операции проекции, трансформация данных в тип Array (или другой подходящий контейнер через другие члены типа Enumerable) обязательна. Получение счетчиков посредством Enumerable При проектировании новых пакетов данных может понадобиться знать количество элементов перед тем, как вернуть их в последовательности. Для определения числа элементов, возвращаемых выражением запроса LINQ, используется расширяющий метод Count () класса Enumerable. Например, следующий метод пересчитает все объекты string в локальном массиве, имеющие длину более шести символов: static void GetCountFromQuery () { string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; // Получить количество из запроса, int numb = (from g in currentVideoGames where g.Length > 6 select g).Count<string>(); // Вывести на консоль количество элементов. Console.WriteLine ("{0} items honor the LINQ query.", numb); } Обращение результирующих наборов Поменять порядок элементов в результирующем наборе на противоположный довольно легко с помощью расширяющего метода Reverse<T>() класса Enumerable. Например, в следующем методе выбираются все элементы из входного массива ProductInfo[] в обратном порядке: static void ReverseEverything(ProductInfo[] products) { Console.WriteLine("Product in reverse:"); var allProducts = from p in products select p; foreach (var prod in allProducts.Reverse()) { Console.WriteLine(prod.ToString()); } } Выражения сортировки В начальных примерах настоящей главы было показано, что выражение запроса может принимать операцию or derby для сортировки элементов в подмножестве по заданному значению. По умолчанию принят порядок по возрастанию, поэтому упорядочение строк производится в алфавитном порядке, числовых значений — от меньшего к большему, и т.д. Чтобы отсортировать в обратном порядке, укажите операцию descending. Рассмотрим следующий метод:
Глава 13. LINQ to Objects 485 static void AlphabetizeProductNames(ProductInfo [ ] products) { // Получить названия товаров в алфавитном порядке. var subset = from p in products orderby p.Name select p; Console.WriteLine("Ordered by Name:"); foreach (var p in subset) { Console.WriteLine(p.ToString()); } } Хотя порядок по возрастанию принят по умолчанию, можно прояснить намерения, явно указав операцию ascending: var subset = from p in products orderby p.Name ascending select p; Для получения элементов в порядке убьшания служит операция descending: var subset = from p in products orderby p.Name descending select p; LINQ как лучшее средство построения диаграмм Класс Enumerable поддерживает набор расширяющих методов, которые позволяют использовать два (или более) запроса LINQ в качестве основы для нахождения объединений, разностей, конкатенации и пересечений данных. Прежде всего, рассмотрим расширяющий метод Except(). Этот метод возвращает результирующий набор LINQ, содержащий разность между двумя контейнерами, которым в данном случае является значение "Yugo": static void DisplayDiff() { List<string> myCars = new List<String> {"Yugo", "Aztec", "BMW"}; List<string> yourCars = new List<String>{"BMW", "Saab", "Aztec" }; var carDiff =(from с in myCars select c) .Except(from c2 in yourCars select c2); Console.WriteLine("Here is what you don't have, but I do:"); foreach (string s in carDiff) Console.WriteLine (s) ; // Выводит Yugo. } Метод Intersect () возвращает результирующий набор, содержащий общие элементы данных для множества контейнеров. Например, следующий метод вернет последовательность из "Aztec" и "BMW". static void Displaylntersection () { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; // Получить общие члены. var carlntersect = (from с in myCars select c) .Intersect(from c2 in yourCars select c2); Console.WriteLine("Here is what we have in common:"); foreach (string s in carlntersect) Console.WriteLine(s) ; // Выводит Aztec и BMW. } Метод Union() возвращает результирующий набор, который включает все члены множества запросов LINQ. Подобно любому объединению, повторяющиеся члены в нем встречаются только однажды. Поэтому следующий метод выводит на консоль значения "Yugo", 'Aztec", "BMW" и "Saab":
486 Часть III. Дополнительные конструкции программирования на С# static void DisplayUnion () { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; // Получить объединение двух контейнеров. var carUnion = (from с in myCars select c) .Union(from c2 in yourCars select c2); Console.WriteLine ("Here is everything:"); foreach (string s in carUnion) Console.WriteLine (s); // Выводит все общие члены. } Наконец, расширяющий метод ConcatO возвращает результирующий набор, представляющий собой прямую конкатенацию результирующих наборов LINQ. Например, следующий метод выводит на консоль в качестве результата "Yugo", "Aztec", "BMW", "BMWVSaab" и "Aztec": static void DisplayConcat () { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; var carConcat = (from с in myCars select c) .Concat(from c2 in yourCars select c2); // Выводит Yugo Aztec BMW BMW Saab Aztec, foreach (string s in carConcat) Console.WriteLine (s); Исключение дубликатов После вызова расширяющего метода Cone at () очень легко можно получить в результате излишние элементы, что иногда является тем, что и требовалось. В других случаях может понадобиться удалить дублированные элементы данных. Для этого необходимо вызвать расширяющий метод DistinctO, как показано ниже: static void DisplayConcatNoDups () { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; var carConcat = (from с in myCars select c) .Concat(from c2 in yourCars select c2) ; // Выводит Yugo Aztec BMW Saab. foreach (string s in carConcat.Distinct ()) Console.WriteLine (s); } Агрегатные операции LINQ Запросы LINQ могут также выполнять различные агрегатные операции над результирующим набором. Одним из примеров агрегации может служить расширяющий метод Count (). Другие возможности включают получение среднего, максимума, минимума или суммы значений за счет использования методов Мах(), Min(), Average(), Sum(), которые являются членами класса Enumerable. Ниже приведен простой пример: static void AggregateOps () { double[] winterTemps = { 2.0, -21.3, 8, -4, 0, 8.2 }; // Различные примеры агрегации. Console.WriteLine("Max temp: {0}", (from t in winterTemps select t).Max());
Глава 13. LINQ to Objects 487 Console.WriteLine("Min temp: {0}", (from t in winterTemps select t).Min()); Console.WriteLine("Avarage temp: {0}я, (from t in winterTemps select t) .Average ()); Console.WriteLine("Sum of all temps: {0}", (from t in winterTemps select t) .Sum()); } Эти примеры должны предоставить достаточно сведений, чтобы вы могли уверенно приступить к построению собственных выражений запросов LINQ. Существуют и дополнительные операции, которые пока еще не рассматривались; они будут описаны позже, когда речь пойдет о связанных технологиях LINQ. В завершение вводного экскурса в LINQ, оставшаяся часть главы посвящена деталям отношений между операциями запросов LINQ и лежащей в основе объектной моделью. Исходный код. Проект FunWithLinqExpressions доступен в подкаталоге Chapter 13. Внутреннее представление операторов запросов LINQ К настоящему моменту вам уже знаком процесс построения выражений запросов с использованием различных операций запросов С# (таких как from, in, where, or derby и select). Также вам известно, что некоторая функциональность API-интерфейса LINQ to Objects может быть доступна только при вызове расширяющих методов класса Enumerable. Реальность, однако, в том, что компилятор С# на этапе компиляции транслирует все операции С# LINQ в вызовы методов класса Enumerable. Фактически это означает, что при желании можно строить все операторы LINQ, не используя ничего помимо лежащей в основе объектной модели. Подавляющее большинство методов Enumerable прототипированы так, чтобы принимать в качестве аргументов делегаты. В частности, многие методы требуют обобщенного делегата по имени Fun с о, определенного в пространстве имен System и находящегося в сборке System.Core.dll. Например, посмотрите на метол Where() класса Enumerable, который вызывается автоматически, когда используется операция where С# LINQ: // Перегруженные версии метода Enumerable.Where<T>() . // Обратите внимание, что второй параметр имеет тип System. Funco. public static IEnumerable<TSource> Where<TSource> ( this IEnumerable<TSource> source, System.Func<TSource,int,bool> predicate) public static IEnumerable<TSource> Where<TSource> ( this IEnumerable<TSource> source, System.Func<TSource,bool> predicate) Делегат Funco (как следует из его имени) представляет шаблон функции с набором аргументов и возвращаемым значением. Просмотрев этот тип в браузере объектов Visual Studio 2010, можно заметить, что делегат Funco принимает от нуля до четырех входных аргументов (типизированных как Т1, Т2, ТЗ и Т4, и названных argl, arg2, arg3 и агд4) и возвращает тип, обозначенный TResult: // Различные форматы делегата Funco. public delegate TResult Func<Tl,T2,T3,T4,TResult>(Tl argl, T2 arg2, ТЗ агдЗ, Т4 arg4) public delegate TResult Func<Tl,T2,T3,TResult>(Tl argl, T2 arg2, T3 arg3) public delegate TResult Func<Tl,T2,TResult>(Tl argl, T2 arg2) public delegate TResult Func<Tl,TResult> (Tl argl) public delegate TResult Func<TResult> ()
488 Часть III. Дополнительные конструкции программирования на С# Поскольку множество членов System.Linq.Enumerable требуют при вызове в качестве входа делегат, можно либо вручную создать новый тип делегата и разработать для него необходимые целевые методы, воспользоваться анонимным методом С# либо определить подходящее лямбда-выражение. Независимо от выбранного подхода, конечный результат будет одним и тем же. Хотя простейший способ построения запросов LINQ предусматривает использование операций запросов С# LINQ, давайте все-таки кратко рассмотрим все возможные подходы, просто чтобы увидеть связь между операциями запросов С# и лежащим в основе типом Enumerable. Построение выражений запросов с использованием операций запросов Для начала создадим новое консольное приложение по имени LinqUsingEnumerable. В классе Program будет определен набор статических вспомогательных методов (каждый из которых вызывается из метода Main()) для иллюстрации различных способов построения выражений запросов LINQ. Первый метод — QueryStringsWithOperators () — обеспечивает наиболее прямолинейный способ построения выражений запросов и идентичен коду примера LinqOverArray, приведенному ранее в этой главе: static void QueryStringWithOperators() { Console.WriteLine ("***** Using Query Operators *****"); string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; var subset = from game in currentVideoGames where game.Contains (" ") orderby game select game; foreach (string s in subset) Console.WriteLine("Item: {0}", s) ; } Очевидное преимущество применения операций запросов С# при построении выражений запросов связано с тем, что делегаты Funco и вызовы типа Enumerable остаются вне поля зрения и внимания, поскольку работа компилятора С# состоит в выполнении необходимой трансляции. Несомненно, создание выражений LINQ с использованием различных операций запросов (from, in, where или orderby) является наиболее распространенным и простым подходом. Построение выражений запросов с использованием типа Enumerable и лямбда-выражений Имейте в виду, что используемые здесь операции запросов LINQ — это на самом деле сокращенные версии вызова различных расширяющих методов, определенных в типе Enumerable. Рассмотрим следующий метод QueryStringsWithEnumerableAndLambdasO, который обрабатывает локальный массив строк, но на этот раз в нем напрямую применяются расширяющие методы Enumerable: static void QueryStringsWithEnumerableAndLambdas() { Console.WriteLine("***** Using Enumerable / Lambda Expressions *****"); string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
Глава 13. LINQ to Objects 489 // Построить выражение запроса с использованием расширяющих методов, // предоставленных типу Array через тип Enumerable, var subset = currentVideoGames.Where(game => game.Contains (" ")) .OrderBy(game => game).Select(game => game); // Вывести "на консоль результаты, foreach (var game in subset) Console.WriteLine ("Item: {0}", game); Console.WriteLine (); } Код начинается с вызова расширяющего метода Where () на строковом массиве currentVideoGames. Вспомните, что класс Array получает этот расширяющий метод от класса Enumerable. Метод Enumerable.Where() требует параметра-делегата System. Func<Tl, TResultx Первый параметр типа этого делегата представляет совместимые с IEnumerable<T> данные для обработки (в рассматриваемом случае это массив строк), в то время как второй — результирующие данные метода, которые получаются от единственного оператора, вставленного в лямбда-выражение. Возвращаемое значение метода Where () в этом примере кода скрыто от глаз, но "за кулисами" работа происходит с типом OrderedEnumerable. На этом объекте вызывается обобщенный метод OrderBy (), который также принимает параметр — делегат Fun с о. На этот раз производится передача всех элементов по очереди через соответствующее лямбда-выражение. Конечным результатом вызова OrderBy () будет новая упорядоченная последовательность начальных данных. И, наконец, производится вызов метода Select () на последовательности, возвращенной OrderBy (), который в конечном итоге вернет результирующий набор данных, сохраняемый в неявно типизированной переменной по имени subset. Приведенный запрос LINQ несколько сложнее понять, чем предыдущий пример с операциями запросов С# LINQ. Часть этой сложности, конечно, связана с вызовами через операцию точки. Ниже показан в точности тот же запрос, но с выделением каждого шага в отдельный фрагмент: static void QueryStringsWithEnumerableAndLambdas2 () { Console.WriteLine("***** Using Enumerable / Lambda Expressions *****"); string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; // Разделить на фрагменты! var gamesWithSpaces = currentVideoGames.Where(game => game.Contains(" ")); var orderedGames = gamesWithSpaces.OrderBy(game => game); var subset = orderedGames.Select(game => game); foreach (var game in subset) Console.WriteLine("Item: {0}", game); Console.WriteLine(); } Как видите, построение выражения запроса LINQ с прямым использованием методов класса Enumerable намного более многословно, чем с применением операций запросов С#. Кроме того, поскольку методы Enumerable требуют параметров-делегатов, обычно необходимо писать лямбда-выражения, чтобы обеспечить обработку входных данных лежащей в основе целью делегата.
490 Часть III. Дополнительные конструкции программирования на С# Построение выражений запросов с использованием типа Enumerable и анонимных методов Учитывая, что лямбда-выражения С# — это просто сокращенная нотация вызова анонимных методов, рассмотрим третье выражение запроса во вспомогательном методе QueryStringsWithAnonymousMethods(): static void QueryStringsWithAnonymousMethods () { Console.WriteLine ("***** Using Anonymous Methods *****"); string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; // Построить необходимые делегаты Funco с использованием анонимных методов. Func<stnng, bool> searchFilter = delegate(string game) { return game.Contains(" "); }; Func<string, string> itemToProcess = delegate(string s) { return s; }; // Передать делегаты в методы Enumerable, var subset = currentVideoGames.Where(searchFilter) .OrderBy(itemToProcess) .Select (itemToProcess); // Вывести на консоль результаты, foreach (var game in subset) Console.WriteLine("Item: {0}", game); Console.WriteLine(); } Этот вариант выражения запроса еще более многословен, потому что делегаты Funco создаются вручную с использованием методов Where(), OrderBy () и Select() класса Enumerable. Положительная сторона этого подхода связана с тем, что синтаксис анонимных методов позволяет заключить всю обработку, выполняемую делегатами, в одном определении метода. Тем не менее, этот метод функционально эквивалентен методам QueryStringsWithEnumerableAndLambdasO и QueryStringsWithOperatorsO, которые рассматривались в предыдущих разделах. Построение выражений запросов с использованием типа Enumerable и низкоуровневых делегатов Наконец, чтобы построить выражение запроса, используя по-настоящему многословный подход, можно отказаться от применения синтаксиса лямбда-выражений и анонимных методов и непосредственно создавать цели делегатов для каждого типа Fun с о. Ниже показана финальная итерация выражения запроса, смоделированная внутри нового типа класса по имени VeryComplexQueryExpression: class VeryComplexQueryExpression { public static void QueryStringsWithRawDelegates() { Console.WriteLine ("***** Using Raw Delegates *****"); string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; // Построить необходимые делегаты Funco. Func<stnng, bool> searchFilter = new Func<string, bool> (Filter) ; Func<string, string> itemToProcess = new Func<string,string>(Processltem); // Передать делегаты в методы Enumerable, var subset = currentVideoGames .Where(searchFilter) .OrderBy(itemToProcess) .Select(itemToProcess) ; // Вывести на консоль результаты, foreach (var game in subset) Console.WriteLine ("Item: {0}", game);
Глава 13. LINQ to Objects 491 Console.WriteLine(); } // Цели делегатов. public static bool Filter(string game) {return game.Contains (" ");} public static string Processltem(string game) { return game; } } Чтобы протестировать эту итерацию логики обработки строк, нужно вызвать этот метод в Main () класса Program следующим образом: VeryComplexQueryExpression.QueryStringsWithRawDelegates (); Если теперь запустить приложение, чтобы опробовать все возможные подходы, вывод окажется идентичным, независимо от выбранного пути. Относительно выражений запросов и их скрытого представления следует помнить перечисленные ниже моменты. • Выражения запросов создаются с использованием различных операций запросов С#. • Операции запросов — это просто сокращенная нотация вызова расширяющих методов, определенных в типе System.Linq.Enumerable. • Многие методы Enumerable принимают в качестве параметров делегаты (в частности, Func<>). • Любой метод, ожидающий параметр-делегат, может принимать вместо него лямбда-выражение. • Лямбда-выражения — это просто замаскированные анонимные методы (которые значительно улучшают читабельность). • Анонимные методы являются сокращенной нотацией для размещения низкоуровневого делегата и ручного построения целевого метода делегата. Приведенная выше информация поможет понять, что на самом деле происходит "за кулисами" дружественных к пользователю операций запросов С#. Исходный код. Проект LinqOverArrayUsingEnumerable доступен в подкаталоге Chapter 13. Резюме LINQ — это набор взаимосвязанных технологий, которые были разработаны для обеспечения единой, симметричной манеры взаимодействия с различными формами данных. Как объяснялось на протяжении этой главы, LINQ может взаимодействовать с любым типом, реализующим интерфейс IEnumerable<T>, включая простые массивы, а также обобщенные и необобщенные коллекции данных. Было показано, что работа с технологиями LINQ обеспечивается несколькими средствами языка С#. Например, учитывая тот факт, что выражения запросов LINQ могут возвращать любое количество результирующих наборов, принято использовать ключевое слово var для представления лежащего в основе типа данных. Кроме того, лямбда- выражения, синтаксис инициализации объектов и анонимные типы позволяют строить очень функциональные и компактные запросы LINQ. Что более важно, вы увидели, что операции запросов С# LINQ на самом деле являются просто сокращенными нотациями вызовов статических членов типа System.Linq. Enumerable. Как было показано, большинство членов Enumerable оперируют типами делегатов Func<T>, которые могут принимать адреса литеральных методов, анонимные методы или лямбда-выражения в качестве ввода для выполнения запроса.
ЧАСТЬ IV Программирование с использованием сборок .NET В этой части... Глава 14. Конфигурирование сборок .NET Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов Глава 16. Процессы, домены приложений и контексты объектов Глава 17. Язык CIL и роль динамических сборок Глава 18. Динамические типы и исполняющая среда динамического языка
ГЛАВА 14 Конфигурирование сборок .NET Каждое из приложений, продемонстрированных в предыдущих главах, разрабатывалось как обычное "автономное" приложение с размещением всей специальной логики внутри единственного исполняемого файла (* . ехе). Одной из главных особенностей платформы .NET является возможность повторного использования двоичного кода, когда в приложениях эксплуатируются типы, содержащиеся внутри нескольких внешних сборок (также называемых библиотеками кода). В этой главе речь пойдет о создании, развертывании и конфигурировании сборок в .NET. Сначала будет показано, как создавать пространства имен в .NET, и в чем состоит разница между однофайловыми и многофайловыми, а также приватными и разделяемыми сборками. Затем будет описано, как в исполняющей среде определяется местонахождение сборки, и что собой представляет глобальный кэш сборок (Global Assembly Cache — GAC), конфигурационные файлы приложений (файлы * . con fig), сборки политик издателя и пространство имен System. Configuration. Определение специальных пространств имен Прежде чем углубляться в детали развертывания и конфигурирования сборок, сначала необходимо узнать о том, как создавать специальные пространства имен в .NET. Вплоть до этого момента в книге разрабатывались лишь небольшие демонстрационные программы, в которых использовались существующие пространства имен в мире .NET (в частности, пространство имен System). При создании собственных приложений, однако, может быть удобно группировать свои взаимосвязанные типы в специальные пространства имен. В С# это делается с помощью ключевого слова namespace. Определение специальных пространств имен явным образом начинает играть даже еще более важную роль при создании dll-сборок в .NET, поскольку другие разработчики должны иметь возможность импортировать эти специальные пространства имен, чтобы использовать содержащиеся в них типы. Чтобы увидеть все это на практике, создадим новый проект типа Console Application (Консольное приложение) по имени СиstomNamespaces. Предположим, что требуется разработать коллекцию геометрических классов Square (квадрат), Circle (круг) и Hexagon (шестиугольник) и, благодаря имеющимся в них схожим чертам, сгруппировать их вместе и разместить внутри сборки CustomNamespaces . ехе в уникальном пространстве имен MyShapes. Для реализации на выбор доступны два основных подхода. Во-первых, можно определить все соответствующие классы в единственном файле С# (ShapesLib. cs), как показано ниже:
Глава 14. Конфигурирование сборок .NET 495 // Файл Shapeslib.cs using System; namespace MyShapes { // Класс Circle public class Circle{ /* Интересные члены. . . */ } // Класс Hexagon public class Hexagon{ /* Более интересные члены. . . */ } // Класс Square public class Square{ /* Даже еще более интересные члены... */ } } Во-вторых, можно разбить одно пространство имен на несколько файлов кода С#. Чтобы обеспечить размещение типов в одной и той же логической группе, нужно просто упаковать определения всех данных классов в контекст одного и того же пространства имен: // Файл Circle.cs using System; namespace MyShapes { // Класс Circle public class Circle { /* Интересные методы... */ } } // Файл Hexagon.сs using System; namespace MyShapes { // Класс Hexagon public class Hexagon { /* Более интересные методы... */ } } // Файл Square.cs using System; namespace MyShapes { // Класс Square public class Square { /* Даже еще более интересные методы... */ } } В обоих случаях важно обратить внимание, что пространство MyShapes выступает в роли своего рода концептуального "контейнера" для данных классов. Если в другом пространстве имен (например, СиstomNamespaces) возникает необходимость в импорте типов, определенных внутри данного пространства имен, достаточно просто воспользоваться ключевым словом using точно так же, как и при использовании типов, определенных в библиотеках базовых классов .NET: II Использование типов, определенных в пространстве имен MyShape. using System; using MyShapes; namespace CustomNamespaces { public class Program { static void Main(string [ ] args) { Hexagon h = new Hexagon () ; Circle с = new Circle (); Square s = new Square (); }
496 Часть IV. Программирование с использованием сборок .NET В этом примере предполагается, что файл (или файлы) с кодом С#, в котором содержится определение пространства имен MyShapes, является частью того же проекта типа Console Application, что и файл с определением пространства имен CustomNamespaces. Другими словами, все эти файлы будут использоваться для компиляции единственной исполняемой сборки .NET. Если пространство имен MyShapes определено внутри какой-то внешней сборки, для успешной компиляции может потребоваться добавить ссылку на эту библиотеку. Создание приложений, предусматривающих взаимодействие с внешними библиотеками, рассматривается далее в настоящей главе. Устранение конфликтов на уровне имен за счет использования полностью уточненных имен С технической точки зрения применять в С# ключевое слово using при ссылках на типы, определенные во внешних пространствах имен, вовсе не требуется. Вместо этого можно использовать полностью уточненное имя типа, под которым, как рассказывалось в главе 1, понимается имя типа с идущим перед ним в качестве префикса названием пространства имен, где этот тип определен: // Обратите внимание, что пространство имен MyShapes больше не импортируется. using System; namespace CustomNamespaces { public class Program { static void Main(string[] args) { MyShapes.Hexagon h = new MyShapes.Hexagon (); MyShapes.Circle с = new MyShapes.Circle (); MyShapes.Square s = new MyShapes.Square (); } } } Обычно необходимости в применении полностью уточненного имени не возникает. Это требует большего объема клавиатурного ввода, при этом никак не влияет на размер и скорость выполнения кода. С другой стороны, в CIL-коде типы всегда определяются с использованием полностью уточненных имен. В результате применение в С# ключевого слова using, по сути, позволяет просто экономить время. Тем не менее, иногда применение полностью уточненных имен может оказываться очень удобным (а то и вообще необходимым) для избегания конфликтов на уровне имен, которые могут возникать при использовании множества пространств имен и наличии в этих пространствах имен одинаково названных типов. Для примера предположим, что появилось новое пространство имен под названием My3DShapes, в котором содержатся определения трех классов, способных визуализировать фигуры в трехмерном формате: // Еще одно пространство имен для работы с фигурами. using System; namespace My3DShapes { // Класс трехмерного круга. public class Circle { } // Класс трехмерного многоугольника. public class Hexagon { } // Класс трехмерного квадрата. public class Square { } }
Глава 14. Конфигурирование сборок .NET 497 Если теперь модифицировать класс Program, как показано ниже, компилятор сообщит о множестве ошибок, связанных с тем, что в обоих пространствах имен присутствуют классы с идентичными именами: // Много неоднозначностей! using System; using MyShapes; using My3DShapes; namespace CustomNamespaces { public class Program { static void Main (string [ ] args) { //На какое пространство имен производится ссылка? Hexagon h = new Hexagon(); // Компилятор сообщит об ошибке! Circle с = new Circle(); // Компилятор сообщит об ошибке! Square s = new Square(); // Компилятор сообщит об ошибке! } } } Использование полностью уточненных имен позволяет устранить все эти неоднозначности: // Неоднозначности устранены. static void Main(string[] args) { My3DShapes.Hexagon h = new My3DShapes.Hexagon (); My3DShapes.Circle с = new My3DShapes.Circle(); MyShapes.Square s = new MyShapes.Square(); } Устранение конфликтов на уровне имен за счет использования псевдонимов В С# ключевое слово using также позволяет создавать псевдоним для полностью уточенного имени типа. В этом случае определяется маркер, на месте которого во время компиляции должно подставляться полностью уточненное имя. Определение псевдонимов является вторым способом разрешения конфликтов на уровне имен. using System; using MyShapes; using My3DShapes; // Устранение неоднозначности за счет применения специального псевдонима. using The3DHexagon = My3DShapes.Hexagon; namespace CustomNamespaces { class Program { static void Main(string[] args) { // Здесь на самом деле создается // класс My 3D Shapes .Hexagon. The3DHexagon h2 = new The3DHexagon(); } } - 1
498 Часть IV. Программирование с использованием сборок .NET Такой альтернативный синтаксис using позволяет создавать псевдонимы и для имеющих длинные названия пространств имен. Например, одним из подобных пространств имен в библиотеке базовых классов является System. Runtime. Serialization. Formatters .Binary, в котором содержится член по имени BinaryFormatter. При желании экземпляр BinaryFormatter можно создать, как показано ниже: using bfHome = System.Runtime.Serialization.Formatters.Binary; namespace MyApp { class ShapeTester { static void Main(string[] args) { bfHome.BinaryFormatter b = new bfHome.BinaryFormatter(); } } } Допускается также применением обычной директивы using: using System.Runtime.Serialization.Formatters.Binary; namespace MyApp { class ShapeTester { static void Main(string [ ] args) { BinaryFormatter b = new BinaryFormatter (); } } } На данном этапе беспокоиться о том, для чего служит класс BinaryFormatter, не нужно (этот класс подробно рассматривается в главе 20). Пока что главное уяснить то, что ключевое слово using в С# позволяет создавать псевдонимы для длинных полностью уточненных имен и потому может применяться для разрешения конфликтов на уровне имен, которые могут возникать в результате импорта пространств имен, содержащих типы с идентичными именами. На заметку! Следует иметь в виду, что чрезмерное использование псевдонимов в С# может привести к получению запутанной кодовой базы. Если другие программисты в команде не знают об этих специальных псевдонимах, они могут посчитать, что данные псевдонимы ссылаются на типы в библиотеках базовых классов .NET, и прийти в замешательство, не найдя их описаний в документации .NET 4.0 Framework SDK. Создание вложенных пространств имен При организации типов допускается определять пространства имен внутри других пространств имен. В библиотеках базовых классов .NET подобное встречается во многих местах и обеспечивает размещение типов на более глубоких уровнях. Например, пространство имен 10 находится внутри пространства имен System и охватывает возможности System. 10. Чтобы создать корневое пространство имен, содержащее внутри себя существующее пространство имен My3DShapes, необходимо модифицировать код следующим образом: // Создание вложенного пространства имен. namespace Chapterl4
Глава 14. Конфигурирование сборок .NET 499 { namespace My3DShapes { // Класс трехмерного круга. class Circle { } // Класс трехмерного шестиугольника. class Hexagon { } // Класс трехмерного квадрата. class Square { } } } Во многих случаях роль корневого пространства имен заключается просто в предоставлении дальнейшего уровня контекста, и потому внутри него самого не могут содержаться определения каких-либо типов (как в случае пространства имен Chapter 14). Когда дело обстоит именно так, вложенные пространства могут определяться внутри него следующим компактным способом: // Создание вложенного пространства имен (другой способ). namespace Chapterl4.My3DShapes { // Класс трехмерного круга. class Circle { } // Класс трехмерного шестиугольника. class Hexagon { } // Класс трехмерного квадрата. class Square { } } Из-за того, что теперь пространство My3DShapes вложено в корневое пространство имен Chapterl4, необходимо соответствующим образом обновить все существующие директивы using и псевдонимы типов: using Chapterl4.My3DShapes; using The3DHexagon = Chapterl4.My3DShapes.Hexagon; Пространство имен, используемое по умолчанию в Visual Studio 2010 В завершение темы пространств имен стоит обратить внимание, что при создании нового проекта на С# в Visual Studio 2010 используемому по умолчанию пространству имен назначается точно такое же название, как у проекта. После этого при вставке в проект новых файлов кода (выбирая в меню Project (Проект) пункт Add New Item (Добавить новый элемент)) все соответствующие типы автоматически помещаются внутрь этого пространства имен. Чтобы изменить его название, откройте окно свойств проекта, перейдите в нем на вкладку Application (Приложение) и введите желаемое имя в поле Default namespace (Пространство имен по умолчанию), как показано на рис. 14.1. После такого изменения любой новый элемент, который будет вставляться в проект, будет размещаться внутри пространства имен MyDefaultNamespace (разумеется, для использования его типов в другом пространстве имен понадобится указать соответствующую директиву using). Пока все идет хорошо. Теперь, когда мы рассмотрели некоторые детали упаковки специальных типов в организованные пространства имен, давайте посмотрим на преимущества и формат сборок .NET, а после этого перейдем к исследованию деталей создания, развертывания и конфигурирования приложений. Исходный код. Проект CustomNamespaces доступен в подкаталоге Chapter 14.
500 Часть IV. Программирование с использованием сборок .NET CustomNamespaces* X Щ рннаниийши Application* Build Build Events Debug Resources Services Settings Reference Paths Signing ' •] Platfoim. [N/A Assembly name: CustomNamespaces Target framework: Default namespace: MyDefaultNamespacej J Output type NET Framework 4 Client Profile | Console Application Startup object (Not set} Resources Specify how application resources will be managed: ' Icon and manifest A manifest determines specific settings for an application. To embed a custom manifest, first add И •»- Рис. 14.1. Изменение названия пространства имен, используемого по умолчанию Роль сборок .NET Приложения .NET создаются за счет складывания вместе некоторого количества сборок. Сборка представляет собой поддерживающий версии самоописываемый двоичный файл, обслуживаемый CLR (Common Language Runtime — общеязьшовая исполняющая среда). Хотя сборки .NET имеют точно такие же файловые расширения (* . ехе или * . dll), как и старые двоичные файлы Windows (в том числе и унаследованные серверы СОМ), внутри они устроены совсем иначе. Поэтому прежде чем углубляться в изучение дальнейшего материала, давайте сначала посмотрим, какие преимущества предлагает формат сборок. Сборки повышают возможность повторного использования кода При построении консольных приложений в предыдущих главах могло показаться, что вся функциональность этих приложений содержалась внутри создававшейся исполняемой сборки. На самом деле во всех этих приложениях использовались многочисленные типы из всегда доступной библиотеки кода .NET по имени ms cor lib. dll (вспомините, что компилятор С# добавляет ссылку Hamscorlib.dll автоматически), а в некоторых случаях также и из библиотеки по имени System.Windows.Forms.dll. Как известно, библиотека кода (также называемая библиотекой классов] представляет собой файл с расширением * .dll, в котором содержатся типы, предназначенные для использования во внешних приложениях. При создании исполняемых сборок, несомненно, придется использовать много системных и специальных библиотек кода по мере разработки текущего приложения. Однако следует учесть, что библиотека кода не обязательно имеет вид файла с расширением * . dll. Вполне допускается (хотя и не часто), чтобы в исполняемой сборке применялись типы, определенные внутри какого-то внешнего исполняемого файла. В таком случае упоминаемый файл * .ехе тоже может считаться библиотекой кода. Какой бы вид не имела библиотека кода, платформа .NET позволяет многократно использовать типы не зависящим от языка образом. Например, можно не только размещать типы на различных языках, но и наследовать от них. Базовый класс, определенный на языке С#, может расширять класс, написанный на Visual Basic. Интерфейсы, определенные на языке Pascal .NET, могут реализовать структуры, определенные в С#, и т.д. Главное понять, что разбиение единого монолитного исполняемого файла на несколько сборок .NET открывает возможности для повторного использования кода в не зависящей от языка манере.
Глава 14. Конфигурирование сборок .NET 501 Сборки определяют границы типов Вспомните, что полностью уточненное имя типа получается за счет добавления к его собственному имени (которое может, например, выглядеть как Console) в качестве префикса названия пространства имен, к которому тип относится (например, System). Сборка, в которой тип размещен, далее определяет его идентичность. Например, две сборки с уникальными именами (скажем, MyCars.dll и YourCars.dll), в обеих из которых определено пространство имен (CarLibrary), содержащее класс по имени SportsCar, в мире .NET будут считаться совершенно отдельными типами. Сборки являются единицами, поддерживающими версии Сборкам .NET присваивается состоящий из четырех частей числовой номер версии в формате кстаришй номер>.<младший номер>.<номер сборки>.<номер редакции>. (Если номер версии явно не указан, сборке автоматически назначается номер 1.0.0.0 из-за применяемых по умолчанию в Visual Studio настроек проекта.) Этот номер вместе с необязательным значением открытого ключа позволяет множеству версий одной и той же сборки сосуществовать на одной и той же машине. Формально сборки, в которых предоставляется значение открытого ключа, называются строго именованными. Как будет показано далее, при наличии строгого имени CLR-среда гарантирует загрузку корректной версии сборки от имени вызывающего клиента. Сборки являются самоописываемыми Сборки считаются самоописываемыми (self-describing) отчасти потому, что содержат информацию о каждой из внешних сборок, к которой им нужно иметь доступ, чтобы функционировать надлежащим образом. Следовательно, если сборке требуется доступ к библиотекам System.Windows.Forms.dll и System.Drawing.dll, это обязательно будет документировано в ее манифесте. Вспомните из главы 1, что манифестом называется блок метаданных, который описывает саму сборку (ее имя, версию, необходимые внешние сборки и т.д.). Помимо данных манифеста, в сборке также содержатся метаданные, которые описывают структуру каждого из имеющихся в ней типов (имена членов, реализуемые интерфейсы, базовые классы, конструкторы и т.п.). Благодаря наличию в самой сборке такого детального описания, CLR-среде не требуется заглядывать в системный реестр Windows для выяснения ее местонахождения (что радикально отличается от предлагавшейся Microsoft ранее модели программирования СОМ). Как будет показано в ходе настоящей главе, вместо этого в CLR-среде применяется совершенно щрвая схема для получения информации о местонахождении внешних библиотек кода. Сборки поддаются конфигурированию Сборки могут развертываться как "приватные" (private) или как "разделяемые" (shared). Приватные сборки размещаются в том же каталоге (или подкаталоге), что и клиентское приложение, в котором они используются. Разделяемые сборки, с другой стороны, представляют собой библиотеки, предназначенные для использования во многих приложениях на одной и той же машине, и потому развертываются в специальном каталоге, который называется глобальным кэшем сборок (Global Assembly Cache — GAC). При любом способе развертывания для сборок могут быть предусмотрены специальные конфигурационные XML-файлы. С их помощью можно указать CLR-среде, где конкретно искать сборки, какую версию той или иной сборки загружать для определенного клиента или в какой каталог на локальной машине, папку в сети или URL-адрес заглядывать. Конфигурационные XML-файлы еще будут рассматриваться в настоящей главе.
502 Часть IV. Программирование с использованием сборок .NET Формат сборки .NET Ознакомившись с некоторыми преимуществами сборок .NET, давайте более детально рассмотрим, как эти сборки устроены внутри. С точки зрения структуры любая сборка .NET (* . dll или * . ехе) включает в себя следующие элементы. • заголовок файла Windows; • заголовок файла CLR; • CIL-код; • метаданные типов; • манифест сборки; • дополнительные встроенные ресурсы. Хотя первые два элемента представляют собой такие блоки данных, на которые обычно можно не обращать внимания, краткого рассмотрения они все-таки заслуживают. Ниже предлагаются краткие описания всех перечисленных элементов. Заголовок файла Windows Заголовок файла Widows указывает на тот факт, что сборка может загружаться и использоваться в операционных системах семейства Windows. Кроме того, этот заголовок идентифицирует тип приложения (консольное, приложение с графическим пользовательским интерфейсом или библиотека кода * . dll). Чтобы просмотреть информацию, содержащуюся в заголовке Windows, необходимо открыть сборку .NET в утилите dumpbin. ехе (в окне командной строки Visual Studio 2010) с указанием флага /headers: dumpbin /headers CarLibrary.dll Ниже показано (не полностью), как будет выглядеть информация в заголовке Windows сборки CarLibrary. dll, которая разрабатывается далее в главе (можете запустить утилиту dumpbin. ехе, указав на месте CarLibrary имя любого из ранее созданных файлов * .dll или * .ехе): Dump of file CarLibrary.dll РЕ signature found File Type: DLL FILE HEADER VALUES 14C machine (x86) 3 number of sections 4B37DCD8 time date stamp Sun Dec 27 16:16:56 2009 0 file pointer to symbol table 0 number of symbols EO size of optional header 2102 characteristics Executable 32 bit word machine DLL OPTIONAL HEADER VALUES 10B magic # (PE32) 8.00 linker version E00 size of code 600 size of initialized data 0 size of uninitialized data 2CDE entry point @0402CDE) 2000 base of code
Глава 14. Конфигурирование сборок .NET 503 4000 base of data 400000 image base @0400000 to 00407FFF) 2000 section alignment 200 file alignment -«4.00 operating system version 0.00 image version 4.00 subsystem version 0 Win32 version 8000 size of image 200 size of headers 0 checksum 3 subsystem (Windows CUI) Данные дампа файла CarLibrary.dll Обнаружена подпись РЕ Тип файла: DLL Значения заголовка файла Машина: 14С (х86) Количество разделов: 3 Дата и время 4B37DCD8: Воскр Декабрь 21 16:16:56 2009 Файловых указателей на таблицу символов: О Количество символов:О Размер необязательного заголовка: ЕО Характеристики 2102: Исполняемый файл Машина с 32-битным словом DLL Необязательные значения заголовка Магическое число (РЕ32): 10В Версия редактора связей: 8.00 Размер кода: Е00 Размер инициализированных данных: 600 Размер неинициализированных данных: О Точка входа: 2CDE @0402CDE) База кода: 2000 База данных: 4000 База образа: 400000 (с 00400000 по 00407FFF) Выравнивание в разделах: 2000 Выравнивание в файле: 200 Версия операционной системы 4.00 Версия образа: 0.00 Версия подсистемы 4.00 Версия Win32: О Размер образа: 8000 Размер заголовков: 200 Контрольная сумма: О Подсистема: 3 (консольный интерфейс пользователя Windows) Теперь, важно уяснить, что большей части программистов, работающей с .NET, никогда не придется беспокоиться о формате встраиваемых в сборку .NET данных заголовков. Если только не требуется разрабатывать новый компилятор для одного из языков .NET (где такая информация действительно важна), можно не вникать в тонкие детали данных заголовков. Однако помните, что эта информация используется "за кулисами", когда Windows загружает двоичный образ в память.
504 Часть IV. Программирование с использованием сборок .NET Заголовок файла CLR Заголовок CLR представляет собой блок данных, который должны обязательно поддерживать все сборки .NET (что они и делают, благодаря компилятору С#) для того, чтобы они могли обслуживаться в CLR-среде. Вкратце, в этом заголовке определяются многочисленные флаги, которые позволяют исполняющей среде разобраться в компоновке управляемого файла. Например, эти флаги указывают, где внутри файла находятся метаданные и ресурсы, как выглядит версия исполняющей среды, на которую ориентировалась данная сборка, каково (необязательное) значение открытого ключа, и т.д. Чтобы просмотреть данные заголовка CLR в сборке .NET, необходимо открыть сборку в утилите dumpbin.exe с указанием флага /clrheader: dumpbin /clrheader CarLibrary.dll Ниже показано, как будут выглядеть данные заголовка CLR в этой сборке .NET: Dump of file CarLibrary.dll File Type: DLL clr Header: 4 8 cb 2.05 runtime version 2164 [ A74] RVA [size] of MetaData Directory 1 flags IL Only 0 entry point token 0] RVA [size] of Resources Directory 0] RVA [size] of StrongNameSignature Directory 0] RVA [size] of CodeManagerTable Directory 0] RVA [size] of VTableFixups Directory 0] RVA [size] of ExportAddressTableJumps Directory 0] RVA [size] of ManagedNativeHeader Directory Данные дампа файла CarLibrary.dll Тип файла: DLL Заголовок clr: 48 cb Версия исполняющей среды: 2.05 2164 [ А74] RVA [размер] каталога MetaData Флагов: 1 Только IL-код Маркер точки входа: 0 0] RVA [размер] каталога Resources 0] RVA [размер] каталога StrongNameSignature 0] RVA [размер] каталога CodeManagerTable 0] RVA [размер] каталога VTableFixups 0] RVA [размер] каталога ExportAddressTableJumps 0] RVA [размер] каталога ManagedNativeHeader Опять-таки, разработчики приложений .NET не должны беспокоиться о тонких деталях заголовков CLR. Главное понимать, что такие данные обязательно содержатся в каждой сборке .NET и применяются исполняющей средой .NET при загрузке образа в память. Теперь рассмотрим информацию, которая является гораздо более полезной при решении повседневных программистских задач. Summary 2000 2000 2000 0 0 0 0 0 0 .reioc . rsrc .text Сводка 2000 2000 2000 0 0 0 0 0 0 .reloc . rsrc . text [ [ [ [ [ [
Глава 14. Конфигурирование сборок .NET 505 CIL-код, метаданные типов и манифест сборки В своей основе любая сборка содержит код на языке CIL, который, как уже рассказывалось ранее, представляет собой промежуточный язык, не зависящий ни от платформы, ни от процессора. Во время выполнения внутренний CIL-код "на лету" (посредством ЛТ-компилятора) компилируется в инструкции, соответствующие требованиям конкретной платформы и процессора. Благодаря этому, сборки .NET в действительности могут выполняться в рамках разнообразных архитектур, устройств и операционных систем. (Хотя разработчики .NET-приложений могут спокойно жить и работать, не разбираясь в деталях языка программирования CIL, в главе 17 все-таки будут приведены базовые сведения о синтаксисе и семантике этого языка.) В любой сборке содержатся метаданные, которые полностью описывают формат находящихся внутри нее типов, а также внешних типов, на которые она ссылается. В исполняющей среде .NET эти метаданные используются для выяснения того, в каком месте внутри двоичного файла находятся типы (и их члены), для размещения типов в памяти и для упрощения процесса удаленного вызова методов. Более подробно о формате этих метаданных будет рассказываться в главе 16 при рассмотрении служб рефлексии. Кроме того, в любой сборке должен обязательно содержаться ассоциируемый с ней манифест (по-другому еще называемый метаданными сборки). В этом манифесте описан каждый входящий в состав сборки модуль, версия сборки, а также любые внешние сборки, на которые ссылается текущая сборка (что отличает сборки от библиотек типов СОМ, в которых не существовало никакой возможности для документирования внешних зависимостей). Как будет показано далее в главе, манифест сборки интенсивно применяется в CLR-среде во время определения места, на которое указывают представляющие внешние сборки ссылки. На заметку! Как будет объясняться позже в главе, термин "модуль .NET" применяется для описания частей в многофайловой сборке. Необязательные ресурсы сборки И, наконец, в любой сборке .NET может также содержаться любое количество вложенных ресурсов, таких как значки приложения, графические файлы, звуковые фрагменты или таблицы строк. На самом деле платформа .NET поддерживает возможность создания даже так называемых подчиненных сборок (satellite assemblies), содержащих только локализованные ресурсы и больше ничего. Эта возможность может быть очень удобной, когда необходимо разделить ресурсы по конкретным культурам (английской, немецкой и т.п.) при построении программного обеспечения, которое предназначено для использования по всему миру. Рассмотрение процесса создания подчиненных сборок выходит за рамки настоящей книги; о вставке ресурсов приложения в сборку будет немного рассказываться в главе 30 при описании API-интерфейса Windows Presentation Foundation. Однофайловые и многофайловые сборки С технической точки зрения сборка может состоять из нескольких так называемых модулей (module). Модуль на самом деле представляет собой не более чем просто общий термин, применяемый для обозначения действительного двоичного файла .NET. В большинстве ситуаций, однако, сборка состоит из единственного модуля. В этом случае между (логической) сборкой и лежащим в ее основе (физическим) двоичным файлом существует соответствие типа "один к одному" (откуда и происходит термин "однофай- ловая сборка").
506 Часть IV. Программирование с использованием сборок .NET Однофайловая сборка CarLibrary.dll Манифест Метаданные типов CIL-код Дополнительные ресурсы Рис. 14.2. Однофайловая сборка В однофайловых сборках все необходимые элементы (заголовки Windows и CIL, метаданные типов, манифест и необходимые ресурсы) размещаются внутри единственного пакета * . ехе или * . dll. На рис. 14.2 схематично показано устройство однофайловой сборки. Многофайловая сборка, с другой стороны, состоит из набора модулей .NET, которые развертываются в виде одной логической единицы и снабжаются одним номером версии. Формально один из этих модулей называется главным модулем и содержит манифест сборки (а также все необходимые CIL-инструкции, метаданные, заголовки и дополнительные ресурсы). В манифесте главного модуля описаны все остальные связанные модули, от которых зависит его работа. По соглашению второстепенным модулям в многофайловой сборке назначается расширение * . netmodule; обязательным требованием для CLR-среды это, однако, не является. Во второстепенных модулях * .netmodule тоже содержится CIL-код и метаданные типов, а также манифест уровня модуля, в котором перечислены внешние сборки, необходимые данному модулю. Главное преимущество многофайловых сборок состоит в том, что они предоставляют очень эффективный способ для выполнения загрузки содержимого. Например, предположим, что есть машина, которая ссылается на удаленную многофайловую сборку, состоящую из трех модулей, главный из которых установлен на клиенте. Если клиенту понадобится какой-то тип из второстепенного удаленного модуля * .netmodule, CLR-среда по его требованию начнет немедленно загружать на локальную машину в специальное место, называемое кэшем загрузки (download cache) только соответствующий двоичный файл. В случае, если размер каждого модуля * .netmodule составляет 5 Мбайт, преимущество сразу же станет заметным (по сравнению с загрузкой одного файла размером в 15 Мбайт). Другим преимуществом многофайловых сборок является то, что они позволяют создавать модули на различных языках программирования .NET (что может быть удобно в крупных корпорациях, где в разных отделах отдают предпочтение разным языкам .NET). После компиляции отдельные модули все могут легко "объединяться" в одну логическую сборку с помощью компилятора С# командной строки. В любом случае следует понимать, что модули, которые образуют многофайловую сборку, не связываются буквально вместе в один файл (большего размера). Вместо этого многофайловые сборки скорее соотносятся на логическом уровне посредством информации, содержащейся в манифесте главного модуля. На рис. 14.3 схематично показано устройство многофайловой сборки, состоящей из трех модулей, каждый из которых написан на своем языке программирования .NET. К этому моменту должно стать более понятно, как изнутри выглядит любой двоичный файл .NET. Зная об этом, теперь можно переходить к рассмотрению способов построения и конфигурирования различных библиотек кода. Создание и использование однофайловой сборки Чтобы приступить к исследованию мира сборок .NET, давайте вначале попробуем создать однофайловую сборку * . dll (по имени Car Libra r у) с небольшим набором общедоступных типов.
CSharpCarLib.dll Манифест (ссылается на другие связанные файлы) Метаданные типов CIL-код Глава 14. Конфигурирование сборок .NET 507 Многофайловая сборка ► VbNetCarLib.netmodule -► PascalCarLib.netmodule CompanyLogo.bmp Рис. 14.3. В главном модуле внутри манифеста сборки содержится информация обо всех остальных вспомогательных модулях Для построения библиотеки кода в Visual Studio 2010 необходимо создать проект типа Class Library (Библиотека классов), выбрав в меню File (Файл) пункт New Project (Новый проект), как показано на рис. 14.4. Instated Template* л Visual С* Windows Web J Office Cloud Service 11 Reporting Sttverlight Test Per user extensions are currently not allc Name: WA Carlrbrary Location: | CAMyCode Solution name: CarLibrary . .NET Framework 4 » Sort by: Default —-* Ж W ИНИЖЗИЯГЭ 35] Windows Forms Application Visual C* 5] ClMiUbrary VisualC* jj|| ASP.NET Web Application Visual C# Jl| Empty ASP.NET Web Applica... Visual G» ^ SirverbghtAppication VisualC* т*шшл.ЫШШтЛшашиаШшкш • 1 * Types VisualC* 4 A project for creating a C* class library [ Browse™ j J Create directory for solution Add to source control СЖ [ Cancel | Рис. 14.4. Создание библиотеки кода на С# Первым в библиотеку добавим абстрактный базовый класс по имени Саг и определим в нем различные данные состояния с помощью синтаксиса автоматических свойств, а также один абстрактный метод по имени TurboBoost (), предусматриваю-
508 Часть IV. Программирование с использованием сборок .NET щий использование специального перечисления (EngineState), значения в котором будут представлять текущее состояние двигателя автомобиля: using System; using System.Collections .Generic- using System.Linq; using System.Text; namespace CarLibrary { // Представляет состояние двигателя. public enum EngineState { engineAlive, engineDead } // Абстрактный базовый класс в данной иерархии. public abstract class Car { public string PetName {get; set;} public int CurrentSpeed {get; set;} public int MaxSpeed {get; set;} protected EngineState egnState = EngineState.engineAlive; public EngineState EngineState { get { return egnState; } } public abstract void TurboBoost(); public Car () {} public Car(string name, int maxSp, int currSp) { PetName = name; MaxSpeed = maxSp; CurrentSpeed = currSp; } } } Теперь создадим двух непосредственных потомков Car с именами MiniVan (минивэн) и SportsCar (спортивный автомобиль) и переопределим в каждом из них абстрактный метод TurboBoost () так, чтобы он предусматривал отображение соответствующего сообщения в окне сообщений Windows Forms, т.е. вставим в проект новый файл классов С# по имени DerivedCars . cs со следующим кодом: using System; using System.Windows.Forms; namespace CarLibrary { public class SportsCar : Car { public SportsCar(){ } public SportsCar(string name, int maxSp, int currSp) : base (name, maxSp, currSp) { } public override void TurboBoost() { MessageBox.Show("Ramming speed1", "Faster is better..."); // Черепашья скорость! Чем быстрее, тем :лучше... } } public class MiniVan : Car { public MiniVan(){ }
Глава 14. Конфигурирование сборок .NET 509 public MiniVan(string name, int maxSp, int currSp) : base (name, maxSp, currSp){ } public override void TurboBoost () { // У минивэнов возможности ускорения всегда плохие! egriState = EngineState.engineDead; MessageBox.Show ("Eek!", "Your engine block exploded1"); } } } Обратите внимание, что в каждом из подклассов метод TurboBoost () реализуется с использованием класса MessageBox, определение которого содержится в сборке System.Windows.Forms.dll. Для использования в своей сборке типов, определенных внутри такой внешней сборки, в проект CarLibrary обязательно понадобится добавить ссылку на соответствующий двоичный файл с помощью диалогового окна Add Reference (Добавление ссылки), которое показано на рис. 14.5 (чтобы открыть его, выберите в меню Project (Проект) пункт Add Reference (Добавить ссылку)). > Add Reference NET [СОМ | Projects | Browse | Recentj Component Name System. Web.Serv ices System.Windows.Forms.Dat.,. System.Windows.Forms.Dat... S stem,Winders.Forms System.Windows.Input.Mani... System.Windcvvs.Presentation Svstem, Workflow. Activities System.Worirflow.Compcne.,. System.Workflcw.Runtime System.WorkflowServices System.Xa ml 4 1 1 ■■ Version 4.0.0.0 4.0.0.0 4,0.0.0 4.0JO.O 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 4.0.0.0 Runtime v4.0.21006 •/4.0.21006 v4.0.21006 V4.021Q06 v4.0.21006 v4.0.21006 v4.021006 v4.021006 У4Л.21006 v4.021006 у4ЛЛ006 III) Path C:\Prograr C:\Prograr C:\Prograr, -( CAPrograr'"""' C:\Prograr C:\Prograr C:\Prograr C:\Prograr C:\Prcgrar C:\Prograr CAPrograr ▼ ► OK Cancel Рис. 14.5. Добавление ссылок на внешние сборки .NET с помощью диалогового окна Add Reference Очень валено знать то, что в диалоговом окне Add Reference на вкладке .NET отображаются далеко не все присутствующие на машине сборки. Специальные сборки и все сборки, которые размещены в GAC, например, здесь показаны не будут. Скорее, в этом диалоговом окне предлагается список лишь общих сборок, которые среда Visual Studio 2010 была изначально запрограммирована отображать. Поэтому при создании приложений, нуждающихся в использовании какой-то такой сборки, которая не отображается в окне Add Reference, необходимо перейти на вкладку Browse (Обзор) и вручную отыскать интересующий файл * . dll или * . ехе. На заметку! Также следует знать о том, что на вкладке Recent (Недавние ссылки) в диалоговом окне Add Reference предлагается постоянно обновляемый список сборок, которые использовались последними. Он может оказываться очень удобным, поскольку во многих проектах .NET часто приходится применять один и тот же ключевой набор внешних библиотек.
510 Часть IV. Программирование с использованием сборок .NET Исследование манифеста Прежде чем переходить к использованию библиотеки CarLibrary.dllB клиентском приложении, давайте сначала посмотрим, как она устроена изнутри. Для этого скомпилируем проект и загрузим его в утилиту ildasm. ехе (рис. 14.6). Р H:\My Book$\C# Book\C# and the .NET Platfor. File View Help -иинмяжн ► MANIFEST ri 9 CarLibrary ф £ CarLibrary.Car *""№ CarLibrary.EngineState й £ CarLibrary.MiniVan ffi £ CarLibrary,SportsCar .assembly CarLibrary < Рис. 14.6. Проект CarLibrary.dll, загруженный в ildasm.exe Теперь давайте отобразим манифест сборки CarLibrary.dll, дважды щелкнув на элементе MANIFEST. В первом блоке любого манифеста всегда перечисляются все внешние сборки, которые требуются текущей сборки для нормального функционирования. Вспомните, что в сборке CarLibrary.dll используются типы из внешних сборок mscorlib.dll и System.Windows.Forms.dll, поэтому обе они и будут отображаться в манифесте под маркерами .assembly extern: .assembly extern mscorlib .publickeytoken .ver 4:0:0:0 (B7 7A 5C 56 19 34 E0 89 ) .assembly extern System.Windows.Forms .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 4:0:0:0 { Здесь каждый блок . assembly extern сопровождается уточняющими директивами .publickeytoken и .ver. Инструкция .publickeytoken появляется только в том случае, если сборка была сконфигурирована со строгим именем (более подробно о строгих именах будет рассказываться позже в настоящей главе, в разделе "Строгие имена"). Маркер .ver отражает числовой идентификатор версии упоминаемой сборки. После ссылок на внешние сборки идет набор маркеров .custom, в которых перечислены атрибуты сборки (информация об авторском праве, название компании, версия сборки и т.д.). Ниже для примера приведена небольшая часть таких данных в манифесте: .assembly CarLibrary .custom instance void . .custom instance void . .custom instance void . .custom instance void . .custom instance void . .custom instance void . .custom instance void . ..AssemblyDescriptionAttribute... ..AssemblyConfigurationAttribute. ..RuntimeCompatibilityAttribute.. ..TargetFrameworkAttribute... ..AssemblyTitleAttribute... ..AssemblyTrademarkAttribute... ..AssemblyCompanyAttribute...
Глава 14. Конфигурирование сборок .NET 511 .custom instance void ...AssemblyProductAttribute... .custom instance void ...AssemblyCopyrightAttribute... .ver 1:0:0:0 } .module CarLibrary.dll Обычно эти параметры устанавливаются визуальным образом в редакторе свойств текущего проекта. Вернитесь в Visual Studio 2010, дважды щелкните на значке Properties (Свойства) в окне Solution Explorer, и затем щелкните на кнопке Assembly Information (Информация о сборке) на вкладке Application (Приложение), которая открывается по умолчанию. Откроется диалоговое окно Assembly Information (Информация о сборке), показанное на рис. 14.7. Assembly Information Title Description: Company: Product: Copyright Trademark: Assembly version File version: CarLibrary Microsoft CarLibrary Copyright © Microsoft 2009 1 0 0 0 1 0 0 0 : GUID: 892da2c2-c0f7-426e-al9e-0b4fl522a228 Neutral language (None) П Make assembly COM-Visible OK Рис. 14.7. Редактирование информации о сборке в диалоговом окне Assembly Information При сохранении изменений это диалоговое окно обновляет соответствующий образом файл AssemblyInfo.cs, который поддерживается Visual Studio 2010 и может просматриваться за счет разворачивания в окне Solution Explorer узла Properties (Свойства), как показано на рис. 14.8. I Solution Explorer £Э Solution 'CarLibrary' A project) 3 CarLibrary л ^£ Properties Л) AssembryInfo.es [> ui References 1^ Щ Car.cs cjaJ DerrvedCars.es c?5 Solution Explorer »nx Рис. 14.8. Использование редактора свойств приводит к обновлению файла Assemblylnf о. cs В этом файле С# можно обнаружить набор заключенных в квадратные скобки атрибутов .NET, подобных приведенным ниже:
512 Часть IV. Программирование с использованием сборок .NET [assembly: AssemblyTitle("CarLibrary")] [assembly: AssemblyDescnption ("") ] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft")] [assembly: AssemblyProduct("CarLibrary")] [assembly: AssemblyCopyright("Copyright ©Microsoft 2010")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture ("")] О роли атрибутов будет более подробно рассказываться в главе 15, поэтому на данном этапе беспокоиться о деталях не нужно. Главное сейчас понять только то, что большинство из атрибутов в файле Assemblylnf о. cs будет использоваться для обновления значений в блоках . custom внутри манифеста сборки. Исследование CIL-кода Вспомните, что ориентированных на конкретную платформу инструкций в сборке не содержится; вместо этого в ней хранятся инструкции на независящем от платформы общем промежуточном языке (Common Intermediate Language — CIL). Когда исполняющая среда .NET загружает сборку в память, лежащий в ее основе код CIL (с помощью JIT-компилятора) компилируется и преобразуется в инструкции, воспринимаемые целевой платформой. Например, если дважды щелкнуть на методе TurboBoost () в классе SportsCar, то в ildasm.exe откроется новое окно, в котором будут отображаться реализующие данный метод CIL-инструкции: .method public hidebysig virtual instance void TurboBoost() cil managed { // Code size 18 @x12) .maxstack 8 IL_0000: nop IL_0001: ldstr "Ramming speed1" IL_0006: ldstr "Faster is better..." IL_000b: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string, string) IL_0010: pop x IL_0011: ret } // end of method SportsCar::TurboBoost Опять-таки, хотя большинству разработчиков приложений .NET не требуется глубоко разбираться в деталях CIL-кода при повседневной работе, в главе 17 все же будут приведены более подробные сведения о синтаксисе и семантике языка CIL. Понимание грамматики языка CIL может быть полезно при создании более сложных приложений, нуждающихся в поддержке более совершенных служб, таких как конструирование сборок во время выполнения (об этом тоже речь пойдет в главе 17). Исследование метаданных типов Если перед созданием приложений, использующих специальную библиотеку .NET, нажать комбинацию клавиш <Ctrl+M>, в окне ildasm.exe отобразятся метаданные по каждому из имеющихся внутри сборки CarLibrary.dll типов (рис. 14.9). Как будет показано в следующей главе, метаданные сборки являются очень важным элементом платформы .NET и служат основой для многочисленных технологий (вроде сериализации объектов, позднего связывания, создания расширяемых приложений и т.д.). В любом случае, теперь, когда мы заглянули внутрь сборки CarLibrary.dll, можно приступать к созданию клиентских приложений, предусматривающих использование содержащихся внутри сборки типов.
Г j? Metal Глава 14. Конфигурирование сборок .NET 513 l°|BJi ScopeName : CarLibrary.dll MUID : <4ABFCE8E-ODC1-417A-89C0-0DD233E92BA6> Global functions Find Find Next '••I Global fields Global MemberRefs TypeDef 111 @2000002) TypDefNane: Cat-Library.Enginestate (82006002) Flags : [Public] [fiutoLayout] [Class] [Sealed] [finsiClass] @000 Extends : 01000001 [TypeRef] System.Enum Field 81 @1000001) Field Name: ualue_ @4000001) Рис. 14.9. Метаданные типов, имеющихся в сборке CarLibrary.dll Исходный код. Проект CarLibrary доступен в подкаталоге Chapter 14. Создание клиентского приложения на С# Поскольку все типы в CarLibrary были объявлены с ключевым словом public, они могут использоваться также и в других сборках. Вспомните, что типы могут объявляться и с таким поддерживаемым в С# ключевым словом, как internal (которое применяется в С# в качестве модификатора доступа по умолчанию). Типы, объявляемые как internal, могут использоваться только в сборке, где находится их определение. Внешние клиенты не могут ни видеть, ни создавать типы, помеченные ключевым словом internal. На заметку! В .NET поддерживается возможность определения так называемых "дружественных сборок", которые позволяют внутренним типам использоваться в указанном наборе сборок. Более подробную информацию об этом можно найти в документации .NET Framework 4.0 SDK, в разделе, посвященном описанию класса InternalsVisibleToAttribute. Однако следует иметь в виду, что прием с созданием дружественных сборок применяется довольно редко. Чтобы использовать типы из библиотеки CarLibrary, создадим новый проект типа Console Application на С# по имени CSharpCarClient. Добавим в него ссылку на CarLibrary.dll на вкладке Browse диалогового окна Add Reference (если сборка CarLibrary.dll компилировалась в Visual Studio, она будет находиться в подкаталоге \Bin\Debug внутри папки проекта CarLibrary). После этого можно приступать к созданию клиентского приложения, предусматривающего использование внешних типов. Модифицируем исходный файл кода на С#, как показано ниже: using System; using System. Collections .Generic- using System.Linq; using System.Text; //He забывайте импортировать пространство имен CarLibrary! using CarLibrary; namespace CSharpCarClient {
514 Часть IV. Программирование с использованием сборок .NET public class Program { static void Main(string [ ] args) { Console.WriteLine ("***** C# CarLibrary Client App **** // Создание спортивного автомобиля. SportsCar viper = new SportsCar("Viper", 240, 40); viper.TurboBoost(); // Создание минивэна. MiniVan mv = new MiniVanO ; mv.TurboBoost(); Console.ReadLine(); } *"). > M Properties :> ijH References j bin л ; Debug J CarLibrary.dll j CarLibrary.pdb j j СSharpCarClient exe j ) CSharpCarClientpdb j CSharpCarClient.v5host.exe ] CSharpCarClient.vshost.exe.manrfest > ...J obj [ &$ Solution Explorer Д } Этот код выглядит точно так же, как и код других разрабатывавшихся ранее приложений. Единственным представляющим интерес моментом в нем является то, что в клиентском приложении на С# теперь используются типы, определенные внутри отдельной специальной сборки. Давайте попробуем запустить это приложение. Как и следовало ожидать, его выполнение приведет к отображению различных окон с сообщениями. Обратите внимание, что Visual Studio 2010 поместит копию сборки CarLibrary. dll в под- папку \bin\Debug проекта CSharpCarClient. Чтобы удостовериться в этом, щелкните на кнопке Show All Files (Показать все файлы) в окне Solution Explorer (рис. 14.10). Как будет объясняться позже в главе, CarLibrary.dll была сконфигурирована как приватная сборка (поскольку именно такое поведение применяется автоматически для всех проектов типа Class Library в Visual Studio 2010). При добавлении ссылок на приватные сборки в новые приложения (вроде CSharpCarClient.exe), IDE-среда всегда реагирует помещением копии соответствующей библиотеки в выходной каталог клиентского приложения. Рис. 14.10. Все приватные сборки Visual Studio 2010 копирует в каталог клиентского приложения Исходный код. Проект CSharpCarClient доступен в подкаталоге Chapter 14. Создание клиентского приложения на Visual Basic Чтобы проверить языковую независимость платформы .NET, создадим еще одно приложение типа Console Application (по имени visualBasicCarClient), но на этот раз на языке Visual Basic (рис. 14.11). После создания проекта добавим в него ссылку на CarLibrary. dll с помощью диалогового окна Add Reference (выбрав в меню Project пункт Add Reference). Как и в С#, в Visual Basic необходимо перечислять все пространства имен, используемые в текущем файле. Вместо ключевого слова using в Visual Basic для этого предусмотрено ключевое слово Imports.
Глава 14. Конфигурирование сборок .NET 515 Рис. 14.11. Создание консольного приложения на языке Visual Basic Добавим в файл кода Modulel. vb следующий оператор Imports: Imports CarLibrary Module Modulel Sub Main () End Sub End Module Обратите внимание, что метод Main () в Visual Basic определяется в рамках типа Module (который не имеет ничего общего с файлом * . netmodule, используемым в многофайловых сборках). Если говорить вкратце, то тип Module в Visual Basic служит просто для обозначения класса, способного содержать только статические методы. В любом случае, чтобы испробовать типы MiniVan и SportsCar в синтаксисе Visual Basic, модифицируем метод Main () следующим образом: Sub Main () Console.WriteLine("***** VB CarLibrary Client App *****") 1 Локальные переменные объявляются 1 с помощью ключевого слова Dim. myMiniVan.TurboBoost () Dim mySportsCar As New SportsCar () mySportsCar.TurboBoost() Console.ReadLine () End Sub Если теперь скомпилировать и запустить приложение, на экране будет отображаться ряд окон с сообщениями. Более того, для этого нового клиентского приложения тоже будет создана собственная отдельная локальная копия CarLibrary.dll в подкаталоге bin\Debug. Межъязыковое наследование в действии Весьма привлекательным аспектом в разработке для .NET является понятие межъязыкового наследования. Давайте посмотрим, что под этим имеется в виду, создав на языке Visual Basic новый класс, унаследованный от класса SportsCar (который был создан на языке С#). Для этого добавим в текущее приложение на Visual Basic (выбрав в меню Project пункт Add Class (Добавить класс)) новый файл класса по имени
516 Часть IV. Программирование с использованием сборок .NET PerformanceCar. vb, с помощью ключевого слова Inherits изменим исходное определение этого класса так, чтобы он наследовался от типа SportsCar, и, наконец, переопределим метод TurboBoost () с помощью ключевого слова Overrides. Imports CarLibrary ' Этот класс на языке VB унаследован от класса ' SportsCar, который был определен на языке С#. Public Class PerformanceCar Inherits SportsCar Public Overrides Sub TurboBoost() Console.WriteLine ("Zero to 60 in a cool 4.8 seconds...") End Sub End Class Чтобы протестировать этот новый класс, модифицируем код метода Main () в модуле следующим образом: Sub Main () Dim dreamCar As New PerformanceCar() ' Использование унаследованного свойства. dreamCar.PetName = "Hank" dreamCar.TurboBoost() Console.ReadLine() End Sub Обратите внимание, что объект dreamCar способен вызывать любой из общедоступных членов (вроде свойства PetName), находящихся выше в цепочке наследования, невзирая на тот факт, что базовый класс был определен на совершенно другом языке и в совершенно другой сборке! Возможность расширять классы за пределами границ сборок независимым от языка образом является очень полезным аспектом цикла разработки в .NET и существенно упрощает использование скомпилированного кода, написанного людьми, которые не захотели создавать свой разделяемый код на С#. Исходный код. Проект VisualBasicCarClient доступен в подкаталоге Chapter 14. Создание и использование многофайловой сборки После рассмотрения создания и использования однофайловой сборки давайте изучим аналогичные процессы для многофайловой сборки. Вспомните, что под многофайловой сборкой понимается коллекция взаимосвязанных модулей, которые развертываются и снабжаются версией в виде цельной логической единицы. На момент написания книги в IDE-среде Visual Studio никакого отдельного шаблона проекта для создания многофайловой сборки на С# не предусматривалось. Для создания такового пока что должен использоваться компилятор командной строки (esc. ехе). Рассмотрим этот процесс на примере, создав многофайловую сборку по имени AirVehicles. В главном модуле этой сборки (airvehicles.dll) будет содержаться единственный тип класса Helicopter (вертолет) и соответствующий манифест, который указывает на наличие дополнительного файла * . netmodule по имени uf о. netmodule, содержащего еще один класс Uf о. Хотя физически оба класса размещаются в отдельных двоичных файлах, пространство имен у них будет одно — AirVehicles. И, наконец, оба класса будут создаваться на С# (хотя, конечно же, вполне допустимо использовать другие языки). Для начала откроем простой текстовый редактор и создадим следующее определение для класса Uf о, после чего сохраним его в файле с именем uf о. cs:
Глава 14. Конфигурирование сборок .NET 517 using System; namespace AirVehicles { public class Ufo I public void AbductHuman() { Console.WriteLine ("Resistance is futile"); } } } Чтобы скомпилировать этот класс в .NET-модуль, откройте в Visual Studio 2010 окно командной строки, перейдите в папку, где был сохранен файл uf о. cs, и введите следующую команду (опция module флага /t указывает, что должен быть создан файл * . netmodule, а не * . dll или * . ехе): csc.exe /t:module ufo.cs В папке с файлом ufo.cs появится новый файл по имени ufo.netmodule. Теперь давайте создадим новый файл helicopter. cs со следующим определением класса: using System; namespace AirVehicles { public class Helicopter { public void TakeOff() { Console.WriteLine("Helicopter taking off!"); } } } Поскольку было решено, что главный модуль в этой многофайловой сборке будет называться airvehicles .dll, осталось только скомпилировать helicopter, cs с использованием соответствующих опций /t: library и /out:. Чтобы включить информацию о двоичном файле ufo .netmodule в манифест сборки, также потребуется добавить соответствующий флаг /addmodule. Полностью необходимая команда выглядит следующим образом: esc /tilibrary /addmodule:ufo.netmodule /out:airvehicles.dll helicopter.es После выполнения этой команды в каталоге должен появиться главный модуль airvehicles .dll, а также второстепенный двоичный файл uf о. netmodule. Исследование файла ufo. netmodule Теперь откроем файл ufo.netmodule в утилите ildasm.exe. Сразу же можно будет заметить, что в нем содержится манифест уровня модуля, единственной задачей которого является перечисление всех внешних сборок, которые упоминаются в кодовой базе. Поскольку в классе Ufo был практически добавлен только вызов Console . WriteLine (), обнаружится следующая ключевая информация: .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 EO 89 ) .ver 4:0:0:0 } .module ufo.netmodule
518 Часть IV. Программирование с использованием сборок .NET Исследование файла airvehicles. dll Далее откроем в ildasm.exe файл главного модуля airvehicles .dll и изучим содержимое манифеста уровня всей сборки. В нем важно обратить внимание, что маркер .file используется для представления информации о других ассоциируемых с многофайловой сборкой модулях (в данном случае — ufo .netmodule), а маркеры .class extern — для перечисления имен внешних типов, которые упоминаются как используемые во второстепенном модуле (в данном случае — Ufo). Ниже приведена соответствующая информация. .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 EO 89 ) .ver 4:0:0:0 } .assembly airvehicles .hash algorithm 0x00008004 .ver 0:0:0:0 file ufo.netmodule class extern public AirVehicles.Ufo .file ufo.netmodule .class 0x02000002 } .module airvehicles.dll Манифест сборки является единственной сущностью, которая связывает airvehicle,s .dll и ufo.netmodule вместе. В один файл * .dll большего размера эти два двоичных файла не объединялись. Использование многофайловой сборки Пользователям многофайловой сборки нет никакого дела до того, состоит ли сборка, на которую они ссылаются, из многочисленных модулей. Чтобы не усложнять пример, построим новое клиентское приложение на языке С# в командной строке. Для начала создадим новый файл по имени Client.cs со следующим определением модуля и сохраним его в месте, где находится многофайловая сборка: using System; usj-ng AirVehicles; class Program { static void Main() { Console.WriteLine ("***** Multifile Assembly Client *****"); Helicopter h = new Helicopter (); h.TakeOff(); // Благодаря этому коду, модуль *.netmodule будет загружаться по требованию. Ufo u = new Ufo () ; u.AbductHuman(); Console.ReadLine(); }
Глава 14. Конфигурирование сборок .NET 519 Чтобы скомпилировать эту исполняемую сборку в командной строке, запустим компилятор esc. ехе с помощью следующей команды: esc /rrairvehicles.dll Client.cs Обратите внимание, что при добавлении ссылки на многофайловую сборку компилятору необходимо указать только имя главного модуля (второстепенные модули * .netmodule будут загружаться CLR-средой по требованию, когда возникнет необходимость в их использовании клиентским кодом). Сами по себе модули * . netmodule не обладают никаким индивидуальным номером версии и потому не могут напрямую загружаться CLR-средой. Отдельные модули * . netmodule могут загружаться только главным модулем (например, файлом, в котором содержится манифест сборки). На заметку! В Visual Studio 2010 также позволяет добавлять ссылку на многофайловую сборку. Для этого потребуется открыть окно Add Reference и выбрать главный модуль. Все остальные связанные с ним второстепенные модули * .netmodule скопируются автоматически по ходу процесса. К этому моменту должно стать более понятно, что собой представляет процесс создания однофайловых и многофайловых сборок. По-правде говоря, в большинстве случаев требуется создавать только однофайловые сборки. Тем не менее, многофайловые сборки полезны, когда большой физический двоичный файл необходимо разбить на меньшие модульные единицы (это довольно удобно в сценариях с удаленной загрузкой). Теперь давайте разберемся с понятием приватных сборок. Исходный код. Файлы кода Multif ileAssembly доступны в подкаталоге Chapter 14. Приватные сборки С технической точки зрения библиотеки кода, создаваемые до сих пор в этой главе, развертывались в виде приватных сборок. Приватные сборки должны всегда размещаться в том же каталоге, что и клиентское приложение, в котором они используются (т.е. в каталоге приложения), или в каком-то из его подкаталогов. Вспомните, что при добавлении ссылки на CarLibrary.dll во время создания приложений CSharpCarClient.exe и VbNetCarClient.exe Visual Studio 2010 (по крайней мере, после первой компиляции) создает копию CarLibrary.dll внутри каталога каждого из этих клиентских приложений. Благодаря этому, при возникновении в какой-то из этих клиентских программ необходимости в использовании типов, определенных во внешней сборке CarLibrary. dll, CLR-среда будет просто загружать локальную копию этой сборки. Отсутствие у исполняющей среды .NET потребности заглядывать в системный реестр при поиске внешних сборок позволяет перемещать CSharpCarClient. ехе (или VisualBasicCarClient. ехе) вместе с CarLibrary .dll в любое другое место на машине и все равно успешно запускать эти приложения (такой процесс часто называют развертыванием с помощью Хсору). Удаление (или репликация) приложения, в котором используются приватные сборки, тоже не требует особых усилий: достаточно просто удалить (или скопировать) папку приложения. В отличие от СОМ-приложений, беспокоиться о десятках остающихся настроек в системном реестре совершенно не нужно. Более того, удаление приватных сборок не может нарушить работу каких-то других приложений на машине.
520 Часть IV. Программирование с использованием сборок .NET Идентификационные данные приватной сборки Идентификационные данные приватной сборки включают дружественное имя и номер версии, которые фиксируются в манифесте сборки. Дружественным называется имя модуля, в котором содержится манифест сборки, без файлового расширения. Например, заглянув в манифест сборки CarLibrary. dll, там можно будет обнаружить следующее: .assembly CarLibrary { .ver 1:0:0:0 } По причине изолированной природы приватной сборки CLR-среда, вполне логично, не использует номер версии при выяснении места ее размещения. Она предполагает, что приватные сборки не нуждаются в выполнении сложной проверки версии, поскольку клиентское приложение является единственной сущностью, которой "известно" об их существовании. Из-за этого на одной машине может размещаться множество копий одной и той же сборки в различных каталогах приложений. Процесс зондирования Исполняющая среда .NET определяет местонахождение приватной сборки с применением так называемой технологии зондирования, которая в действительности является менее докучливой, чем может показаться из-за ее названия. В ходе процесса зондирования производится отображение запроса внешней сборки на место размещения соответствующего двоичного файла. Запрос внешней сборки, собственно говоря, может быть явным или неявным. Неявный запрос происходит тогда, когда CLR-среда заглядывает в манифест для определения, где находится требуемая сборка, по маркерам .assembly extern: // Неявный запрос на выполнение загрузки. .assembly extern CarLibrary { ... } Явный запрос осуществляется программно за счет применения метода Load () или LoadFromO (оба являются членами класса System.Reflection.Assembly) и обычно предназначен для выполнения позднего связывания или динамического вызова членов интересующего типа. Более подробно об этом речь пойдет в главе 15, а пока что ниже приведен простой пример того, как может выглядеть явный запрос: // Явный запрос на выполнение загрузки, основанный //на использовании дружественного имени. Assembly asm = Assembly.Load("CarLibrary"); В обоих случаях CLR-среда извлекает дружественное имя сборки и начинает зондировать каталог клиентского приложения в поисках интересующего файла (например, файла по имени CarLibrary.dll). Если ей не удается обнаружить такой файл, она предпринимает попытку найти исполняемую сборку с таким же 'дружественным именем (т.е., например, CarLibrary. ехе). Если ей не удается найти ни одного из упомянутых файлов в каталоге приложения, она генерирует во время выполнения исключение FileNotFoundException. На заметку! С технической точки зрения, если копия запрашиваемой сборки не обнаружена в каталоге клиентского приложения, CLR-среда будет также пытаться найти подкаталог с именем, совпадающим с дружественным именем запрашиваемой сборки (например, С: \MyClient\ CarLibrary). Обнаружив такой подкаталог, CLR-среда будет загружать запрашиваемую сборку в память из него.
Глава 14. Конфигурирование сборок .NET 521 Конфигурирование приватных сборок Хотя допускается развертывания .NET-приложения просто за счет копирования всех требуемых сборок в одну папку на жестком диске пользователя, скорее всего, понадобится определить несколько отдельных подкаталогов для группирования взаимосвязанного содержимого. Например, предположим, что имеется каталог приложения под названием С: \MyApp, в котором содержится CSharpCarClient. exe. Тогда в этом каталоге может также присутствовать и подкаталог MyLibraries, содержащий сборку CarLibrary.dll. Несмотря на подразумеваемую взаимосвязь между двумя этими каталогами, CLR- среда не будет зондировать подкаталог MyLibraries, если только ей не будет предоставлен конфигурационный файл с соответствующим указанием. Конфигурационные файлы включают в себя различные XML-элементы, которые позволяют влиять на процесс зондирования. Эти файлы должны обязательно иметь такое же имя, как запускающее их приложение, и расширение *.config, а также развертываться в каталоге приложения клиента. То есть если нужно создать конфигурационный файл для приложения CSharpCarClient.exe, потребуется называть его CSharpCarClient. exe . config и разместить (в рассматриваемом примере) в каталоге С: \MyApp. Чтобы увидеть, как этот процесс выглядит на практике, давайте создадим на диске С: с помощью проводника Windows новый каталог по имени МуАрр, после чего скопируем в него CSharpCarClient.exe и CarLibrary.dll и запустим программу, дважды щелкнув на ее исполняемом файле. В этот раз выполнение программы должно пройти успешно (вспомните, что сборки не регистрируются). Далее создадим в С: \MyApp новый подкаталог по имени MyLibraries, как показано на рис. 14.12, и переместим сборку CarLibrary.dllв него. нИСТ**^^^ JW Organize л £| Mongo Drive (С:) DeusEx > М Dot Net Stuff t> i» Games! > Ji! inetpub ■й - а Ф \\ MyLibraries ■ MyApp и CSharpCar Client.exe I. MyLibraries > Jfc PerfLogs t Program Files Prnnram Fil^, fKflfil MyLibraries Date modified: 12/27/2009 5:49 PM ft File folder Рис. 14.12. Теперь CarLibrary.dll размещается в подкаталоге MyLibraries Теперь попробуем запустить программу снова. На этот раз CLR-среде не удается обнаружить сборку CarLibrary непосредственно в каталоге приложения и потому она сгенерирует исключение FileNotFoundException. Чтобы указать CLR-среде, что следует зондировать подкаталог MyLibraries, создадим новый конфигурационный файл по имени CSharpCarClient. exe . config с помощью любого текстового редактора и сохраним его в той лее папке, в которой содержится само приложение CSharpCarClient. exe (в данном примере С: \MyApp). Откроем этот файл и введем в него следующий код (обратите внимание, что язык XML чувствителен к регистру символов):
522 Часть IV. Программирование с использованием сборок .NET <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl"> <probing private Pa th=llMyLibraries"/> </assemblyBinding> </runtime> </configuration> Файлы * . conf ig в .NET всегда начинаются с корневого элемента <conf igurationx Вложенный в него элемент <runtime> может содержать элемент <assemblyBinding>, a тот, в свою очередь — вложенный элемент <probing>. В данном примере главный интерес представляет атрибут privatePath, поскольку именно он служит для указания подкаталогов внутри каталога приложения, которые должна зондировать CLR. Теперь, когда конфигурационный файл CSharpCarClient .exe . conf ig готов, давайте снова попробуем запустить клиентское приложение. На этот раз выполнение CSharpCarClient .exe должно пройти без проблем (если это не так, вернитесь и проверьте конфигурационный файл еще раз на предмет опечаток). Обратите особое внимание, что то, какая именно сборка размещается в каждом подкаталоге, в элементе <probing> не указывается. Другими словами, утверждать, что в подкаталоге MyLibraries размещена сборка CarLibrary, а в подкаталоге OtherStuf f — сборка MathLibrary нельзя. Элемент <probing> просто указывает CLR-среде искать запрашиваемую сборку во всех перечисленных подкаталогах до тех пор, пока не будет найдено первое совпадение. На заметку! Очень важно иметь в виду, что атрибут privatePath не допускается применять для указания ни абсолютного (С: \Папка\Подпапка), ни относительного (. . \\ОднаПапка\\ ДругаяПапка) пути. Чтобы указать каталог, находящийся за пределами каталога клиентского приложения, необходимо использовать совершенно другой XML-элемент — <codeBase> (который более подробно рассматривается позже в главе). В атрибуте privatePath можно указывать несколько подкаталогов в виде разделенного точками с запятой списка. В данном случае этого делать не требуется, но ниже приведен пример, в котором CLR-среде указывается просматривать клиентские подкаталоги MyLibraries и MyLibraries\Tests: <probmg privatePath="MyLibraries; MyLibraries\Tests"/> Изменим для целей тестирования имя конфигурационного файла и попробуем запустить приложение снова. На этот раз выполнение клиентского приложения должно завершиться неудачей. Вспомните, что имя файла * . conf ig должно включать в качестве префикса имя соответствующего клиентского приложения. И, наконец, давайте попробуем открыть конфигурационный файл для редактирования и заменить символы любого XML-элемента на прописные. В этом случае запуск клиентского приложения должен завершиться неудачей (язык XML чувствителен к регистру символов). На заметку! Важно понимать, что CLR-среда будет загружать сборку, которая во время процесса зондирования обнаруживается первой. Например, если в папке С: \MyApp имеется копия CarLibrary.dll, то она и будет загружаться в память, а копия, содержащаяся в папке MyLibraries, будет проигнорирована. Конфигурационные файлы и Visual Studio 2010 Хотя конфигурационные файлы XML можно всегда создавать вручную в любом предпочитаемом текстовом редакторе, Visual Studio 2010 позволяет это делать автомати-
Глава 14. Конфигурирование сборок .NET 523 чески прямо во время разработки клиентской программы. Давайте загрузим решение CSharpCarClient в Visual Studio 2010 и вставим в него новый элемент типа Application Configuration File (Конфигурационный файл приложения), выбрав в меню Project пункт Add New Item, как показано на рис. 14.13. Add New Hem-CSharpCarClient M litttaled TempUtet 1 л Visual С* Hems Code Data General Web Windows Forms WPF Reporting Workflow BSBHBI Per user extensions are currently not ail Name: App.config Sort by. Default 3 Windows Script Host 4Cl*j Debugger Visualizer Hffl Installer Class j Application Configuration File I 4ijfl Component Class I 1ЭД Application Manifest File 1 лД Preprocessed Text Template owed to load. Enable loading of per циаи* GD Visual C# hems j" Visual C* Items Visual C# hems Visual C* Items Visual C# hems Visual C# Hems Visual C# Hems [ Search installed Templates J Type: Visual C* Items E j A file for storing application ( and settings values [ Add '-^Mf.W1 -3 1 onfiguration Cancel 1 Рис. 14.13. Вставка нового файла Арр. conf ig в проект Visual Studio 2010 Перед щелчком на кнопке О К важно обратить внимание, что файлу было назначено имя App.config (его ни в коем случае не следует изменять). Если затем заглянуть в окно Solution Explorer, можно увидеть, что файл Арр. conf ig был вставлен в текущий проект (рис. 14.14). Теперь можно приступать к вводу необходимых XML-элементов для разрабатываемого клиента. Замечательно, что при каждой компиляции проекта Visual Studio 2010 будет автоматически копировать данные из App.config в каталог \bin\Debug и назначать создаваемым копиям соответствующие принятым соглашениям имена, вроде CSharpCarClient .exe . conf ig, как показано на рис. 14.15. Однако подобное будет происходить только в случае, если конфигурационный файл действительно называется Арр.config. Solution Explorer ЕОЕИННН ^3 Solution 'CSharpCarClient' A project) a  CSharpCarClient о ,M Properties эл References > ,_j bin -j obj j App.config l] Program.cs ■ Solution Explorer Рис. 14.14. Редактирование файла Арр. conf ig с целью сохранения необходимых данных для клиентских приложений Solution Explorer 3 Solution 'CSharpCarClient' A project) л .3 CSharpCarClient > "Ш Properties jy| References * ;■:3 bin j 3j Debug j CarLibrary.dll j CarLibrary.pdb J CSharpCarClient.exe CSharpCarCltent.exe.config ) CSharpCarClient.pdb j CSharpCarClient.vshost.exe Jj CSharpCarClientA'shost.exe.manifest _: Obj j» App.config i£ Program.cs ^9 -J Solution Explorer I Рис. 14.15. Содержимое Арр. conf ig будет копироваться в именуемый надлежащим образом файл * . conf ig, который находится в выходном каталоге проекта
524 Часть IV. Программирование с использованием сборок .NET При таком подходе все, что требуется — это лишь поддерживать файл Арр. con fig, и тогда среда Visual Studio 2010 сама позаботиться о том, чтобы в каталоге приложения содержались самые последние и актуальные конфигурационные данные (даже если проект будет переименован). На заметку! Использовать файлы Арр. con fig в Visual Studio 2010 рекомендуется всегда. В случае добавления файла * . conf ig в папку bin\Debug вручную через окно проводника Windows при следующей компиляции проекта Visual Studio 2010 может удалить либо модифицировать его! Разделяемые сборки Теперь, когда уже известно о том, как развертывать и конфигурировать приватные сборки, можно перейти к рассмотрению роли так называемых разделяемых: сборок. Подобно приватной сборке, любая разделяемая сборка представляет собой коллекцию типов и (необязательно) ресурсов. Самое очевидное отличие между разделяемой и приватной сборкой состоит в том, что одна копия разделяемой сборки может использоваться сразу в нескольких приложениях на одной и той же машине. Давайте вспомним все приложения, которые создавались до сих пор в настоящей книге, и в которые требовалось добавлять ссылку на сборку System. Windows . Forms . dll. Если заглянуть в каталог любого из этих клиентских приложений, то никакой приватной копии данной сборки .NET там не обнаружится. Это объясняется тем, что сборка System. Windows . Forms . dll является разделяемой. Очевидно, что при возникновении необходимости в создании библиотеки классов, пригодной для использования в масштабах всей машины, именно такую сборку и нужно использовать. На заметку! Принятие решения о том, развертывать библиотеку кода как приватную или как разделяемую сборку, является еще одним вопросом, который должен быть продуман на этапе проектирования, и ответ на него зависит от множества специфических деталей проекта. Как правило, при создании библиотек, подлежащих использованию во множестве различных приложений, может быть удобнее использовать разделяемые сборки, так как их очень легко обновлять до новых версий (это будет показано далее в главе). Как не трудно догадаться, разделяемые сборки не развертываются внутри того же самого каталога, что и приложения, в которых они должны использоваться. Вместо этого они устанавливаются в так называемом глобальном кэше сборок (Global Assembly Cache — GAC). Этот кэш размещен в каталоге Windows внутри подкаталога под названием Assembly (например, путем к нему может быть С : \Windows\Assembly), как показано на рис. 14.16. На заметку! Устанавливать в GAC исполняемые сборки (* . ехе) не разрешено. В качестве разделяемых можно развертывать только сборки с расширением * . dll. Строгие имена Прежде чем развертывать сборку в GAC, ей обязательно необходимо назначить строгое имя (strong name), которое позволяет уникальным образом идентифицировать издателя данного двоичного файла .NET. Следует иметь в виду, что в роли "издателя" может выступать как отдельный программист, так и подразделение компании или вообще целиком вся компания.
Глава 14. Конфигурирование сборок .NET 525 I fjj jY. ► Computer ► 1 Organize ▼ ^ Open Jtl inetpub"* " l MyApp ± PerfLogs Mongo Drive (C:) ► Windows ► assembly ► Share with » Burn Compatibility files j» Program Files i, Program Fries (x86) k ProgramDataTechSmith Users addins t AppCompat AppPatch assembly Boot fa Branding Ъ 1 item selected * Assembly Name ' ^Accessibility Лаооов Г1 i^AuditPolicyGPMana.. fj dftAudrtPolicyGPMana.. dbBDATunePIA UUBDATunePIA :ij ComSvcConfig db CppCodeProvider jJUcscompmgd ifh С ustomMars haters 4Ь С usto m M arshal ers Ad*> ' dbdfsvc • ^ 4hehOR New folder Version Cut... 2.0.0.0 7Д330... 6100 610 0 6100 61DO ЗЛЛЛ III 2000 10.0.45... 2.0.0.0 6100 ~M Public Key Token b03f5f7flld50a3a b03f5f7flld50a3a 31bf3856ad364e35 31bf3856ad364e35 31bf3856ad364e35 31bf3856ad364e35 b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7flld50a3a 31bf3856ad364e35 b03f5f7flld50a3a 31bf3856ad364e35 1-oiah Ш* a Proces... MSIL ЛЙ6 AMD64 x86 AMD64 MSIL MSIL MSIL x86 AMD64 MSI MSIL • t 4 li 1* '1 Рис. 14.16. Глобальный кэш сборок В некотором отношении строгое имя является современным .NET-эквивалентом глобально уникальных идентификаторов (GUID), которые применялись в СОМ. Те, кому приходилось работать с СОМ, наверняка помнят, что глобально уникальными идентификаторами приложений называются идентификаторы, которые характеризуют конкретные СОМ-приложения. В отличие от GUID-значений в СОМ (которые представляют собой 128-битные числа), строгие имена в .NET основаны (отчасти) на двух взаимосвязанных криптографических ключах, называемых открытым (public) и секретным (private) ключом и являющихся гораздо более уникальными и устойчивыми к подделке по сравнению с простыми идентификаторами GUID. Формально любое строгое имя состоит из набора взаимосвязанных данных, большая часть из которых указывается с помощью перечисленных ниже атрибутов уровня сборки. • Дружественное имя сборки (которое представляет собой имя сборки без файлового расширения). • Номер версии сборки (назначается в атрибуте [AssemblyVersion]). • Значение открытого ключа (назначается в атрибуте [AssemblyKeyFile]). • Значение, обозначающее культуру, которое является необязательным и может предоставляться для локализации приложения (присваивается в атрибуте [AssemblyCulture]). • Вставляемая цифровая подпись, созданная с использованием хеш-кода по содержимому сборки и значения секретного ключа. Для создания строгого имени сборки сначала генерируются данные открытого и секретного ключей с помощью поставляемой в составе .NET Framework 4.0 SDK утилиты sn. ехе. Эта утилита генерирует файл, который обычно оканчивается расширением * . snk (Strong Name Key — ключ строгого имени) и содержит данные для двух разных, но математически связанных ключей — "открытого" и "секретного". После указания местонахождения этого файла * . snk компилятору С# тот запишет полное значение открытого ключа в манифест сборки с использованием дескриптора .publickey. Кроме того, компилятор С# генерирует на основе всего содержимого сборки (CIL- кода, метаданных и т.д.) соответствующий хеш-код. Как упоминалось в главе 6, хеш- кодом называется числовое значение, которое является статистически уникальным для
526 Часть IV. Программирование с использованием сборок .NET фиксированных входных данных. Следовательно, в случае изменения какого-то аспекта сборки .NET (даже одного символа в строковом литерале), компилятор выдает другой хеш-код. Далее этот хеш-код объединяется с содержащимися внутри файла * . snk данными секретного ключа для получения цифровой подписи, вставляемой в сборку внутрь данных заголовка CLR. На рис. 14.17 схематично показано, как выглядит процесс создания строгого имени. CarLibrary.dll Манифест с открытым ключом Метаданные типов CIL-код Цифровая подпись Рис. 14.17. Во время компиляции на основе данных открытого и секретного ключей генерируется цифровая подпись, которая затем вставляется в сборку Важно понимать, что данные секретного ключа сами нигде в манифесте не встречаются, а служат только для снабжения содержимого сборки цифровой подписью (вместе с генерируемым хеш-кодом). Суть использования открытого и секретного ключей состоит просто в исключении вероятности наличия у двух компаний, подразделений или отдельных программистов одинаковых идентификационных данных в мире .NET. В любом случае по завершении процесса создания и назначения строгого имени сборка может устанавливаться в GAC. На заметку! Строгие имена также обеспечивают определенную степень защиты от возможной подделки содержимого сборок. Поэтому в .NET наилучшим практическим приемом считается назначение строгих имен всем сборкам (в том числе и сборкам . ехе), независимо от того, будут они развертываться в GAC или нет. Генерирование строгих имен в командной строке Рассмотрим теперь процесс назначения строгого имени на примере сборки CarLibrary, которая была создана ранее в этой главе. В нынешнее время предпочтение практически наверняка будет отдаваться генерированию необходимого файла * . snk в Visual Studio 2010. Однако в прежние времена (примерно до 2003 г.) назначать сборке строгое имя можно было только в командной строке. Давайте посмотрим, как это делается. В первую очередь должны быть сгенерированы необходимые данные ключей с помощью утилиты sn. ехе. Хотя эта утилита обладает множеством опций командной строки, в настоящий момент основной интерес представляет только флаг -к, который заставляет эту утилиту генерировать новый файл с информацией об открытом и секретном ключах. Для примера создадим на диске С: новую папку по имени MyTestKeyPair, перейдем в нее в окне командной строки Visual Studio 2010 и введем следующую команду, чтобы сгенерировать файл MyTestKeyPair. snk: sn -k MyTestKeyPair.snk Хеш-код сборки Данные секретного ключа Цифровая подпись
Глава 14. Конфигурирование сборок .NET 527 Теперь, имея данные ключей, необходимо проинформировать компилятор С# о том, где расположен файл MyTestKeyPair. snk. Как уже рассказывалось ранее в настоящей главе, при создании любого нового проекта на С# в Visual Studio 2010 в числе первоначальных файлов (отображаемых в узле Properties окна Solution Explorer) всегда автоматически создается файл по имени Assembly Info . cs. В этом файле содержится набор атрибутов, описывающих саму сборку. С помощью атрибута AssemblyKeyFile можно сообщить компилятору о местонахождении действительного файла * . snk. Путь должен быть указан в виде строкового параметра, например: [assembly: AssemblyKeyFile(@"C:\MyTestKeyPair\MyTestKeyPair.snk")] На заметку! Когда значение атрибута [AssemblyKeyFile] задается вручную, Visual Studio 2010 будет генерировать предупреждение, которое информирует о том, что необходимо использовать для csc.exe опцию /keyfile или создать файл ключей в окне Properties. Об этом речь пойдет позже, а пока генерируемое предупреждение можно проигнорировать. Из-за того, что в состав строгого имени должен обязательно входить номер версии разделяемой сборки, указание версии для сборки CarLibrary.dll является важной деталью. В файле Assemblylnf о. cs доступен еще один атрибут под названием AssemblyVersion. Первоначально в нем в качестве номера версии для сборки устанавливается значение 1.0.0.0: [assembly: AssemblyVersion (.0.0.0")] Вспомните, что в .NET номер версии состоит из четырех частей (старшего номера, младшего номера, номера сборки и номера редакции). Хотя указывать номер версии нужно самостоятельно, за счет использования группового символа (*) вместо конкретных значений можно позволить Visual Studio 2010 автоматически увеличивать номер сборки и редакции во время каждой компиляции. В рассматриваемом примере этого делать не требуется, но в принципе это может выглядеть следующим образом: // Формат номера версии: // <старший номер>.<младший номер>.<номер сборки>.<номер редакции> //В каждой из этих частей допускается указывать значения // от 0 до 65535. [assembly: AssemblyVersion(.0.*")] Теперь у компилятора С# есть вся необходимая информация для генерации строгого имени (поскольку конкретная культура в атрибуте [AssemblyCulture] не указывалась, компилятор будет использовать культуру, которая установлена на текущей машине). Давайте скомпилируем библиотеку кода CarLibrary, откроем ее в ildasm.exe и заглянем в манифест. Теперь можно увидеть новый дескриптор publickey, содержащий всю информацию об открытом ключе, и дескриптор . ver, отражающий номер версии, который был задан в атрибуте [AssemblyVersion] (рис. 14.18). Вот и замечательно! Теперь можно развертывать разделяемую сборку CarLibrary. dl l в GAC. Однако вспомните о том, что в нынешние дни для создания сборок со строгими именами у разработчиков приложений .NET есть возможность применять Visual Studio, а не утилиту командной строки sn. ехе. Прежде чем опробовать этот способ создания строгих имен, обязательно удалите (или закомментируйте) следующую строку кода в файле Assembly Info . cs (если она была добавлена): // [assembly: AssemblyKeyFile(@"C:\MyTestKeyPair\MyTestKeyPair.snk")]
528 Часть IV. Программирование с использованием сборок .NET Find Find Next // — The following custon attribute is added automatically, do not uncoiment // .custon instance uoid [mscorlibJSysten.Diagnostics.Debuggablenttribute::.ctor(uali ■1!ДД1Н!Иг1- (OB 2d Ot 88 M 80 BO 00 9i| ИО Й0 00 ПА 02 08 88 // .$ aa 2* 88 aa 52 53 *1 31 aa 8*000001 00 01 00 // .$..rsai 70 31 62 7F 01 8B 1B ЕЕ СЙ 57 5H 8Й 9B 2C B6 HS // >1b WT....E F7 *■ 25 77 F3 D7 21 6C 7D 39 52 E8 5Й 21 83 ИЗ // .в*м..!1>9R.Z!.. 9F 7F 51 16 2k 2E 83 2E B9 6Й CM D1 24 D7 18 42 // ..Q.$ j..$..B B8 Й6 79 27 flD C8 4Й 91 91 DB FH BE 88 59 45 81 // ..y'..J VE. 52 68 86 81 58 D8 4F BE DF 11 13 F4 3D 26 45 ЙЙ // Rh..P.0 ftl . D1 D1 FD 3F 7B 7C 66 B7 8C 5Й 38 DA 9E 1D 4F 49 // ...?{|f..Z8...01 i 4E 19 2B ЙВ SB 39 E2 F1 СЙ flO 9D F7 ВО С8 53 89 // N.+ ..9 S. 94 88 79 DF 32 1F 68 B4 75 Й9 E8 EF 98 21 38 B9 ) // ..y.2.h.U 10. .hash algorithm 8x88888684 .ver 1:8:8:8 nodule CarLibrary.dll Рис. 14.18. Если сборке назначено строгое имя, в ее манифест записывается информация об открытом ключе Генерирование строгих имен с помощью Visual Studio 2010 В Visual Studio 2010 можно как указывать путь к какому-нибудь уже существующему файлу *.snk на странице свойств проекта, так и генерировать новый файл * . snk. Чтобы создать новый файл * . snk для проекта CarLibrary, дважды щелкните на значке Properties (Свойства) в окне Solution Explorer, перейдите на вкладку Signing (Подпись), отметьте флажок Sign the assembly (Подписать сборку) и выберите в раскрывающемся списке вариант New (Новый), как показано на рис. 14.19. CarLibrary* X п [ Ц/А » Platform: ; N/A Ш Sign the Choose a strong name key file _ < Browse.. > ~\£ When delay signed, the project will not run or be debuggable. ■ ord... j Рис. 14.19. Создание нового файла * . snk в Visual Stuido 2010 После этого откроется окно с приглашением указать имя для нового файла * . snk (например, myKeyFile . snk) и флажком Protect my key file with a password (Защитить файл ключей с помощью пароля), отмечать который в рассматриваемом примере необязательно (рис. 14.20). После этого новый файл * . snk появится в окне Solution Explorer, как показано на рис. 14.21, и при каждой компоновке приложения эти данные будут использоваться для назначения сборке надлежащего строгого имени. На заметку! Вспомните, что на вкладке Application (Приложение) в окне Properties имеется кнопка Assembly Information (Информация о сборке). В результате щелчка на ней открывается диалоговое окно, в котором можно устанавливать для сборки многочисленные различные атрибуты, вроде номера версии, информации об авторских правах и т.д
Глава 14. Конфигурирование сборок .NET 529 Create Strong Name Key Key file name: myKeyPair.snkj 1 [j Protect my key file with a password Enter password: Confirm password; |__ ___, L OK Щ-ШШГ ] 1 ( Cancel 1 Рис. 14.20. Указание имени для нового файла *. snk в Visual Studio 2010 1 Solution Explorer >>H I /5 Solution 'CarLibrary' A project) 1 л ^СлгНЬтяту 3i Properties > \M References <£) Cer.cs С|Ц DerivedCars.es |Ц myKeyPair.snk Я - ,J Solution Explorer -n*| ■LflHI швзшш Рис. 14.21. Теперь сборке при каждой компиляции в Visual Studio 2010 будет автоматически назначаться строгое имя Установка сборок со строгими именами в GAC Напоследок осталось лишь установить теперь имеющую строгое имя сборку CarLibrary.dll в GAC. Хотя предпочитаемым способом для развертывания сборок в GAC в производственной среде является создание инсталляционного пакета Windows MSI (или применение какой-то коммерческой инсталляционной программы, подобной InstallShield), в .NET Framework 4.0 SDK поставляется работающая утилита командной строки gacutil .exe, которой удобно пользоваться для проведения быстрых тестов. На заметку! Для взаимодействия с GAC на своей машине необходимо иметь права администратора, что может требовать внесения соответствующих изменений в параметры контроля учетных записей пользователей (UAC) в Windows Vista или Windows 7. В табл. 14.1 перечислены некоторые наиболее важные опции этой утилиты (для вывода полного списка поддерживаемых ею опций служит флаг /?). Таблица 14.1. Опции, которые принимает утилита gacutil .exe Опция Описание /i /и /1 Инсталлирует сборку со строгим именем в GAC Удаляет сборку из GAC Отображает список сборок (или конкретную сборку) в GAC
530 Часть IV. Программирование с использованием сборок .NET Чтобы инсталлировать сборку со строгим именем с помощью gacutil.exe, нужно открыть в Visual Studio окно командной строки и перейти в каталог, в котором содержится библиотека CarLibrary.dll, например: cd С:\MyCode\CarLibrary\bin\Debug Затем можно инсталлировать библиотеку с помощью опции -i: gacutil -i CarLibrary.dll После этого можно проверить, действительно ли библиотека была развернута, выполнив следующую команду (обратите внимание, что расширение файла при использовании опции /1 не указывается): gacutil -1 CarLibrary Если все в порядке, в окне консоли должен появиться примерно такой вывод (разумеется, значение PublicKeyToken будет уникальным): The Global Assembly Cache contains the following assemblies: carlibrary, Version=l.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9, processorArchitecture=MSIL В глобальном кэше сборок (GAC) содержатся следующие сборки: carlibrary, Version=l.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9, processor-Architecture=MSIL Просмотр содержимого GAC с помощью проводника Windows Как было описано в предыдущих изданиях этой книги, посвященных версиям .NET 1.0 — .NET 3.5, на этом этапе обычно нужно было открыть проводник Windows и найти установленную сборку CarLibrary в GAC, перейдя в папку С: \Windows\assembly. Однако (и это очень важно) если проделать подобное теперь, выяснится, что ожидаемого значка CarLibrary в папке С: \Windows\assembly нет, несмотря на то, что утилита gacutil.ехе подтвердила установку этой библиотеки. Дело в том, что с выходом версии .NET 4.0 кэш GAC был поделен на две части. В частности, в папке С: \Windows\assembly теперь размещаются библиотеки базовых классов .NET 1.0 — .NET 3.5 (а также различные дополнительные сборки, в том числе библиотеки сторонних производителей). Однако при компиляции сборки в .NET 4.0 и ее развертывания в GAC с помощью gacutil .ехе она размещается в совершенно новом месте, а именно — в папке С: \Windows\Microsoft .NET\assembly\GAC_MSIL. В этой новой папке также имеется набор подкаталогов, каждый из которых именован в соответствии с дружественным именем конкретной библиотеки кода (например, \System. Windows . Forms/\System. Core и т.д.). Внутри каждой папки с дружественным именем размещен еще один подкаталог, который всегда именуется по следующей схеме: v4.0_major.minor.build.revision publicKeyTokenValue v4.0_<старший номер>.<младший номер>.<номер сборки>.<номер редакции> <значение открытого ключа> Префикс v4 . О свидетельствует о том, что библиотека компилировалась в версии .NET 4.O. После этого префикса идет один символ подчеркивания, а за ним — номер версии изучаемой сборки, каковым в рассматриваемом примере будет 1.0.0.0 для CarLibrary.dll. Далее за парой символов подчеркивания следует значение маркера открытого ключа, основанное на строгом имени. На рис. 14.22 показано, как может выглядеть структура каталогов CarLibrary в среде Windows 7.
Глава 14. Конфигурирование сборок .NET 531 i» | Search у^СЦЛО...* f>\ ss ^ a • Type Applied J| Accessibility ,. AspNetMMCExt J* Carlibrary ii *.0_*Л.0,0_ЗЗа2Ьс294331е8Ь9 £t CppCodeProvtder i FShaip Compiler CarLibrary.dH Date modified: 12/27/2009 745 PM Application extension Size 6Я0 KB 'fc Oate crested; 12/27/2009 7:45 PM Рис. 14.22. Разделяемая сборка CarLibrary со строгим именем (версии 1.0.0.0) Использование разделяемой сборки При создании приложений, использующих разделяемую сборку, единственным отличием от применения приватной сборки является способ, которым должна добавляться ссылка на соответствующую библиотеку в Visual Studio 2010. Фактически для этого используется тот же самый инструмент — диалоговое окно Add Reference. Однако разница в том, что в этом случае упомянутое диалоговое окно не позволит добавить ссылку на сборку за счет нахождения нужного файла в папке С: \Windows\Assembly, которая предназначена только для сборок .NET 3.5 и предшествующих версий. На заметку! Опытные разработчики приложений .NET наверняка помнят, что и раньше при переходе в папку С: \Windows\assembly в диалоговом окне Add Reference внутри Visual Studio не разрешалось добавлять ссылки на разделяемые библиотеки. По это причине приходилось создавать отдельную копию библиотеки просто для того, чтобы иметь возможность добавлять на нее ссылку. В Visual Studio 2010 дела обстоят гораздо лучше. Если необходимо добавить ссылку на сборку, которая была развернута в GAC версии .NET 4.0, на вкладке Browse необходимо перейти в представляющий нужную библиотеку каталог по имени v4 . 0_ma j or. minor . build, revision publicKeyTo ken Value, как показано на рис. 14.23. С учетом этого немного сбивающего с толку факта, давайте создадим новый проект типа Console Application по имени SharedCarLibClient и добавим в него ссылку на сборку CarLibrary только что описанным образом. Как и следовало ожидать, после этого в папке References внутри окна Solution Explorer появится соответствующий значок. Выделив этот значок и выбрав в меню View (Вид) пункт Properties (Свойства), в окне Properties можно увидеть, что свойство Copy Local (Локальная копия) теперь установлено в False. jO Add Reference >™^TBrow"TSiL Loekh: i, v40_1 000_33a2bc294331eab9 ~Ц ■ЕЭ- Recent Hetns 2» Cad ^Network ^lAraries Д, AndrewTroeben •jjHomegroup jl^ Computer £ Mongo Drive (C j |t Windows 1: Microsoft NET File name Rtesoityi at |Гуре (Application extension GAC_MSIL ■ Cartixary i J$ DVD RW Drive (D.) 3 DVD/CD-RW Drive (E) -J&CDObntF) ^ My Passport <H) Ц AndnewTrodsen i iPhone Рис. 14.23. Добавление ссылки на разделяемую сборку CarLibrary (версии 1.0.0.0) в Visual Studio 2010
532 Часть IV. Программирование с использованием сборок .NET Несмотря на это, добавим в новое клиентское приложение следующий тестовый код: using System; using System.Collections.Generic; using System.Linq; using System.Text; using CarLibrary; namespace SharedCarLibClient { class Program { static void Main(string[] args) { Console.WriteLine("***** Shared Assembly Client *****"); SportsCar с = new SportsCarO ; c.TurboBoost (); Console.ReadLine (); } } } Если теперь скомпилировать это клиентское приложение и с помощью проводника Windows перейти в каталог, в котором содержится файл SharedCarLibClient.exe, то можно будет увидеть, что среда Visual Studio 2010 не скопировала CarLibrary.dll в каталог клиентского приложения. Объясняется это тем, что при добавлении ссылки на сборку, в манифесте которой присутствует значение .publickey, Visual Studio 2010 считает, что данная обладающая строгим именем сборка, скорее всего, будет развертываться в GAC, и потому не заботится о копировании ее двоичного файла. Исследование манифеста SharedCarLibClient Вспомните, что при генерировании строгого имени для сборки в ее манифест записывается открытый ключ. В связи с этим, когда в клиентское приложение добавляется ссылка на строго именованную сборку, в ее манифест записывается и компактное хеш- значение всего открытого ключа в маркере .publickeytoken. Поэтому, если открыть манифест SharedCarLibClient.exe в утилите ildasm.exe, можно увидеть там следующий код (разумеется, значение открытого ключа в маркере .publickeytoken будет выглядеть по-другому): .assembly extern CarLibrary { .publickeytoken = C3 A2 ВС 29 43 31 E8 B9 ) .ver 1:0:0:0 } Если теперь сравнить значение открытого ключа, записанного в манифесте клиента, и отображаемого в GAC, то легко обнаружить, что они совпадают. Как упоминалось ранее, открытый ключ является одной из составляющих идентификационных данных строго именованной сборки. Из-за этого CLR-среда будет загружать только версию 1.0.0.0 сборки по имени CarLibrary, из открытого ключа которой может быть получено хеш-значение ЗЗА2ВС294331Е8В9. В случае невозможности обнаружить сборку, соответствующую такому описанию в GAC (и приватную сборку по имени CarLibrary в каталоге клиента), будет сгенерировано исключение FileNotFoundException. Исходный код. Проект SharedCarLibClient доступен в подкаталоге Chapter 14.
Глава 14. Конфигурирование сборок .NET 533 Конфигурирование разделяемых сборок Как и приватные сборки, разделяемые сборки можно конфигурировать за счет добавления в клиентское приложение файла * .config. Разумеется, поскольку разделяемые сборки развертываются в хорошо известном месте (в поддерживаемом в .NET 4.0 кэше GAC), использовать для них элемент <privatePath>, как для приватных сборок, не требуется (хотя, если в клиенте применяются и разделяемые, и приватные сборки, элемент <privatePath> может присутствовать в файле * . conf ig). Конфигурационные файлы приложения вместе с разделяемыми сборками могут использоваться, когда необходимо заставить CLR-среду привязаться к другой версии отдельной сборки, обойдя значение, которое записано в манифесте клиента. Это может быть удобно по нескольким причинам. Например, предположим, что была выпущена версия 1.0.0.0 сборки, в которой через какое-то время обнаружился серьезный дефект. Одна из возможных мер предусматривает переделку клиентского приложения так, чтобы оно ссылалось на правильную, не содержащую дефекта сборку (с версией, например, 1.1.0.0), и передачу приложения и новой библиотеки на все целевые машины. Другой вариант состоит в поставке новой библиотеки кода и файла * . conf ig, который указывает исполняющей среде использовать новую (лишенную дефекта) версию. После установки этой новой версии в GAC не понадобится ни компилировать, ни распространять исходное клиентское приложение заново. Давайте рассмотрим еще один пример. Предположим, что была поставлена первая лишенная дефектов версия сборки A.0.0.0), а через пару месяцев в нее были добавлены новые функциональные возможности, в результате чего получилась новая версия 2.0.0.0. Очевидно, что существующие клиентские приложения, которые компилировались с версией 1.0.0.0, не будут иметь никакого понятия о появлении новых типов, поскольку в их кодовой базе те никак не упоминаются. В новых клиентских приложениях, однако, ссылка на новые функциональные возможности, предлагаемые в версии 2.0.0.0, добавиться должна. В этом случае можно поставить на целевые машины сборку версии 2.0.0.0 и сделать так, чтобы она могла работать вместе со сборкой версии 1.0.0.0. В случае необходимости существующие клиенты могут динамически перенаправляться для загрузки версии 2.0.0.0 (чтобы получить доступ к реализованным в этой версии улучшениям) с использованием конфигурационного файла, при этом компилировать и развертывать клиентское приложение заново не понадобится. Фиксация текущей версии разделяемой сборки Для иллюстрации динамической привязки к конкретной версии разделяемой сборки откроем окно проводника Windows и скопируем текущую версию скомпилированной сборки CarLibrary.dll A.0.0.0) в другой подкаталог (например, CarLibrary Version 1.0.0.0), чтобы сымитировать фиксацию данной версии (рис. 14.24). Создание разделяемой сборки версии 2.0.0.0 Теперь откроем существующий проект CarLibrary и обновим кодовую базу, добавив в нее новый тип enum по имени MusicMedia, определяющий четыре возможных музыкальных проигрывателя: // Тип музыкального проигрывателя, установленный в автомобиле. public enum MusicMedia musicCd, musicTape, musicRadio, musicMp3
534 Часть IV. Программирование с использованием сборок .NET Qf^f' « MyCode ► CarLibraryVersion 1.0.0.0 ■ Open with... Вцгп New folder „. inetpub Щ MyCode s. CarLibrary CarLibrary Version 1.0.0.0 |> CSharpCarClient JB CustomMamespaces ** MurtrfileAssembly |j SharedCarLibClient 1 VisuatBasicCarCtient jo CarLibrary.dll CarLibrary.dH Date modify 12/27/2009 6:43 PM ''•(в} Application extension Srze 6.00 KB Рис. 14.24. Фиксация текущей версии CarLibrary.dll Добавим в тип Саг новый общедоступный метод, позволяющий вызывающему коду включать один из определенных музыкальных проигрывателей (не забыв при необходимости импортировать в Car. cs пространство имен System. Windows . Forms): public abstract class Car { public void TurnOnRadio(bool musicOn, MusicMedia mm) { if(musicOn) MessageBox.Show(string.Format("Jamming {0}", mm)); else MessageBox.Show("Quiet time..."); } } Теперь модифицируем конструкторы класса Car так, чтобы они предусматривали отображение окна MessageBox с сообщением, подтверждающим то, что используется сборка CarLibrary версии 2.0.0.0: public abstract class Car { public Car() { MessageBox.Show("CarLibrary Version 2.0!") } public Car(string name, int maxSp, int currSp) { MessageBox.Show("CarLibrary Version 2.0!"); PetName = name; MaxSpeed = maxSp; CurrentSpeed = currSp; И, наконец, что не менее важно, прежде чем снова компилировать новую библиотеку, обновим номер версии с 1.0.0.0 на 2.0.0.0. Вспомните, что это можно сделать визуально, дважды щелкнув на значке Properties в окне Solution Explorer, а затем щелкнув на кнопке Assembly Information внутри вкладки Application (Приложение). В отрывшемся диалоговом окне соответствующим образом измените значения в полях Assembly Version (Версия сборки), как показано на рис. 14.25.
Глава 14. Конфигурирование сборок .NET 535 Assembly Information I Г МШИ Г*** Prescription: J Company: I Product I Cflpyrigr* 1 Trademark: | Assembly version 1 file version: i euro: Carlibrary Microsoft CarLibrary Copyright С Microsoft 2009 ;2j ;0 0 0 1 0 0~~ <f 892da2c2-c0f7-426e-al98-0b4fl522a223 Neutral language: (None) ▼ : ] Make assembr KOM-Visible [ . QK [_ Cancel ; Рис. 14.25. Изменение номера версии сборки CarLibrary.dll на 2.0.0.0 Если теперь заглянуть в каталог \bin\Debug проекта, можно заметить, что в нем появилась новая версия сборки B.0.0.0), а прежняя версия 1.0.0.0 благополучно сохранена в подкаталоге CarLibrary Version 1.0.0.0. Инсталлируем новую версию сборки в GAC для .NET 4.0 с помощью утилиты gacutil.exe, как было описано ранее в настоящей главе. Обратите внимание, что после этого на машине будут присутствовать две версии одной и той же сборки (рис. 14.26). ©■ v\ « assembly ► GAC MSB. ► CarLibrary ► v4J0 2.0.0Л_ЗЗа2Ьс294331е8Ь9 _.^^ *J_:;.. -J. Mkrosoft.NET л J* assembly > Jb GAC32 .' k GAC.64 л £ GAC.MSIL t> Jj Accessibility > t, AspNetMMCExt <• , CarLibrary M у41).1.0ЛЛ_ЗЗа2Ьс294331е8Ь9 уЦЛ_2ЛЛЛ_ЗЗа2Ьс294331«вЬ9 ■> j. CppCodeProvider M CarLibrary.d!l Date modified: 12/27/2009 8:S4 PM Date created: 12/27/2009 8:54 PM Application extension Sue: 600 KB Рис. 14.26. Несколько версий одной и той же разделяемой сборки После запуска приложения SharedCarLibClient. exe окно с сообщением CarLibrary Version 2.0! не отображается, поскольку в манифесте явным образом запрашивается версия 1.0.0.0. Каким же образом указать CLR-среде, что должна осуществляться привязка к версии 2.0.0.0? Ответ на этот вопрос ищите ниже. На заметку! В Visual Studio 2010 ссылки автоматически переустанавливаются соответствующим образом при компиляции приложений. Поэтому в случае запуска приложения SharedCarLibClient.exe в среде Visual Studio 2010, она автоматически подхватит версию 2.0.0.0. Если вы случайно запустили приложение именно таким образом, просто удалите текущую ссылку на CarLibrary.dll и выберите версию 1.0.0.0 (которая должна была быть размещена в папке CarLibrary Version 1.0.0.0).
536 Часть IV. Программирование с использованием сборок .NET Динамическое перенаправление на конкретную версию разделяемой сборки Чтобы заставить CLR-среду загружать отличную от указанной в манифесте версию разделяемой сборки, можно создать файл *. con fig, содержащий элемент <dependentAssembly>. В этом элементе необходимо создать подэлемент <assemblyldentity> с дружественным именем сборки, которая указана в манифесте клиента сборки (в настоящем примере CarLibrary), и необязательным атрибутом культуры (которому можно присвоить пустую строку или вообще опустить, если должна использоваться культура по умолчанию). Помимо этого, в элементе <dependentAssembly> понадобится создать подэлемент <bindingRedirect> и указать в нем версию, в теку щий момент заданную в манифесте (в атрибуте oldVersion), и версию, которая должна загружаться вместо нее из GAC (в атрибуте newVersion). Давайте создадим в каталоге приложения SharedCarLibClient новый конфигурационный файл SharedCarLibClient. ехе. conf ig и добавим в него приведенные ниже XML-данные. На заметку! Значение открытого ключа на машине читателя, разумеется, будет выглядеть не так, как показано в приведенном ниже коде разметки; для просмотра открытого ключа необходимо заглянуть в манифест клиента с помощью утилиты ildasm.exe или через GAC. <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl"> <dependentAssembly> <assemblyIdentity name="CarLibrary" publicKeyToken= 3A2BC2 94 331E8B9" culture=llneutral"/> <bindingRedirect oldVersion= .0.0.0" newVersion= .0.0.0"/> </dependentAssemblу> </assemblyBinding> </runtime> </configuration> Теперь снова попробуем запустить приложение SharedCarLibClient.exe. На этот раз должно появиться окно с сообщением об использовании версии 2.0.0.0. В конфигурационном файле клиента допускается создавать несколько элементов <dependentAssembly>. Хотя в текущем примере в подобном нет никакой необходимости, давайте предположим, что в манифесте SharedCarLibClient.exe также упоминается и сборка MathLibrary версии 2.5.0.0. Тогда, для указания, что помимо сборки CarLibrary версии 2.0.0.0 должна использоваться и сборка MathLibrary версии 3.0.0.0, можно было бы создать конфигурационный файл SharedCarLibClient. ехе. conf ig со следующим содержимым: <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl"> <!-- Этот раздел отвечает за привязку к CarLibrary --> <dependentAssembly> <assemblyIdentity name="CarLibrary" publicKeyToken= 3A2BC2 94 331E8B9" culture=""/> <bindingRedirect oldVersion= .0.0.0" newVersion= .0.0.0"/> </dependentAssembly>
Глава 14. Конфигурирование сборок .NET 537 <!— Этот раздел отвечает эа привязку к MathLibrary —> <dependentAssembly> <assemblyIdentity name="MathLibrary" publicKeyToken= 3A2BC2 94 331E8B9" culture=""/> <bindingRedirect oldVersion= .5.0.0" newVersion= .0.0.0"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration> На заметку! В атрибуте oldVers ion можно указывать диапазон номеров прежних версий; например, <bindingRedirect oldVersion="l.0.0.0-1.2.0.0" newVersion=.0.0.0"/> заставляет CLR-среду использовать версию 2.0.0.0 вместо любой из прежних версий, относящихся к диапазону от 1.0.0.0 до 1.2.0.0. Сборки политик издателя Следующим моментом, который мы рассмотрим относительно конфигурирования сборок, является роль так называемых сборок политик издателя (publisher policy assemblies). Как было только что показано, за счет создания файлов * . con fig можно заставить исполняющую среду использовать какую-то конкретную версию разделяемой сборки, обходя ту, что упоминается в манифесте клиента. Все это замечательно, но давайте представим, что для выполнения обязанностей администратора требуется переконфигурировать все клиентские приложения на текущей машине так, чтобы в них использовалась сборка CarLibrary.dll именно версии 2.0.0.0. Из-за строгих правил по именованию конфигурационных файлов, придется дублировать одно и то же XML- содержимое во множестве мест (при условии, что места, в которых находятся использующие CarLibrary исполняемые файлы, действительно известны). Очевидно, что это будет настоящим кошмаром! Политики издателя позволяют "издателю" конкретной сборки (в роли которого может выступать отдельный программист, подразделение или целая компания) поставлять двоичную версию файла * . con fig, подлежащую установке в GAC вместе с более новой версией соответствующей сборки. Преимущество такого подхода состоит в том, что при его применении добавлять отдельные файлы *.configB каталоги клиентских приложений не нужно. Вместо этого CLR-среда будет считывать текущий манифест и пытаться найти запрашиваемую версию сборки в GAC. Однако если CLR-среда обнаружит там сборку политик издателя, она прочитает содержащиеся в ней XML-данные и выполнит запрашиваемое перенаправление на уровне GAC. Создавать сборки политик издателя можно в командной строке с помощью такой поставляемой в .NET утилиты, как al. exe (assembly linker — редактор связей сборки). Эта утилита поддерживает множество опций, но для создания сборки политик издателя ей требуется передать только следующие сведения: • путь к файлу * . config или * .xml, в котором содержатся касающиеся перенаправления инструкции; • имя результирующей сборки политик издателя; • путь к файлу * . snk, который должен использоваться для подписения сборки политик издателя; • номера версий, который необходимо назначить создаваемой сборке политик издателя.
538 Часть IV. Программирование с использованием сборок .NET Чтобы создать сборку политик издателя для библиотеки CarLibrary. dll, выполните следующую команду (должна вводиться в одной строке): al /link: CarLibraryPolicy.xml /out:policy.1.0.CarLibrary.dll /keyf:C:\MyKey\ myKey.snk /v:l.0.0.0 В команде указано, что необходимое XML-содержимое находится в файле по имени CarLibraryPolicy .xml. Имя выходного файла (которое должно обязательно соответствовать формату policy. <старший номер>. <младший номер>.<имя конфигурируемой сборки>) задано с помощью флага /out. Кроме того, обратите внимание, что имя файла, в котором содержатся значения открытого и секретного ключей, тоже должны быть указаны с помощью флага /keyf. Помните, что файлы политик издателя являются разделяемыми и потому должны обязательно иметь строгие имена! В результате выполнения данной команды создается новая сборка, которую далее можно помещать в GAC, принуждая всех клиентов использовать библиотеку CarLibrary.dll версии 2.0.0.0, без создания отдельных конфигурационных файлов для каждого клиентского приложения. Благодаря такому подходу, несложно обеспечить перенаправление в масштабах всей машины для всех приложений, использующих конкретную версию (или набор версий) существующей сборки. Отключение политик издателя Теперь предположим, что (исполняя обязанности системного администратора) была развернута сборка политик издателя (и новейшая версия соответствующей сборки) в GAC на клиентской машине. При этом, как обычно бывает, девять из десяти задействованных приложений переключились на использование версии 2.0.0.0 безо всяких проблем, а одно (по ряду причин) при получении доступа к CarLibrary. dll версии 2.0.0.0 постоянно выдает ошибку (как известно, создание программного обеспечения с полной обратной совместимостью практически невозможно). В подобных случаях в .NET можно создавать для проблемного клиента конфигурационный файл, указывающий CLR-среде игнорировать наличие любых установленных в GAC файлов политик издателя. Остальные клиентские приложения, способные работать с новейшей версией сборки .NET продолжают перенаправляться должным образом через установленную сборку политик издателя. Чтобы отключить политики издателя на уровне отдельных клиентов, необходимо создать файл * .config (с соответствующим именем), добавить в него элемент <publisherPolicy> и установить в нем атрибут apply в по. После этого CLR-среда начинает снова загружать ту версию сборки, которая была изначально указана в манифесте клиента. <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl"> <publisherPolicy apply="no11 /> </assemblyBinding> </runtime> </configuration> Элемент <codeBase> В конфигурационных файлах приложений с помощью элемента <codeBase> можно указывать так называемые кодовые базы. Этот элемент заставляет CLR-среду выполнять поиск зависимых сборок в различных местах (например, в сетевых конечных точках или каталогах, находящихся на той же машине, но за пределами каталога клиентского приложения).
Глава 14. Конфигурирование сборок .NET 539 Если в <codeBase> указано место на удаленной машине, загрузка сборки будет производиться только по требованию и только в определенный каталог внутри GAC, называемый кэшем загрузки. На основе того, что уже известно о развертывании сборок в GAC, ясно, что сборки, загружаемые из указанного в <codeBase> места, должны иметь строгие имена (иначе CLR-среда не смогла бы их инсталлировать в GAC). Просмотреть содержимое кэша загрузки на своей машине можно, запустив утилиту gacutil.exe с опцией /ldl: gacutil /ldl На заметку! С технической точки зрения элемент <codeBase> допускается использовать и для поиска сборок, не обладающих строгим именем. В таком случае место размещения сборки должно указываться относительно каталога клиентского приложения (в таком случае этот элемент является не более чем альтернативой <privatePath>). Чтобы посмотреть на элемент <codeBase> в действии, давайте создадим новое консольное приложение по имени CodeBaseClient, добавим в него ссылку на CarLibrary.dll версии 2.0.0.0 и обновим первоначальный файл кода, как показано ниже: using System; using System.Collections.Generic; using System.Linq; using System.Text; using CarLibrary; namespace CodeBaseClient { class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with CodeBases *****"); SportsCar с = new SportsCar(); Console.WriteLine("Sports car has been allocated."); Console.ReadLine() ; } } } Из-за того, что сборка CarLibrary.dll была развернута в GAC, в принципе можно уже запускать приложение в таком, как оно есть виде. Чтобы поработать с элементом <codeBase>, создадим на диске С: новую папку (например, С: \MyAsms) и поместим в нее копию сборки CarLibrary.dll версии 2.0.0.0. Теперь добавим в проект CodeBaseClient файл Арр. conf ig (как объяснялось ранее в этой главе) и вставим в него следующее XML-содержимое (не забывайте о том, что значение .publickeytoken у каждого будет выглядеть по-своему, и посмотреть его можно в GAC): <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl"> <dependentAssembly> <assemblyIdentity name="CarLibrary" publicKeyToken=3A2BC2 94331E8B9" /> <codeBase version=.0.0.0" href="file:///C:\MyAsms\CarLibrary.dll" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
540 Часть IV. Программирование с использованием сборок .NET Как здесь показано, элемент <codeBase> размещен внутри элемента <assemblyldentity>, в котором в атрибутах name и publicKeyToken указано ассоциируемое со сборкой дружественное имя и значение открытого ключа. В самом элементе <codeBase> сообщается версия и (в свойстве href) место размещения сборки, которая должна загружаться. Теперь, благодаря этому, даже в случае удаления сборки CarLibrary. dll версии 2.0.0.0 из GAC, данное клиентское приложение все равно будет успешно выполняться, потому что CLR-среда сможет находить необходимую внешнюю сборку в каталоге С : \MyAsms. На заметку! Размещение сборок в произвольных местах на машине для разработки, по сути, приводит к перестройке системного реестра (и, соответственно, возникновению "ада DLL"), из-за чего при перемещении или переименовании папки, в которой содержатся двоичные файлы, текущие связи будут нарушаться. Поэтому элементом <codeBase> следует пользоваться с осторожностью. Элемент <codeBase> может быть удобен при добавлении ссылки на сборки, находящиеся на удаленной машине в сети. Например, предположим, что есть разрешение на доступ к папке, находящейся по адресу http : //www .MySite . com. В таком случае, чтобы обеспечить загрузку из нее удаленного файла * . dll в кэш загрузки GAC на локальной машине, можно обновить элемент <codeBase> следующим образом: <codeBase version=.О.О.О" href="http://www.MySite.com/Assemblies/CarLibrary.dll" /> Исходный код. Проект CodeBaseClient доступен в подкаталоге Chapter 14. Пространство имен System.Configuration До сих пор во всех показанных файлах * . conf ig применялись лишь хорошо известные XML-элементы для указания CLR-среде, где следует искать внешние сборки. Помимо этих элементов, в конфигурационный файл клиента можно также включать и касающиеся только конкретного приложения данные, не имеющие ничего общего с механизмом связывания. В связи с этим неудивительно, что в составе .NET Framework поставляется пространство имен, которое позволяет программно считывать данные из конфигурационного файла клиента. Это пространство имен называется System.Conf iguration и включает в себя небольшой набор типов, которые могут применяться для считывания специальных настроек из конфигурационного файла клиента. Эти специальные настройки должны содержаться внутри элемента <appSettings>. Элемент <appSettings>, в свою очередь, может содержать любое количество элементов <add> с парами получаемых программно ключей и значений. Для примера предположим, что имеется файл * . conf ig, предназначенный для консольного приложения AppConf igReaderApp, в котором определены два касающихся конкретно этого приложения значения: <configuration> <appSettings> <add key="TextColor" value="Green" /> <add key="RepeatCount" value="8" /> </appSettings> </configuration> Чтобы прочитать эти значения с целью использования в клиентском приложении, можно вызвать на уровне экземпляра метод Get Value (), который является членом
Глава 14. Конфигурирование сборок .NET 541 типа System.Conf iguration . AppSettingsReader. Как показано в следующем коде, в качестве первого параметра метод GetValue () принимает имя ключа, указанного в файле * . conf ig, а во втором — тип, к которому этот ключ относится (получаемый посредством операции type of): using System; using System. Collections .Generic- using System.Ling; using System.Text; using System.Configuration; namespace AppConflgReaderApp { class Program { static void Main(string[] args) { Console.WriteLine("***** Reading <appSettings> Data *****\n"); // Получаем специальные данные из файла *.config. AppSettingsReader ar = new AppSettingsReader(); int numbOfTimes = (int)ar.GetValue("RepeatCount", typeof(int)); string textColor = (string)ar.GetValue("TextColor", typeof(string)); Console.ForegroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), textColor); // Теперь выводим сообщение правильным образом. for(int 1=0; i < numbOfTimes; i++) Console.WriteLine("Howdy!"); Console.ReadLine(); } } } В остальной части книги будут рассматриваться многие другие важные разделы, которые встречаются в конфигурационных файлах клиентских или веб-приложений. Очевидно, что чем глубже погружаться в детали программирования с использованием .NET, тем важнее становится понимание деталей конфигурационных файлов XML. Исходный код. Проект AppConf igReaderApp доступен в подкаталоге Chapter 14. Резюме В этой главе было показано, каким образом CLR-среде определяет месторасположение внешних сборок, на которые производятся ссылки. Сначала рассматривались элементы, которые включаются в состав содержимого сборок: заголовки, метаданные, манифесты и CIL-код. Затем было показано, как создавать однофайловые и многофайловые сборки, а также клиентские приложения (на разных языках). Далее были описаны отличия между приватными и разделяемыми сборками. Приватные сборки копируются в подкаталог клиента, а разделяемые развертываются в кэше глобальных сборок (GAC) при условии назначения им строгих имен. И, наконец, в главе было показано, как конфигурировать приватные и разделяемые сборки с помощью конфигурационного файла XML, действующего на стороне клиента, и с помощью сборки политик издателя, действующей на уровне всей машины.
ГЛАВА 15 Рефлексия типов, позднее связывание и программирование с использованием атрибутов Как рассказывалось в предыдущей главе, базовой единицей разработки в мире .NET являются сборки. С применением встроенных средств для просмотра объектов Visual Studio 2010 (и многих других IDE-сред) легко узнать, какие типы входят в состав упоминаемых в проекте сборок, а с помощью внешних инструментов, таких как утилиты ildasm.exe и reflector.exe — проанализировать CIL-код, метаданные типов и манифест сборки для любого двоичного файла .NET. Помимо такого исследования сборок .NET на этапе проектирования, ту же самую информацию о них можно получить и программно с использованием пространства имен System.Reflection. В главе сначала рассматривается роль рефлексии и необходимость применения метаданных .NET. В остальной части главы затрагивается ряд других тесно связанных с этим тем, так или иначе касающихся служб рефлексии. Например, будет показано, как клиентское приложение .NET посредством динамической загрузки и позднего связывания может активизировать типы, о которых на момент компиляции ничего не было известно. Кроме того, будет описано, как вставлять в сборки .NET специальные метаданные с использованием как системных, так и специальных атрибутов. Для практической демонстрации всех этих аспектов в завершение главы приводится пример построения нескольких так называемых "объектов-оснасток", которые можно подключать к расширяемому приложению Windows Forms. Необходимость в метаданных типов Возможность полностью описывать типы (классы, интерфейсы, структуры, перечисления и делегаты) с помощью метаданных является одной из ключевых в платформе .NET. Во многих технологиях .NET, таких как сериализация объектов, удаленная работа, веб-службы XML и Windows Communication Foundation (WCF), эта возможность нужна
Глава 15. Рефлексия типов, позднеесвязываниеипрограммированиесиспользованиематрибутов 543 для выяснения формата типов во время выполнения. Более того, в средствах обеспечения функциональной совместимости между языками, многочисленных службах компилятора и предлагаемой в IDE-среде функции IntelliSense везде за основу берется конкретное описание пиша. Невзирая на важность (а, возможно, и благодаря ей), метаданные не являются новинкой, поставляемой только в .NET Framework. В Java, CORBA и СОМ тоже применяются похожие концепции. Например, в СОМ для описания типов, содержащихся внутри сервера СОМ, используются специальные библиотеки типов СОМ (которые являются не более чем просто скомпилированным IDL-код). Как и в СОМ, в библиотеках кода в .NET поддерживаются метаданные типов, но эти метаданные, конечно же, синтаксически совершенно не похожи на IDL-метаданные из СОМ. Вспомните, что утилита ildasm.exe позволяет просматривать метаданные всех содержащихся в сборке типов, для чего в ней нужно нажать комбинацию клавиш <Ctrl+M> (см. главу 1). Если открыть в этой утилите любую из сборок * . dll или * . ехе, которые создавались ранее в книге (например, CarLibrary .dll из предыдущей главы) и нажать <Ctrl+M>, можно увидеть метаданные всех содержащихся в ней типов, как показано на рис. 15.1. Global fields Global MemberRefs TypeDef 1И @2000002) TypDefNane: CarLibrary.HusicHedia @2000002) Flags : [Public] [flutoLayout] [Class] [Sealed] [ftnsiClass] @000 Extends : 01000001 [TypeRef] System.Enum Field «1 @4000001) Field Name: ualue_ @4000001) Рис. 15.1. Просмотр метаданных сборки с помощью утилиты ildasm.exe На этом рисунке видно, что метаданные типов .NET отображаются в ildasm.exe очень подробно (в двоичном формате они гораздо компактнее). В действительности описание всех метаданных сборки CarLibrary.dll заняло бы несколько страниц. Однако вполне достаточно будет кратко рассмотреть только некоторые наиболее важные описания метаданных сборки CarLibrary .dll. На заметку! Глубоко вдаваться в то, что обозначает синтаксис в каждом из приводимых в следующих разделах фрагментов метаданных .NET, не стоит. Главное — понять, что метаданные .NET являются очень описательными и предусматривают перечисление каждого определенного внутри (или упоминаемого с помощью внешней ссылки) типа, который встречается в данной кодовой базе. Просмотр (части) метаданных перечисления EngineState Каждый тип, который определен внутри текущей сборки, сопровождается маркером TypeDef #n (TypeDef — сокращение от type definition (определение типа)). Если описываемый тип предусматривает использование еще какого-то типа, определенного в другой
544 Часть IV. Программирование с использованием сборок .NET сборке .NET, этот второй тип сопровождается маркером TypeRef #n (TypeRef — сокращение от type reference (ссылка на тип)). Маркер TypeRef, по сути, является указателем на полное определение метаданных соответствующего типа во внешней библиотеке. Вкратце, метаданные в .NET представляют собой ряд таблиц, в которых явным образом перечислены все определения типов (TypeDef) и типы, на которые они ссылаются (TypeRef), причем те и другие можно просматривать в специальном окне метаданных утилиты ildasm.exe. В случае сборки CarLibrary.dll одним из описаний, встречающихся среди определений типов TypeDef, является метаданные перечисления CarLibrary .EngineState (номер описания может отличаться, потому что нумерация определений типов зависит от порядка, в котором компилятор С# обрабатывает файл): TypeDef #2 @2000003) TypDefName: CarLibrary.EngineState @2000003) Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] @0000101) Extends : 01000001 [TypeRef] System.Enum Field #1 @4000006) Field Name: value @4000006) Flags : [Public] [SpecialName] [RTSpecialName] @0000606) CallCnvntn: [FIELD] Field type: 14 Field #2 @4000007) Field Name: engineAlive @4000007) Flags : [Public] [Static] [Literal] [HasDefault] @0008056) DefltValue: A4) 0 CallCnvntn: [FIELD] Field type: ValueClass CarLibrary.EngineState Маркер TypDefName используется для описания имени данного типа, маркер Extends — для описания базового класса, который лежит в его основе (и каковым в данном случае является тип System.Enum), а маркер Field #n — для описания каждого поля, которое входит в его состав. Ради краткости здесь были перечислены только метаданные поля CarLibrary.EngineState.engineAlive. Просмотр (части) метаданных типа Саг Ниже приведена часть метаданных типа Саг, в которой иллюстрируются следующие аспекты: • как поля представляются в виде метаданных .NET; • как методы представляются в виде метаданных .NET; • как автоматическое свойство представляется в метаданных .NET. TypeDef #3 @2000004) TypDefName: CarLibrary.Car @2000004) Flags : [Public] [AutoLayout] [Class] [Abstract] [AnsiClass] [BeforeFieldlnit] @0100081) Extends : 01000002 [TypeRef] System.Object
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 545 Field #2 @400000а) Field Name: <PetName>k BackingField @400000A) Flags : [Private] @0000001) CallCnvntn: [FIELD] Field type: String Method #1 @6000001) MethodName Flags RVA ImplFlags CallCnvntn hasThis ReturnType : String No arguments. get_PetName @6000001) [Public] [HideBySig] [ReuseSlot] [SpecialName] @0000886) 0x000020d0 [IL] [Managed] @0000000) [DEFAULT] Method #2 @6000002) MethodName Flags RVA ImplFlags CallCnvntn hasThis ReturnType 1 Arguments Argument #1 1 Parameters A) ParamToken : @8000001) Name : value flags set_PetName @6000002) [Public] [HideBySig] [ReuseSlot] [SpecialName] @00008 0x000020e7 [IL] [Managed] @0000000) [DEFAULT] Void 56) String [none] @0000000) Property #1 A7000001) Prop.Name Flags CallCnvntn hasThis ReturnType : No arguments. DefltValue : Setter : Getter : 0 Others PetName A7000001) [none] @0000000) [PROPERTY] String @6000002) set_PetName @6000001) get_PetName Прежде всего, обратите внимание, что в метаданных класса Саг указан базовый класс типа (System.Object) и различные флаги, которые описывают то, каким образом был создан тип ([Public], [Abstract] и т.д.). Для метода (такого как конструктор класса Саг) приводится имя, принимаемые параметры и возвращаемое значение. Далее важно отметить, что автоматическое поле приводит к генерации компилятором приватного вспомогательного поля (по имени <PetName>k BackingField) и двух методов (в случае свойства, доступного для чтения и записи), которыми в рассматриваемом примере являются getPetName () и setPetName (). И, наконец, само свойство отображается в метаданных .NET на внутренние методы get /set с использованием маркеров Setter и Getter.
546 Часть IV. Программирование с использованием сборок .NET Изучение блока TypeRef Вспомните, что в метаданных сборки описаны не только внутренние типы (Саг, EngineStateHT.A.), но и любые внешние типы, на которые эти внутренние типы ссылаются. Например, поскольку в сборке CarLibrary.dll были определены два перечисления, в метаданных типа System.Enum будет присутствовать следующий блок TypeRef: TypeRef #1 @1000001) Token: 0x01000001 ResolutionScope: 0x23000001 TypeRefName: System.Enum Просмотр метаданных самой сборки В окне метаданных утилиты ildasm.exe можно также просматривать метаданные .NET, описывающие саму сборку, для обозначения которых используется маркер Assembly. Как показано в приведенном ниже (неполном) листинге, информация, документируемая внутри таблицы Assembly, совпадает с той, что можно просматривать через значок MANIFEST. Ниже показана часть метаданных манифеста сборки CarLibrary.dll (версии 2.0.0.0): Assembly Token: 0x20000001 Name : CarLibrary Public Key : 00 24 00 00 04 80 00 00 // и т.д. Hash Algorithm : 0x00008004 Major Version: 0x00000002 Minor Version: 0x00000000 Build Number: 0x00000000 Revision Number: 0x00000000 Locale: <null> Flags : [PublicKey] ... ' Просмотр метаданных внешних сборок, на которые имеются ссылки в текущей сборке Помимо маркера Assembly и набора блоков TypeDef и TypeRef, в метаданных .NET могут применяться маркеры AssemblyRef #n для описания каждой из внешних сборок, на которые имеются ссылки в текущей сборе. Например, поскольку в сборке CarLibrary. dll используется класс System. Windows . Forms .MessageBox, в метаданных для сборки System.Windows . Forms будет присутствовать следующий блок AssemblyRef: AssemblyRef #2 B3000002) Token: 0x23000002 Public Key or Token: b7 7a 5c 56 19 34 eO 89 Name: System.Windows.Forms Version: 4.0.0.0 Major Version: 0x00000004 Minor Version: 0x00000000 Build Number: 0x00000000 Revision Number: 0x00000000 Locale: <null> HashValue Blob: Flags: [none] @0000000)
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 547 Просмотр метаданных строковых литералов И, наконец, последним интересным моментом относительно метаданных .NET является то, что внутри маркера User Strings обязательно документируются все присутствующие в кодовой базе строковые литералы: User Strings 70000001 70000019 70000035 70000065 70000083 700000ab 700000cd A1) L'Mamming {0}" A3) L"Quiet time. . . " B3) L"CarLibrary Version 2.01 A4) L"Pamming speed!" A9) L"Faster is better..." A6) L"Time to call AAA" A6) L"Your car is dead" На заметку! Как показано в последнем листинге с метаданными, следует всегда помнить, что все строки четко документируются в метаданных сборки. Это может приводить к серьезным последствиям для безопасности, если строковые литералы используются для представления паролей, номеров кредитных карт и прочей секретной информации. Теперь может возникнуть вопрос (в лучшем случае) о том, как использовать эту информацию в своих приложениях, или (в худшем случае) о том, а зачем вообще нужны эти метаданные. Для получения ответа необходимо ознакомиться с поддерживаемыми в .NET службами рефлексии. Имейте в виду, что польза от рассматриваемых ниже средств может проясниться только к концу главы, поэтому наберитесь терпения. На заметку! В окне Metalnfo утилиты ildasm.exe будет отображаться набор маркеров CustomAttribute, которые служат для описания атрибутов, применяемых внутри кодовой базы. Роль этих атрибутов более подробно рассматривается далее в этой главе. Рефлексия В мире .NET рефлексией (reflection) называется процесс обнаружения типов во время выполнения. С применением служб рефлексии те же самые метаданные, которые отображает утилита ildasm.exe, можно получать программно в виде удобной объектной модели. Например, рефлексия позволяет извлечь список всех типов, которые содержатся внутри определенной сборки * . dll или * . ехе (или даже внутри файла * . netmodule, если речь идет о многофайловой сборке), в том числе методов, полей, свойств и событий, определенных в каждом из них. Можно также динамически обнаруживать набор интерфейсов, которые поддерживаются данным типом, параметров, которые принимает данный метод, и других деталей подобного рода (таких как имена базовых классов, информация о пространствах имен, данные манифеста и т.д.). Как и в любом другом пространстве имен, в System.Reflection (которое поставляется в составе сборки mscorlib.dll) содержится набор взаимосвязанных типов. В табл. 15.1 описаны некоторые наиболее важные из этих типов. Чтобы понять, каким образом использовать пространство имен System. Reflection для программного чтения метаданных .NET, необходимо сначала ознакомиться с классом System.Type.
548 Часть IV. Программирование с использованием сборок .NET Таблица 15.1. Некоторые типы из пространства имен System.Reflection Тип Описание Assembly AssemblyName Eventlnfo Fieldlnfo MemberInfo Methodlnfo Module Parameterlnfo Propertylnfo В этом абстрактном классе содержатся статические методы, которые позволяют загружать сборку, исследовать ее и производить с ней различные манипуляции Этот класс позволяет выяснить различные детали, связанные с идентификацией сборки (номер версии, информация о культуре и т.д.) В этом абстрактном классе хранится информация о заданном событии В этом абстрактном классе хранится информация о заданном поле Этот абстрактный базовый класс определяет общее поведение для типов Eventlnfo, Fieldlnfo, Methodlnfo и Propertylnfo В этом абстрактном классе содержится информация по заданному методу Этот абстрактный класс позволяет получить доступ к определенному модулю внутри многофайловой сборки В этом классе хранится информация по заданному параметру В этом абстрактном классе хранится информация по заданному свойству Класс System.Type Класс System. Type имеет набор членов, которые могут применяться для изучения метаданных типа, и большинство из которых возвращает типы из пространства имен System. Reflection. Например, член Туре. GetMethods () возвращает массив объектов типа Methodlnfo, член Type.GetFields () — массив объектов типа Fieldlnfo, и т.д. В табл. 15.2 приведен частичный список членов, которые поддерживает System.Type (полный перечень можно найти в документации .NET Framework 4.0 SDK). Таблица 15.2. Некоторые члены System.Type Тип Описание IsAbstract IsArray IsClass IsCOMObject IsEnum IsGenericTypeDefinition IsGenericParameter Islnterface IsPrimitive IsNestedPrivate IsNestedPublic IsSealed IsValueType GetConstructors() GetEvents() GetFields() Getlnterfaces() GetMembers() GetMethods() GetNestedTypes() GetProperties() Эти свойства позволяют выяснять ряд основных деталей об интересующем типе (например, является ли он абстрактной сущностью, массивом, вложенным классом и т.д.) Эти методы позволяют получать массив представляющих интерес элементов (интерфейсов, методов, свойств и т.д.). Каждый из этих методов возвращает соответствующий массив (например, GetFields () возвращает массив Fieldlnfo, GetMethods () — массив Methodlnfo и тд.). Следует иметь в виду, что каждый из них имеет также форму единственного числа (например, GetMethod (), GetProperty () и т.п.), которая позволяет извлекать только один элемент по имени, а не целый массив подобных элементов
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 549 Окончание табл. 15.2 Тип Описание FindMembers () Этот метод возвращает массив объектов типа Memberlnf о на основе указанных критериев поиска GetType () Этот статический метод возвращает экземпляр Туре, обладающий указанным строковым именем InvokeMember () Этот метод позволяет выполнять "позднее связывание" для заданного элемента. Понятие позднего связывания рассматривается позже в настоящей главе Получение информации о типе с помощью System. Object. GetType () Экземпляр класса Туре можно получить несколькими способами. Единственное, что нельзя делать — так это напрямую создавать объект Туре с помощью ключевого слова new, потому что класс Туре является абстрактным. Что касается первого способа, то вспомните, что System. Object имеет метод GetType (), который возвращает экземпляр класса Туре, представляющий метаданные текущего объекта: // Получение информации о типе с использованием экземпляра SportsCar. SportsCar sc = new SportsCar(); Type t = sc.GetType () ; Очевидно, что такой подход будет работать только при условии известности типа на этапе компиляции (в данном случае типа SportsCar) и наличии его экземпляра в памяти. Из-за этого ограничения должно быть понятно, почему в утилитах вроде ildasm. ехе информация о типах не получается путем непосредственного вызова для каждого из них метода System. Object .GetType (), ведь ildasm.exe не компилировалась со специальными сборками. Получение информации о типе с помощью typeof () Следующий способ для получения информации о типе заключается в применении операции typeof: // Получение информации о типе с использованием операции typeof. Type t = typeof (SportsCar); В отдичие от метода System.Object .GetType (), операция typeof удобна тем, что она не требует создания экземпляра объекта перед извлечением информации о типе. Однако кодовой базе по-прежнему должно быть известно о представляющем интерес типе на этапе компиляции, поскольку typeof ожидает получения строго типизированного имени типа, а не его текстового представления. Получение информации о типе с помощью System. Туре. GetType () Для получения информации о типе более гибким образом можно вызывать статический метод GetType () класса System. Type и указывать полностью уточненное строковое имя типа, который требуется изучить. При таком подходе знать тип, из которого будут извлекаться метаданные, на этапе компиляции не требуется, поскольку Туре . GetType () принимает в качестве параметра экземпляр System. String, который присутствует везде.
550 Часть IV. Программирование с использованием сборок .NET На заметку! Когда речь идет о том, что при вызове метода Туре. GetType () знать тип на этапе компиляции не нужно, понимается просто то, что этот метод может принимать любое строковое значение (а не строго типизированную переменную). Разумеется, знать, как выглядит имя типа в строковом формате, все равно необходимо! Метод Туре. GetType () имеет перегруженную версию, принимающую дополнительно два булевских параметра. Один из них отвечает за то, должно ли генерироваться исключение, когда обнаружить тип не удается, а второй — за то, должен ли учитываться регистр символов в строке. Для примера рассмотрим следующий код: // Получение информации типа с использованием статического метода // Туре.GetType () (указано не генерировать исключение, если не удается // обнаружить тип SportsCar, и игнорировать регистр символов). Type t = Type .GetType ("CarLibrary.SportsCar11, false, true); Обратите внимание в этом примере на то, что в строке, передаваемой методу GetType (), никак не упоминается сборка, внутри которой содержится интересующий тип. В таком случае подразумевается, что тип содержится внутри сборки, выполняемой в текущий момент Если требуется получить метаданные для типа, находящегося в какой-то внешней приватной сборке, передаваемую строку необходимо сформатировать так, чтобы в ней присутствовало полностью уточненное имя самого типа, запятая и следом за ней дружественное имя сборки, внутри которой он находится: // Получение информации о типе, находящемся внутри внешней сборки. Type t = null; t = Type.GetType("CarLibrary.SportsCar, CarLibrary"); Кроме того, в передаваемой GetType () строке может указываться знак плюс (+) для обозначения вложенного типа. Например, предположим, что требуется получить информацию о типе перечисления (SpyOptions), вложенного внутри класса по имени JamesBondCar. Для этого можно воспользоваться следующим кодом: // Получение информации о типе перечисления, // вложенного внутри текущей сборки. Type t = Type.GetType("CarLibrary.JamesBondCar+SpyOptions"); Создание специальной программы для просмотра метаданных Чтобы проиллюстрировать базовый процесс рефлексии (и оценить пользу от System. Type), создадим консольное приложение по имени MyTypeViewer. Это приложение будет отображать детали методов, свойств, полей и поддерживаемых интерфейсов (а также другие интересные элементы данных) для любого из типов, содержащихся как в самом приложении, так и в сборке mscorlib.dll (доступ к которой все приложения .NET получают автоматически). После создания соответствующего проекта первым делом необходимо импортировать пространство имен System.Reflection: // Для выполнения рефлексии должно импортироваться это пространство имен. using System.Reflection; Рефлексия методов Далее потребуется модифицировать класс Program, определив в нем ряд статических методов так, чтобы каждый из них принимал единственный параметр System.Type и возвращал void. Сначала определим метод ListMethods (), который будет отображать имена методов, определенных в указанном типе. Необходимый код приведен
Глава15.Рефлексиятипов,позднеесвязываниеипрограммированиесиспользованиематрибутов 551 ниже. Обратите внимание, что Type.GetMethods () возвращает массив типов System. Reflection.Methodlnfо, по которому можно осуществлять проход с помощью цикла foreach. // Отображение имен методов типа. static void ListMethods(Type t) { Console.WriteLine ("***** Methods *****••); MethodInfo[] mi = t. Getllethods () ; foreach(Methodlnfо m in mi) Console.WriteLine ("->{0}", m.Name); Console.WriteLine ()/ } В коде с применением свойства Methodlnf о. Name выводятся имена методов. Как не трудно догадаться, класс Methndlnfo имеет дополнительные члены, которые позволяют выяснить, является метод статическим, виртуальным, обобщенным или абстрактным. Кроме того, тип Methodlnf о позволяет получить информацию о том, какое значение возвращает метод, и какие параметры он принимает. Чуть позже реализация ListMethods () будет соответствующим образом улучшена. Для перечисления имен методов при желании можно было бы также создать LINQ- запрос. Вспомните из главы 13. что технология LINQ to Object позволяет создавать строго типизированные запросы и применять их в отношении коллекций объектов, находящихся в памяти. При обнаружении блоков с циклами или программной логикой принятия решения хорошим правилом считается использование соответствующего LINQ-запроса. Например, предыдущий метод можно было бы переписать следующим образом: static void ListMethods(Type t) { Console.WriteLine("***** Methods *****"); var methodNames = from n in t.GetMethods() select n.Name; foreach (var name in methodNcimes) Console.WriteLine("->{0}", name); Console.WriteLine(); } Рефлексия полей и свойств Реализация метода ListFields () выглядит похоже. Единственным заметным отличием в ней является вызова Type.GetFieldsO и возврат в результате массива Fieldlnfo. Для простоты осуществляется отображение лишь имени каждого из полей за счет применения LINQ-запроса: // Отображение имен полей типа. static void ListFields(Type t) { Console.WriteLine ("***** Fields *****"); Fieldlnfo [] fi = t.GetFieldsO; var fieldNames = from f in t.GetFieldsO select f.Name; foreach (var name in fieldNames) Console.WriteLine ("->{0}", name); Console.WriteLine (); } Логика для отображения имен свойств типа выглядит аналогично: // Отображение имен свойств типа. static void ListProps(Type t)
552 Часть IV. Программирование с использованием сборок .NET Console.WriteLine ("***** Properties *****"); Propertylnfo [ ] pi = t.GetProperties (); var propNames = from p in t.GetProperties () select p.Name; foreach (var name in propNames) Console.WriteLine ("->{0}", name); Console.WriteLine (); } Рефлексия реализуемых интерфейсов Давайте создадим метод по имени ListlnterfacesO, способный отображать имена любых из поддерживаемых указываемым типом интерфейсов. Здесь главный интерес представляет вызов Getlnterfaces (), в результате которого возвращается массив System. Types, что вполне логично, поскольку интерфейсы в действительности представляют собой типы. // Отображение имен реализуемых интерфейсов. static void Listlnterfaces(Type t) { Console.WriteLine ("***** Interfaces *****"); var ifaces = from 1 in t .Getlnterfaces () select 1; foreach(Type i in ifaces) Console.WriteLine ("->{0 } ", l.Name); На заметку! Следует иметь в виду, что большинство из "получающих", т.е. get-методов в System. Type (GetMethods (), Getlnterfaces () и т.д.), имеют перегруженные версии, принимающие значения из перечисления BindingFlags. Это позволяет более точно указать, поиск чего должен производиться (например, только статических членов, только общедоступных членов, включая приватные члены, и т.д.). Подробную информацию об этом можно найти в документации .NET Framework 4.0 SDK. Отображение различных дополнительных деталей И, наконец, реализуем еще один вспомогательный метод, способный отображать различные статистические данные о заданном типе (которые показывают, является ли тип обобщенным, как выглядит его базовый класс, не является ли он запечатанным, и т.д.). // Просто для предоставления полной картины. static void ListVariousStats(Type t) { Console.WriteLine ("***** Various Statistics *****"); Console.WriteLine(Base class is: {0}, t.BaseType); // Базовый класс. Console.WriteLine (Is type abstract? {0}, t.IsAbstract); // Абстрактный? Console.WriteLine(Is type sealed? {0}, t.IsSealed); // Запечатанный? Console.WriteLine (Is type generic {0}, t. IsGenericTypeDef mition) ; // Обобщенный? Console.WriteLine (Is type a class type? {0}, t.IsClass); // Класс? Console.WriteLine (); } Реализация метода Main () Модифицируем метод Main () в классе Program так, чтобы он запрашивал у пользователя полностью уточненное имя типа, передавал его методу Туре. Get Type () и затем отправлял извлеченный в результате его выполнения объект System. Type каждому из
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 553 вспомогательных методов. Весь этот процесс должен повторяться до тех пор, пока пользователь не введет Q для завершения работы приложения. static void Main(string[] args) { Console.WriteLine("***** Welcome to MyTypeViewer *****••); string typeName = ""; do { Console.WriteLine ("\nEnter a type name to evaluate); // Запрос имени типа. Console.Write(or enter Q to quit: ); // Для выхода из программы необходимо ввести Q. // Получение имени типа. typeName = Console.ReadLine(); // Пользователь желает выйти из программы? if (typeName.ToUpper() == "Q" ) { break; } // Отобразить информацию о типе. try { Type t = Type.GetType(typeName); Console.WriteLine(""); ListVariousStats(t); ListFields(t); ListProps(t); ListMethods(t); Listlnterfaces(t); } catch { Console .WriteLine ("Sorry, can't find type11); // Указанный тип не найден. } } while (true); } Теперь приложение MyTypeViewer. ехе готово к первому тестовому запуску. Давайте запустим ее и введем следующие полностью уточненные имена (не забывая, что выбранный способ вызова Type .GetType () требует ввода строковых имен с учетом регистра): • System.Int32 • System.Collections.ArrayList • System.Threading.Thread • System.Void • System.10.BinaryWriter • System.Math • System.Console • MyTypeViewer.Program Ниже показан частичный вывод при указании типа System.Math.
554 Часть IV. Программирование с использованием сборок .NET ***** Welcome to MyTypeViewer ***** Enter a type name to evaluate or enter Q to quit: System.Math ***** Various Statistics ***** Base class is: System.Object Is type abstract? True Is type sealed? True Is type generic? False Is type a class type? True ***** Fields ***** ->PI ->E ***** properties ***** ***** Methods ***** ->Acos ->Asin ->Atan ->Atan2 ->Ceiling ->Ceiling ->Cos Рефлексия обобщенных типов При вызове Туре . Get Type () для получения описаний метаданных обобщенных типов должен обязательно применяться специальный синтаксис в виде символа обратной одинарной кавычки (Л) со следующим за ним числовым значением, которое представляет количество параметров, поддерживаемое данным типом. Например, чтобы отобразить описание метаданных обобщенного типа System. Collections . Generic. List<T>, приложению потребуется передать следующую строку: System.Collections.Generic.Listч1 Здесь используется числовое значение 1, поскольку List<T> имеет только один параметр. Для применения рефлексии в отношении типа Dictionary<TKey, TValue>, однако, пришлось бы указать значение 2: System.Collections.Generic.Dictionary'2 Рефлексия параметров и возвращаемых значений методов В текущем состоянии приложение работает вполне нормально. Однако давайте внесем в него одно небольшое улучшение, а именно — обновим вспомогательную функцию ListMethods () так, чтобы она предусматривала вывод не только имени соответствующего метода, но также тип возврата и типы принимаемых параметров. В типе Methodlnfo для этого предусмотрено свойство ReturnType и метод GetParameters (). Ниже приведен измененный соответствующим образом код. Обратите внимание, что строка с информацией о типе и имени каждого параметра создается с использованием вложенного цикла f о reach (а не LINQ-запроса). static void ListMethods(Type t) { Console .WnteLine ("***** Methods *** + *"); Methodlnfo[] mi = t.GetMethods(); foreach (Methodlnfo m in mi) {
Глава 15. Рефлексиятипов.позднеесвязываниеипрограммированиесиспользованием атрибутов 555 // Получение информации о возвращаемом типе. string retVal = m.ReturnType.FullName; string paramlnfo = " ( "; // Получение информации о принимаемых параметрах. foreach (Parameterlnfo pi in m.GetParameters ()) { paramlnfo += string.Format("{0} {1} " , pi.ParameterType, pi.Name); } paramlnfo += " ) "; // Отображение общей сигнатуры метода. Console.WriteLine ("->{0} {1} {2}", retVal, m.Name, paramlnfo); } Console.WriteLine (); } Если теперь запустить обновленное приложение, то обнаружится, что методы указанного типа будут описаны гораздо более подробно. Например, в случае передачи ему в качестве входного параметра имени все того же типа System.Object, можно увидеть следующее описание его методов: ***** Methods ***** ->System.String ToString ( ) ->System.Boolean Equals ( System.Object obj ) ->System.Boolean Equals ( System.Object objA System.Object objB ) ->System.Boolean ReferenceEquals ( System.Object objA System.Object objB ) ->System.Int32 GetHashCode ( ) ->System.Type GetType ( ) Такая реализация ListMethods () удобна, поскольку позволяет исследовать непосредственно каждый параметр и возвращаемый тип методов с помощью объектной модели System.Reflection. Следует иметь в виду, что каждый из типов XXXInfo (Methodlnfo, Propertylnfo, Eventlnfo и т.д.) имеет переопределенную версию ToString (), способную отображать сигнатуру запрашиваемого элемента. Следовательно, ListMethods () можно было бы реализовать и так, как показано ниже (с использованием LINQ-запроса, выбирающего все объекты Methodlnfo, а не только значения Name): public static void ListMethods(Type t) { Console.WriteLine ("***** Methods *****"); Methodlnfo[] mi = t.GetMethods(); var methodNames = from n in t.GetMethods() select n; foreach (var name in methodNames) Console.WriteLine("->{0 } ", name); Console.WriteLine (); } Весьма занимательно, не так ли? Разумеется, пространство имен System. Reflection и класс System. Туре позволяют подвергать рефлексии многие другие аспекты типа помимо тех, которые в настоящей момент охвачены в приложении MyTypeViewer. Они позволяют также получать информацию о событиях, которые поддерживает тип, любых обобщенных параметрах, которые способны принимать его члены, и десятки других деталей. Тем не менее, к настоящему моменту получился своего рода браузер объектов. Его главное ограничение состоит в том, что он позволяет подвергать рефлексии только текущую сборку (MyTypeViewer) или всегда доступную сборку mscorlib.dll. Возникает вопрос: как создать приложение, способное загружать (и подвергать рефлексии) сборки, о которых на момент компиляции ничего не известно? Об этом пойдет речь в следующем разделе.
556 Часть IV. Программирование с использованием сборок .NET Исходный код. Проект MyTypeViewer доступен в подкаталоге Chapter 15. Динамически загружаемые сборки В предыдущей главе рассказывалось о том, что при поиске внешних сборок, на которые ссылается текущая сборка, CLR-среда заглядывает в манифест сборки. Во многих случаях необходимо, чтобы сборки могли загружаться на лету программно, даже если в манифесте о них не упоминается. Формально процесс загрузки внешних сборок по требованию называется динамической загрузкой. В пространстве имен System. Reflection поставляется класс по имени Assembly, с применением которого можно динамически загружать сборку, а также просматривать ее собственные свойства. Этот класс позволяет выполнять динамическую загрузку приватных и разделяемых сборок, причем находящихся в произвольных местах. По сути, класс Assembly предоставляет методы (в частности, Load () и LoadFrom ()), которые позволяют программно поставлять ту же информацию, которая встречается в клиентских файлах * . conf ig. Чтобы посмотреть, как обеспечивать динамическую загрузку на практике, создадим новый проект типа Console Application по имени ExternalAssemblyRef lector. Определим в нем метод Main (), который будет запрашивать у пользователя дружественное имя сборки для динамической загрузки. Кроме того, обеспечим передачу ссылки на эту сборку вспомогательному методу по имени DisplayTypes (), который будет выводить имена всех содержащихся в сборке классов, интерфейсов, структур, перечислений и делегатов. Необходимый код выглядит на удивление просто. using System; using System.Collections .Generic- using System.Linq; using System.Text; using System.Reflections- using System.10; // Для определения FileNotFoundException. namespace ExternalAssemblyReflector { class Program { static void DisplayTypesInAsm(Assembly asm) { Console.WriteLine ("\n***** Types in Assembly *****"); Console.WriteLine("->{0}", asm.FullName); Type [ ] types = asm.GetTypes(); foreach (Type t in types) Console.WriteLine("Type: {0}", t) ; Console.WriteLine ( ""); } static void Main(string [ ] args) { Console.WriteLine (***** External Assembly Viewer *****); string asmName = ; Assembly asm = null; do {
Глава 15. Рефлексиятипов.позднеесвязываниеипрограммированиесиспользованиематрибутов 557 Console.WriteLine("\nEnter an assembly to evaluate"); // Запрос имени интересующей сборки. Console.Write(or enter Q to quit: ); // Для выхода из программы необходимо ввести Q. // Получение имени сборки. asmName = Console.ReadLine(); // Пользователь желает выйти из программы? if (asmName.ToUpper() == "Q") { break; } // Выполнение загрузки сборки. try { asm = Assembly.Load(asmName); DisplayTypesInAsm(asm); } catch { Console.WriteLine("Sorry, can't find assembly."); // Сборка не найдена. } } while (true); } } } Обратите внимание, что статическому методу Assembly. Load () передается только дружественное имя сборки, которую требуется загрузить в память. Следовательно, чтобы подвергнуть рефлексии сборку CarLibrary.dll с помощью этой программы, понадобится сначала скопировать двоичный файл CarLibrary. dll в подкаталог bin\Debug внутри каталога приложения ExternalAssemblyRef lector. После этого можно будет получить примерно такой вывод: ***** External Assembly Viewer ***** Enter an assembly to evaluate or enter Q to quit: CarLibrary \ ***** Types in Assembly ***** ->CarLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9 Type: CarLibrary.MusicMedia Type: CarLibrary.EngineState Type: CarLibrary.Car Type: CarLibrary.SportsCar Type: CarLibrary.MiniVan Чтобы сделать приложение ExternalAssemblyRef lector более гибким, можно модифицировать код так, чтобы загрузка внешней сборки производилась с помощью метода Assembly .LoadFrom (), а не Assembly. Load (): try { asm = Assembly.LoadFrom(asmName); DisplayTypesInAsm(asm); } После этого пользователь сможет вводить абсолютный путь к интересующей сборке (например, C:\MyApp\MyAsm.dll). По сути, метод Assembly. LoadFrom () позволяет про-
558 Часть IV. Программирование с использованием сборок .NET граммно предоставлять значение <codeBase>. Теперь консольному приложению можно передавать полный путь. Таким образом, если сборка Car Library. dll находится в папке С : \МуСode, будет получен следующий вывод: ***** External Assembly Viewer ***** Enter an assembly to evaluate or enter Q to quit: C:\MyCode\CarLibrary.dll ***** Types in Assembly ***** ->CarLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9 Type: CarLibrary.EngineState Type: CarLibrary.Car Type: CarLibrary.SportsCar Type: CarLibrary.MiniVan Исходный код. Проект ExternalAssemblyRef lector доступен в подкаталоге Chapter 15. Рефлексия разделяемых сборок Метод Assembly. Load () имеет несколько перегруженных версий. Одна из них позволяет указать значение культуры (для локализуемых сборок), а также номер версии и значение маркера открытого ключа (для разделяемых сборок). Все вместе эти элементы, которые идентифицируют сборку, называются отображаемым именем (display name), формат которого выглядит так: сначала идет дружественное имя сборки, за ней строка разделенных запятыми пар "имя/значение", а потом необязательные спецификаторы (в любом порядке). Ниже приведен образец, которым следует пользоваться (необязательные элементы указаны в круглых скобках): Имя (, Vers ion = <старший номера .<мл а дший номер* .'помер сборки* .<номер редакции>) (,Culture = <маркер культуры>) (,PublicKeyToken = <маркер открытого ключа>) При создании отображаемого имени использование PubliuKeyToken=null означает, что связывание и сопоставление должно выполняться со сборкой, не имеющей строгого имени, a Culture=" " — что сопоставление должно выполняться с использованием культуры, которая принята на целевой машине по умолчанию: // Выполнение загрузки CarLibrary версии 1.0.0.0 //с использованием принятой по умолчанию культуры. Assembly a = Assembly. Load (@"CarLibrary, Version=l. 0 . 0 . 0, PublicKeyToken=null, Culture=""" ) ; Следует также иметь в виду, что в пространстве имен System. Reflection поставляется тип AssemblyName, который позволяет представлять строковую информацию наподобие той, что была показана выше, в виде удобной объектной переменной. Обычно этот класс применяется вместе с классом System. Version, который позволяет упаковывать в объектно-ориентированную оболочку номер версии сборки. После создания отображаемое имя может передаваться перегруженной версии метода Assembly. Load (): // Использование AssemblyName для определения отображаемого имени. AssemblyName asmName; asmName = new AssemblyName(); asmlJame .Name = "CarLibrary"; Version v = new Version (.0.0.0"); asmName.Version = v; Assembly a = Assembly.Load(asmName);
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 559 Для загрузки разделяемой сборки из GAC в параметре Assembly. Load () должно обязательно указываться значение PublicKeyToken. Например, предположим, что создано новое консольное приложение по имени SharedAsmRef lector, с помощью которого нужно загрузить сборку System.Windows.Forms.dll версии 4.0.0.0, поставляемую в составе библиотек базовых классов .NET. Поскольку количество типов в данной сборке довольно велико, давайте сделаем так, чтобы это приложение предусматривало вывод только имен всех общедоступных перечислений за счет применения просто соответствующего LINQ-запроса: using System; using System.Collections .Generic- using System.Linq; using System.Text; using System.Reflection; using System.10; using System.Linq; namespace SharedAsmReflector { public class SharedAsmReflector { private static void Displaylnfo(Assembly a) { Console.WriteLine (***** info about Assembly *****); Console.WriteLine(Loaded from GAC? {0}, a.GlobalAssemblyCache); Console.WriteLine(Asm Name: {0}, a.GetName().Name); Console.WriteLine(Asm Version: {0}, a.GetName().Version); Console.WriteLine(Asm Culture: {0}, a.GetName().Culturelnfo.DisplayName); Console.WriteLine(\nHere are the public enums:); // Использование LINQ-запроса для нахождения // общедоступных перечислений. Туре[] types = a.GetTypes(); var publicEnums = from ре in types where pe.IsEnum && pe.IsPublic select pe; foreach (var pe in publicEnums) { Console.WriteLine(pe); } } static void Main(string[] args) { Console.WriteLine("***** The Shared Asm Reflector App *****\n"); // Выполнение загрузки System.Windows.Forms.dll из GAC. string displayName = null; displayName = "System.Windows.Forms," + "Version=4.0.0.0, " + "PublicKeyToken=b77a5c561934e089," + @"Culture= ; Assembly asm = Assembly.Load(displayName); Displaylnfo(asm); Console.WriteLine("Done!"); Console.ReadLine();
560 Часть IV. Программирование с использованием сборок .NET Исходный код. Проект SharedAsmRef lector доступен в подкаталоге Chapter 15. К этому моменту должно стать более-менее понятно, как использовать некоторые ключевые члены пространства имен System.Reflection для получения метаданных во время выполнения. Разумеется, необходимость в самостоятельном создании специальных браузеров объектов в повседневной работе, скорее всего, будет возникать не слишком часто. Тем не менее, ни в коем случае не следует забывать о том, что службы рефлексии обеспечивают основу для применения ряда других полезных методик программирования, в том числе и позднего связывания. Позднее связывание Поздним связыванием (late binding) называется технология, которая позволяет создавать экземпляр определенного типа и вызывать его члены во время выполнения без кодирования факта его существования жестким образом на этапе компиляции. При создании приложения, в котором предусмотрено позднее связывание с типом из какой- то внешней сборки, добавлять ссылку на эту сборку нет никакой причины, и потому в манифесте вызывающего кода она непосредственно не указывается. На первый взгляд увидеть выгоду от позднего связывания не просто. Действительно, если есть возможность выполнить раннее связывание с объектом (например, добавить ссылку на сборку и разместить тип с помощью ключевого слова new), следует обязательно так и поступать. Одна из наиболее веских причин состоит в том, что ранее связывание позволяет выявлять ошибки во время компиляции, а не во время выполнения. Тем не менее, позднее связывание тоже играет важную роль в любом создаваемом расширяемом приложении. Пример создания такого "расширяемого" приложения будет приведен в конце настоящей главы, в разделе "Создание расширяемого приложения", а пока рассмотрим роль класса Activator. Класс System. Activator Класс System.Activator (определенный в сборке mscorlib.dll) играет ключевую роль в процессе позднего связывания в .NET. В текущем примере интересует пока что только его метод Activator . Createlnstance (), который позволят создавать экземпляр подлежащего позднему связыванию типа. Этот метод имеет несколько перегруженных версий и потому обеспечивает довольно высокую гибкость. В самой простой версии Createlnstance () принимает действительный объект Туре, описывающий сущность, которая должна размещаться в памяти на лету. Чтобы увидеть, что имеется в виду, давайте создадим новый проект типа Console Application по имени LateBindingApp, импортируем в него пространства имен System. 10 и System.Reflection с помощью ключевого слова using и затем изменим класс Program, как показано ниже. // Это приложение будет загружать внешнюю сборку и создавать // объект за счет применения позднего связывания. public class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Late Binding *****"); // Попытка загрузки локальной копии CarLibrary. Assembly a = null;
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 561 try { а = Assembly.Load("CarLibrary"); } catch(flleNotFoundException ex) { Console.WriteLine(ex.Message); return; } if (a != null) CreateUsingLateBinding(a); Console.ReadLine(); } static void CreateUsingLateBinding(Assembly asm) { try { // Получение метаданных типа Mini van. Type miniVan = a.GetType("CarLibrary.MiniVan"); // Создание экземпляра Minivan на лету. object obj = Activator.Createlnstance(miniVan); Console.WriteLine("Created a {0} using late binding!", obj); } catch(Exception ex) { Console.WriteLine(ex.Message); } } } Прежде чем запускать данное приложение, необходимо вручную скопировать сборку CarLibrary. dll в подкаталог bin\Debug внутри каталога этого нового приложения с помощью проводника Windows. Дело в том, что здесь вызывается метод Assembly. Load (), а это значит, что CLR-среда будет зондировать только папку клиента (при желании можно было бы воспользоваться методом Assembly. LoadFrom () и указывать полный путь к сборке, но в данном случае в этом нет никакой необходимости). На заметку! Добавлять ссылку на CarLibrary. dll с помощью Visual Studio в данном примере тоже не следует! Это приведет к добавлению записи о данной библиотеке в манифест клиента. Вся суть применения позднего связывания состоит в том, чтобы попробовать создать объект, о котором на момент компиляции ничего не известно. Обратите внимание, что метод Activator. Createlnstance () возвращает экземпляр объекта System.Object, а не строго типизированный объект MiniVan. Следовательно, в случае применения к переменной obj операции точки никаких членов класса MiniVan увидеть не получится. На первый взгляд может показаться, что эту проблему удастся решить за счет применения операции явного приведения: // Приведение для получения доступа к членам MiniVan? // Нет! Компилятор выдаст ошибку! object obj = (MiniVan)Activator.Createlnstance(minivan); Из-за того, что в приложение не была добавлена ссылка на CarLibrary .dll, применять ключевое слово using для импортирования пространства имен CarLibrary нельзя, значит и нельзя использовать экземпляр MiniVan в операции приведения.
562 Часть IV. Программирование с использованием сборок .NET Следует уяснить, что весь смысл применения позднего связывания состоит в том, чтобы создавать экземпляры объектов, о которых на этапе компиляции ничего не известно. Из-за этого возникает вопрос: как обеспечить вызов методов MiniVan на ссылке System.Object? Ответ: конечно же с помощью рефлексии. Вызов методов без параметров Предположим, что требуется обеспечить вызов в отношении объекта MiniVan метода TurboBoost () . Вспомните, что этот метод приводит двигатель в нерабочее состояние и затем отображает окно с соответствующим сообщением. Первым шагом является получение для метода TurboBoost () объекта Methodlnfo с применением Type-. GetMethod () . После получения объекта Methodlnfo можно вызвать и сам MiniVan . TurboBoost () , воспользовавшись Invoke () . Метод Methodlnfo . Invoke () ожидает всех параметров, которые подлежат передаче представляемому Methodlnfo методу, в виде массива System.Object (поскольку эти параметры могут быть самыми разнообразными сущностями). Из-за того, что метод TurboBoost () не требует никаких параметров, методу Methodlnfo. Invoke () может быть передано просто значение null (свидетельствующее об отсутствии параметров у данного метода). Исходя из всего этого, модифицируем метод CreateUsingLateBinding () следующим образом: static void CreateUsingLateBinding(Assembly asm) { try { // Получение метаданных типа Mini van. Type miniVan = asm.GetType ("CarLibrary.MiniVan"); // Создание экземпляра MiniVan на лету. object ob] = Activator.Createlnstance(miniVan); Console.WriteLine("Created a {0} using late binding!", obj); // Получение информации для TurboBoost. Methodlnfo mi = miniVan.GetMethod("TurboBoost"); // Вызов метода (null означает отсутствие параметров). mi.Invoke (obj, null); } catch(Exception ex) { Console.WriteLine(ex.Message); } } Теперь после запуска приложения при вызове метода TurboBoost () появится окно с сообщением, показанное на рис. 15.2. our engine block exploded! \e£3td Eek! OK | Рис. 15.2. Вызов метода с применением позднего связывания
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 563 Вызов методов с параметрами Когда позднее связывание используется для вызова метода, требующего каких-то параметров, необходимо предоставить аргументы в виде слабо типизированного массива object. Вспомните, что в библиотеке CarLibrary.dll версии 2.0.0.0 был определен следующий метод в классе Саг: public void TurnOnRadio(bool musicOn, MusicMedia mm) { if (musicOn) MessageBox.Show(string.Format("Jamming {0}", mm)); else MessageBox.Show("Quiet time..."); } Этот метод принимает два параметра: булевское значение, которое указывает, включена ли музыкальная система в автомобиле, и перечисление, отражающее тип музыкального проигрывателя. Как было показано, это перечисление структурировано следующим образом: public enum MusicMedia { musicCd, // О musicTape, // 1 musicRadio, // 2 musicMp3 // 3 } Ниже приведен код нового метода в классе Program, в котором вызывается метод TurnOnRadio (). Обратите внимание, что для перечисления MusicMedia используются лежащие в основе числовые значения. static void InvokeMethodWithArgsUsingLateBinding(Assembly asm) { try { // Получение описания метаданных типа SportsCar. Type sport = asm.GetType("CarLibrary.SportsCar"); // Создание объекта типа SportsCar. object obj = Activator.Createlnstance(sport); // Вызов метода TurnOnRadio() с аргументами. Methodlnfo mi = sport.GetMethod("TurnOnRadio"); mi.Invoke(obj, new object [] { true, 2 }); } catch (Exception ex) { Console.WriteLine(ex.Message); } } К этому моменту уже должно быть понятно, как в целом выглядят отношения между рефлексией, динамической загрузкой и поздним связыванием. Разумеется, что помимо описанной здесь в API-интерфейсе рефлексии предлагается множество другой функциональности, однако основные сведения уже были предоставлены выше в этой главе. Может по-прежнему интересовать вопрос: когда выгодно применять все эти приемы в своих приложениях? Это должно проясниться в конце главы, а в следующем разделе пока что пойдет речь о роли атрибутов в .NET. Исходный код. Проект LateBindingApp доступен в подкаталоге Chapter 15.
564 Часть IV. Программирование с использованием сборок .NET Роль атрибутов .NET Как показывалось в начале настоящей главы, одной из задач компилятора .NET является генерирование описаний метаданных для всех типов, которые были определены, и на которые имеется ссылка в текущем проекте. Помимо стандартных метаданных, которые помещаются в любую сборку, программисты в .NET могут включать в состав сборки дополнительные метаданные с использованием атрибутов. В сущности, атрибуты представляют собой не более чем просто аннотации, которые могут добавляться в код и применяться к какому-то конкретному типу (классу, интерфейсу, структуре и т.д.), члену (свойству, методу и т.д.), сборке или модулю. Концепция аннотирования кода с применением атрибутов является далеко не новой. Еще в COM IDL (Interface Definition Language — язык описания интерфейсов) поставлялось множество предопределенных атрибутов, которые позволяли разработчикам описывать типы, содержащиеся внутри того или иного СОМ-сервера. Однако в СОМ атрибуты представляли собой немногим более чем просто набор ключевых слов. Когда требовалось создать специальный атрибут, разработчик в СОМ мог делать это, но затем он должен был ссылаться на этот атрибут в коде с помощью 128-битного числа (GUID- идентификатора), что, как минимум, довольно затрудняло дело. В .NET атрибуты представляют собой типы классов, которые расширяют абстрактный базовый класс System. Attribute. В поставляемых в .NET пространствах имен доступно множество предопределенных атрибутов, которые полезно применять в своих приложениях. Более того, можно также создавать собственные атрибуты и тем самым дополнительно уточнять поведение своих типов, создавая для атрибута новый тип, унаследованный от Attribute. В табл. 15.3 перечислены некоторые из предопределенных атрибутов, предлагаемые в различных пространствах имен .NET. Таблица 15.3. Некоторые из предопределенных атрибутов в .NET Атрибут Описание [CLSCompliant ] Заставляет элемент, к которому применяется, отвечать требованиям CLS (Common Language Specification — общеязыковая спецификация). Вспомните, что типы, которые отвечают требованиям CLS, могут без проблем использоваться во всех языках программирования .NET [Dlllmport] Позволяет коду .NET отправлять вызовы в любую неуправляемую библиотеку кода на С или C++, в том числе и API-интерфейс лежащей в основе операционной системы. Обратите внимание, что при взаимодействии с программным обеспечением, работающим на базе СОМ, этот атрибут не применяется [Obsolete ] Позволяет указать, что данный тип или член является устаревшим. Когда другие программисты попытаются использовать элемент с таким атрибутом, они получат соответствующее предупреждение от компилятора [Serializable] Позволяет указать, что класс или структура является "сериализируемой", т.е. способна сохранять свое текущее состояние в потоке [NonSerialized] Позволяет указать, что данное поле в классе или структуре не должно сохраняться в процессе сериализации [WebMethod] Позволяет указать, что метод может вызываться посредством HTTP- запросов, и CLR-среда должна преобразовывать его возвращаемое значение в формат XML
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 565 Важно уяснить, что при применении атрибутов в коде размещающиеся внутри них метаданные, по сути, остаются бесполезными до тех пор, пока не запрашиваются явным образом в каком-то другом компоненте программного обеспечения посредством рефлексии. Если этого не происходит, они спокойно игнорируются и не причиняют никакого вреда. Потребители атрибутов Как нетрудно догадаться, в составе NET Framework 4.0 SDK поставляется множество утилит, которые позволяют производить поиск разнообразных атрибутов. Даже сам компилятор С# (esc . exe) запрограммирован так, что он проверяет наличие разных атрибутов во время компиляции. Например, сталкиваясь с атрибутом [CLSCompliant], он автоматически проверяет соответствующий элемент и удостоверяется в том, что в нем содержатся только отвечающие требованиям CLS инструкции, а при обнаружении элемента с атрибутом [Obsolete] отображает внутри окна Error List (Список ошибок) в Visual Studio 2010 соответствующее предупреждение. Помимо утилит, предназначенных для использования во время разработки, многие методы в библиотеках базовых классов .NET тоже изначально запрограммированы так, чтобы распознавать определенные атрибуты посредством рефлексии. Например, чтобы информация о состоянии объекта сохранялась в файле, все, что потребуется делать — это просто добавить в класс или структуру в виде аннотации атрибут [Serializable]. Встретив этот атрибут, метод Serialize () из класса BinaryFormatter автоматически сохраняет соответствующий объект в файле в компактном двоичном формате. CLR-среда в .NET тоже выполняет проверки на предмет наличия определенных атрибутов. Самым известным из них, пожалуй, является атрибут [WebMethod], который применяется для создания веб-служб XML с помощью ASP.NET. Чтобы можно было получать доступ к методу посредством HTTP-запросов, а его возвращаемое значение автоматически преобразовывалось в формат XML, понадобится применить к этому методу атрибут [WebMethod], а обо всех остальных деталях будет заботиться CLR-среда. Помимо разработки веб-служб, атрибуты также играют важную роль в функционировании системы безопасности NET, в Windows Communication Foundation, в обеспечении функциональной совместимости между СОМ и .NET, и во многом другом. И, наконец, допускается разрабатывать приложения, способные распознавать специальные атрибуты, а также любые из тех, что поставляются в составе библиотек базовых классов .NET. Это позволяет, по сути, создавать набор "ключевых слов", понятных только определенному множеству сборок. Применение предопределенных атрибутов в С# Чтобы посмотреть, как применение атрибутов в С# выглядит на практике, создадим новое консольное приложение по имени ApplyingAttributes. Предположим, что требуется создать класс по имени Motorcycle (мотоцикл), который бы мог сохраняться в двоичном формате. Для этого достаточно применить к определению этого класса атрибут [Serializable]. Если в классе есть какое-то поле, которое не должно сохраняться, к нему должен быть применен атрибут [NonSerialized]. // Этот класс может сохраняться на диске. [Serializable] public class Motorcycle { // Это поле сохраняться не будет. [NonSerialized] float weightOfCurrentPassengers;
566 Часть IV. Программирование с использованием сборок .NET // Эти поля будут сериалиэованы. bool hasRadioSystem; bool hasHeadSet; bool hasSissyBar; } На заметку! Действие любого атрибута распространяется только на элемент, который следует сразу же после него. Например, в классе Motorcycle сериализации не будет подвергнуто только поле weightOfCurrentPassengers. Все остальные поля будут обязательно сериа- лизоваться, потому что класс снабжен атрибутом [Serializable]. На данный момент вдаваться в сам процесс сериализации объектов не стоит (соответствующие детали подробно рассматриваются в главе 20). Главное обратить внимание на то, что для применения атрибута его имя должно быть заключено в квадратные скобки. После компиляции этого класса можно просмотреть дополнительные метаданные с помощью утилиты ildasm. exe. При этом следует обратить внимание на то, что эти атрибуты будут сопровождаться маркером serializable (в строке с треугольником красного цвета внутри класса Motorcycle) и маркером notserialized(B строке, представляющей поле weightOfCurrentPassengers), как показано на рис. 15.3. orm 5th ed",First Draft'. File View Help v E:\My Books\C# Book\C# and the .NET Platform 5th ed\First Draft\Chapter_15\Code\Applyi !•••••► MANIFEST В Щ ApplyingAttributes й WE ApplyingAttributes. Motorcycle ► .class public auto ansi serializable beforefieldinit v HasHeadSet: private bool v hasRadioSystem : private bool v hasSissyBar: private bool .ctor: void() ■ ApplyingAttributes. Program assembly ApplyingAttributes Рис. 15.3. Просмотр атрибутов в ildasm.exe Как не трудно догадаться, к каждому элементу может применяться сразу несколько атрибутов. Например, предположим, что имеется некий унаследованный тип класса на С# (по имени HorseAndBuggy), который был помечен как сериализируемый, но теперь считается устаревшим для текущей разработки. В этом случае, вместо того чтобы удалять определение этого класса из кодовой базы (и подвергаться риску нарушения работы существующего программного обеспечения), можно просто дополнительно пометить этот класс атрибутом [Obsolete ]. Для применения к одному элементу нескольких атрибутов они должны быть представлены в виде разделенного запятыми списка: [Serializable, Obsolete("Use another vehicle!")] public class HorseAndBuggy // } В качестве альтернативны применять несколько атрибутов к одному элементу можно и так, как показано ниже (конечный результат будет тот же):
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 567 [Serializable] [Obsolete("Use another vehicle!")] public class HorseAndBuggy { // ... } Сокращенное обозначение атрибутов в С# Если вы заглядывали в документацию .NET Framework 4.0 SDK, то наверняка заметили, что на самом деле имя класса, представляющего атрибут [Obsolete], выглядит не как Obsolete, а как ObsoleteAttribute. Дело в том, что по соглашению имена всех атрибутов в .NET (в том числе и специальных, создаваемых разработчиком) сопровождаются суффиксом Attribute. Для упрощения процесса применения атрибутов в языке С# вводить этот суффикс не обязательно. То есть приведенная ниже версия HorseAndBuggy будет работать точно так же, как предыдущая (но требовать немного больше клавиатурного ввода): [SerializableAttribute] [ObsoleteAttribute ("Use another vehicle!")] public class HorseAndBuggy { // ... } Имейте в виду, что подобный сокращенный вариант написания атрибутов допускается только в языке С#, а во всех остальных совместимых с .NET языках он может не поддерживаться. Указание параметров конструктора для атрибутов Обратите внимание, что атрибут [Obsolete] может принимать нечто похожее на параметр конструктора. Если взглянуть на формальное определение этого атрибута в окне кода, обнаружится, что этот класс действительно предоставляет конструктор, получающий System.String: public sealed class ObsoleteAttribute : Attribute { public ObsoleteAttribute(string message, bool error); public ObsoleteAttribute(string message); public ObsoleteAttribute (); public bool IsError { get; ) public string Message { get; } } Важно понять, что при предоставлении атрибуту параметров конструктора, этот атрибут не размещается в памяти до тех пор, пока в отношении данных параметров не будет произведена рефлексия каким-то другим типом или внешней утилитой. Вместо этого строковые данные, определяемые на уровне атрибутов, сохраняются внутри сборки в виде блока метаданных. Атрибут [Obsolete] в действии Теперь, когда класс HorseAndBuggy помечен как устаревший, при попытке разместить его экземпляр: static void Main(string[] args) { HorseAndBuggy mule = new HorseAndBuggy(); }
568 Часть IV. Программирование с использованием сборок .NET можно будет увидеть, что поставляемые внутри него строковые данные извлекаются и отображаются в Visual Studio 2010 внутри окна Error List, а при наведении курсора мыши на этот устаревший тип появляется причиняющая проблему строка кода (рис. 15.4). Program.cs X JtAppryingAttributes-Program " I -УМл'гЧ^ппдИ args) class Program { static void Main(string[] args) { } ■ApplyineAttributes.HorseAndBuggy' is obsolete: 'Use another vehicle J' } 100 %~^"| •> Рис. 15.4. Атрибуты в действии В данном случае в роли "другого компонента программного обеспечения кода", воспроизводящего атрибут [Obsolete] посредством рефлексии, выступает компилятор С#. К настоящему моменту должны быть ясно следующие важные моменты, касающиеся атрибутов .NET. • Атрибуты в .NET представляют собой классы, унаследованные от класса System. Attribute. • Применение атрибутов приводит к включению в сборку дополнительных метаданных. • Атрибуты, по сути, остаются бесполезными до тех пор, пока какой-то другой агент не воспроизведет их через рефлексию. • В С# атрибуты указываются в квадратных скобках. Теперь посмотрим, каким образом создавать собственные атрибуты и специальный код, воспроизводящий добавляемые ими метаданные посредством рефлексии. Исходный код. Проект ApplyingAttributes доступен в подкаталоге Chapter 15. Создание специальных атрибутов Первым шагом в процессе создания специального атрибута является создание нового класса, унаследованного от System.Attribute. Чтобы не отклоняться от автомобильной темы, используемой повсюду в этой книге, давайте создадим новый проект типа Class Library (Библиотека классов) по имени AttnbutedCarLibrary, в котором определим несколько транспортных средств с помощью специального атрибута по имени VehicleDescriptionAttribute: // Специальный атрибут. public sealed class VehicleDescriptionAttribute : System.Attribute { public string Description { get; set; } public VehicleDescriptionAttribute (string vehicalDescnption.) { Description = vehicalDescnption; public VehicleDescriptionAttribute()
Глава15.Рефлексиятипов,позднеесвязываниеипрограммированиесиспользованиематрибутов 569 Как здесь видно, в VehicleDescriptionAttribute поддерживается фрагмент строковых данных, предусматривающий возможность манипуляций над ним с помощью автоматического свойства (Description). Помимо того факта, что данный класс наследуется от System.Attribute, ничего особенного в нем больше нет. На заметку! Ради безопасности в .NET наилучшим приемом считается запечатывание всех специальных атрибутов. На самом деле в Visual Studio 2010 даже поставляется специальный фрагмент кода под названием Attribute, который позволяет быстро генерировать в окне кода новый производный от Attribute класс. Об использовании фрагментов кода подробно рассказывалось в главе 2. Применение специальных атрибутов Из-за того, что класс VehicleDescriptionAttribute унаследован от System. Attribute, теперь можно снабжать транспортные средства любыми подходящими аннотациями. Для целей тестирования добавим в новую библиотеку классов следующие определения классов: // Присваивание описания с помощью "именованного свойства". [Serializable] [VehicleDescription(Description = "My rocking Harley")] // Мой классный Харли public class Motorcycle { } [SerializableAttribute] [ObsoleteAttnbute ("Use another vehicle!")] // Используйте другое транспортное средство1 [VehicleDescription("The old gray mare, she ain't what she used to be...")] // Старая серая кобыла, она уже не та, что была раньше. . . public class HorseAndBuggy { } [VehicleDescription (A very long, slow, but feature-rich auto")] // Очень длинное, медленное, но богатое возможностями авто public class Winnebago { } Синтаксис именованных свойств Обратите внимание, что для присваивания описания классу Motorcycle применяется новый фрагмент связанного с атрибутами синтаксиса, называемый иженован- ным свойством. В конструкторе первого атрибута [VehicleDescription] установка лежащих в основе строковых данных производится за счет применения свойства Description. В случае применения рефлексии к данному атрибуту каким-то внешним агентом, свойству Description будет передаваться соответствующее значение (синтаксис именованного свойства разрешено использовать только в случае предоставления в атрибуте доступного для записи свойства .NET). В отличие от Motorcycle, в типах HorseAndBuggy и Winnebago синтаксис именованного свойства не применяется, и вместо этого передача строковых данных осуществляется через специальный конструктор. В любом случае после компиляции сборки AttnbutedCarLibrary можно воспользоваться утилитой ildasm.exe и посмотреть, какие описания метаданных будут вставлены для каждого из этих типов. Например, на рис. 15.5 показано, как в ildasm.exe будет выглядеть описание класса Winnebago, в частности — данные внутри элемента beforefieldinit.
570 Часть IV. Программирование с использованием сборок .NET // Attribute -'Car! 'Orsry, Ainnebago;..class pubfic auto ansi beforefieidinit Find ng) - L ( 01 00 28 41 20 76 65 72 79 20 6C 6F 6E 67 2C 20 // ..(ft uery long, 73 6C 6F 77 2C 20 62 75 74 20 66 65 61 74 75 72 // slow, but featur 65 2D 72 69 63 60 20 61 75 74 6F 00 00 ) // e rich auto.. ЫшшшьшшшяфтШттшт*шшшшшй > _l Рис. 15.5. Вставленные данные описания транспортного средства Ограничение использования атрибутов По умолчанию действие специальных атрибутов может распространяться на практически любой аспект кода (методы, классы, свойства и т.д.). Следовательно, если бы потребовалось, можно было бы использовать VehicleDescription (помимо прочего) и для уточнения описания методов, свойств или полей: [VehicleDescription("A very long, slow, but feature-rich auto")] // Очень длинное, медленное, но авто с полным фаршем public class Winnebago { [VehicleDescription("My rocking CD player")] // Мой классный проигрыватель компакт-дисков public void PlayMusic(bool On) } } В одних случаях подобное поведение оказывается именно тем, что нужно, но в других, однако, может потребоваться создать вместо этого специальный атрибут, действие которого бы распространялось только на избранные элементы кода. Чтобы ограничить область действия специального атрибута, необходимо применить к его определению атрибут [AttributeUsage]. Атрибут [AttributeUsage] позволяет предоставлять (посредством операции OR) любую комбинацию значений из перечисления AttributeTargets: //В этом перечислении описаны целевые объекты, //на которые распространяется действие атрибута. public enum AttributeTargets { All, Assembly, Class, Constructor, Delegate, Enum, Event, Field, GenericParameter, Interface, Method, Module, Parameter, Property, ReturnValue, Struct } Более того, атрибут [AttributeUsage] также позволяет дополнительно устанавливать свойство AllowMultiple, которое указывает, может ли атрибут применяться к одному и тому же элементу более одного раза (значением по умолчанию этого свойства является false). Помимо этого он также позволяет указывать, должен ли атрибут наследоваться производными классами, за счет применения именованного свойства Inherited (значением по умолчанию этого свойства является true). Например, чтобы указать, что атрибут [VehicleDescription] может применяться к классу или структуре только один раз, необходимо обновить определение VehicleDescriptionAttribute следующим образом:
Глава 15. Рефлексия типов, позднеесвязываниеипрограммированиесиспользованиематрибутов 571 //На этот раз мы используем атрибут AttributeUsage // для аннотирования специального атрибута. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] public sealed class VehicleDescriptionAttribute : System.Attribute { } Если разработчик попытается применить атрибут [VehicleDescription] не к классу или структуре, на этапе компиляции возникнет соответствующая ошибка. Атрибуты уровня сборки и модуля Атрибуты можно также применять ко всем типам внутри конкретного модуля (если речь идет о многофайловой сборке, которые были описаны в главе 14) или ко всем модулям внутри отдельной сборки, используя, соответственно, дескрипторы [module: ] и [assembly: ]. Например, предположим, что необходимо позаботиться о соответствии требованиям CLS всех общедоступных членов во всех общедоступных типах, определенных внутри сборки. На заметку! В главе 1 рассказывалось о роли сборок, отвечающих требованиями CLS. Вспомните, что отвечающая требованиям CLS сборка может использоваться во всех языках программирования .NET. Если в общедоступных типах создаются общедоступные члены, не соответствующие требованиям, которые указаны в CLS для конструкций программирования (например, они принимают параметры без знака или указатели), их использование в других языках .NET может оказаться невозможным. По этой причине при создании библиотек кода на С#, которые должны быть пригодны для применения во множестве языков .NET, проверка на предмет соответствия требования CLS является обязательным условием. Для этого необходимо добавить в самом начале файла исходного кода на С# показанный ниже атрибут уровня сборки. Имейте в виду, что все атрибуты уровня сборки или модуля должны быть перечислены за пределами области действия любого из пространств имен! При добавлении в проект атрибутов уровня сборки (или модуля) рекомендуется следовать такой схеме в файле кода: // Сначала должны перечисляться операторы using. using System; using System. Collections .Generic- using System.Linq; using System.Text; // Далее необходимо перечислить атрибуты уровня сборки или модуля. // Следует позаботиться, чтобы все общедоступные типы в данной // сборке обязательно отвечали требованиям CLS. [assembly: CLSCompliant(true)] // Теперь можно перечислять собственные пространства имен и типы. namespace AttributedCarLibrary { // Типы... } Если теперь добавить сюда фрагмент кода, который не отвечает требованиям CLS (например, явный элемент данных без знака): // Типы ulong не отвечают требованиям CLS. public class Winnebago
572 Часть IV. Программирование с использованием сборок .NET public ulong notCompliant; } компилятор выдаст соответствующее предупреждение. Solution Explorer а* !ЗЭ Solution 'AttributedCarLibrary' (l project) л .J9 AttributedCarLibrary 4» t# Properties fj Assemblylnfo.cs j^ References <jfl Classl.cs -"^ Solution Explorer I Файл Assembly Info. cs, генерируемый Visual Studio 2010 По умолчанию в Visual Studio 2010 во все проекты автоматически добавляется файл по имени Assembly In f о . cs, который можно просмотреть, раскрыв в окне Solution Explorer узел Properties (Свойства), как показано на рис. 15.6. Этот файл представляет собой очень удобное место для размещения атрибутов, которые должны применяться на уровне сборки. В главе 14 во время обсуждения сборок .NET уже рассказывалось о том, что в манифесте содержатся метаданные уровня сборки. Большая часть из них берется из атрибутов уровня сборки, которые описаны в табл. 15.4. Таблица 15.4. Некоторые атрибуты уровня сборки Рис. 15.6. Файл Assemblylnf о. cs Атрибут Описание [AssemblyCompany] [AssemblyCopyright] [AsseniblyCulture] [AssemblyDescription] [AssemblyKeyFile] [AssemblyProduct] [AssemblyTrademark] [AssemblyVersion] Хранит общую информацию о компании Хранит любую информацию, которая касается авторских прав на данный продукт или сборку Предоставляет информацию о том, какие культуры или языки поддерживает сборка Хранит удобное для восприятия описание продукта или модулей, из которых состоит сборка Указывает имя файла, в котором содержится пара ключей, используемых для подписания сборки (т.е. создания строгого имени) Предоставляет информацию о продукте Предоставляет информацию о торговой марке Указывает информацию о версии сборки в формате <старший_ номер.младший_номер.номер_сборки.номер_редакции> Исходный код. Проект AttributedCarLibrary доступен в подкаталоге Chapter 15. Рефлексия атрибутов с использованием раннего связывания Вспомните, что атрибуты остаются бесполезными до тех пор, пока к их значениям не применяется рефлексия в каком-то другом компоненте программного обеспечения. После обнаружения атрибута в этом компоненте может предприниматься любое необходимое действие. Как и в любом приложении, обнаружение специального атрибута может быть реализовано с использованием либо раннего, либо позднего связывания.
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 573 Для применения раннего связывания определение интересующего атрибута (в данном случае VehicleDescriptionAttribute) должно присутствовать в клиентском приложении уже на этапе компиляции. С учетом того, что специальный атрибут определен в сборке At tribute dCarLibrary как общедоступный класс, использование раннего связывания будет наилучшим вариантом. Чтобы проиллюстрировать на практике рефлексию специальных атрибутов, создадим новый проект типа Console Application по имени VehicleDescriptionAttributeReader, добавим в него ссылку на сборку AttributedCarLibrary и поместим в исходный файл *. cs следующий код: // Рефлексия атрибутов с использованием раннего связывания. using System; using System.Collections.Generic; using System.Linq; using System.Text; using AttributedCarLibrary; namespace VehicleDescriptionAttributeReader { class Program { static void Main(string[ ] args) { Console.WriteLine("***** Value of VehicleDescriptionAttribute *****\n"); ReflectOnAttributesUsingEarlyBinding(); Console.ReadLine(); } private static void ReflectOnAttributesUsingEarlyBinding() { // Получение типа, представляющего Winnebago. Type t = typeof (Winnebago) ; // Получение всех атрибутов Winnebago. object[] customAtts = t.GetCustomAttributes(false); // Вывод описания. foreach (VehicleDescriptionAttribute v in customAtts) Console.WriteLine("-> {0}\n", v.Description); } } } Метод Type.GetCustomAttributes () возвращает массив объектов со всеми атрибутами, которые применяются к члену, представленному экземпляром Туре (булевский параметр указывает, должен ли поиск продолжаться вверх по цепочке наследования). После получения списка атрибутов осуществляется проход по всем классам VehicleDescriptionAttribute с отображением значения свойства Description. Исходный код. Проект VehicleDescriptionAttributeReader доступен в подкаталоге Chapter 15. Рефлексия атрибутов с использованием позднего связывания В предыдущем примере для вывода описания транспортного средства типа Winnebago применялось ранее связывание. Это было возможно благодаря тому, что тип класса VehicleDescriptionAttribute определен в сборке AttributedCarLibrary как
574 Часть IV. Программирование с использованием сборок .NET общедоступный член. Для рефлексии атрибутов также допускается применение динамической загрузки и позднего связывания. Рассмотрим пример использования этого подхода, создав новый проект по имени VehicleDescriptionAttributeReaderLateBinding, скопировав сборку AttributedCarLibrary.dll в его каталог bin\Debug и модифицировав класс Program следующим образом: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Reflection; namespace VehicleDescriptionAttributeReaderLateBinding { class Program { static void Main(string[] args) { Console.WriteLine("***** Value of VehicleDescriptionAttribute *****\n"); Ref lectAttributesUsingLateBindmg () ; Console.ReadLine(); } private static void Ref lectAttributesUsingLateBindmg () { try { // Загрузка локальной копии сборки AttributedCarLibrary. Assembly asm = Assembly.Load("AttributedCarLibrary"); // Получение информации о типе VehicleDescriptionAttribute. Type vehicleDesc = asm.GetType("AttributedCarLibrary.VehicleDescriptionAttribute" ) ; // Получение информации о типе Description. Propertylnfo propDesc = vehicleDesc.GetProperty("Description"); // Получение всех типов из сборки. Туре[] types = asm.GetTypes(); // Проход по типам с получением атрибутов VehicleDescriptionAttribute. foreach (Type t in types) { object [] objs = t.GetCustomAttributes(vehicleDesc, false); // Проход по атрибутам VehicleDescriptionAttribute и вывод // описаний с использованием позднего связывания. foreach (object о in objs) { Console.WriteLine("-> {0}: {l}\n", t.Name, propDesc.GetValue(o, null)); } } } catch (Exception ex) { Console.WriteLine(ex.Message); } } } }
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 575 Тем, кто внимательно прорабатывал приводимые ранее примеры, данный код должен быть более-менее понятен. Единственным интересным моментом здесь является использование метода Propertylnfo . GetValue (), который служит для активизации средства доступа к свойству. Ниже показано, как будет выглядеть вывод в случае выполнения текущего примера: ***** Value of VehicleDescriptionAttribute ***** -> Motorcycle: My rocking Harley -> HorseAndBuggy: The old gray mare, she ain't what she used to be... -> Winnebago: A very long, slow, but feature-rich auto Исходный код. Проект VehicleDescriptionAttributeReaderLateBinding доступен в подкаталоге Chapter 15. Возможное применение на практике рефлексии, позднего связывания и специальных атрибутов Хотя в главе и приводилось множество примеров применения данных методик, все равно может остаться вопрос, когда следует применять рефлексию, динамическое связывание и специальные атрибуты в своих приложениях. Эта тема может показаться более теоретической, нежели практической. Чтобы оценить возможность их применений в реальном мире, необходим какой-то более солидный пример. Поэтому давайте предположим, что была поставлена задача разработать так называемое расширяемое приложение, к которому можно было бы подключать сторонние инструменты. Что именно подразумевается под расширяемым приложением? Рассмотрим IDE- среду Visual Studio 2010. При разработке в этом приложении были предусмотрены специальные "ловушки" (hook) для предоставления другим производителям ПО возможности подключать свои специальные модули. Понятно, что разработчики Visual Studio 2010 не могли добавить ссылки на несуществующие внешние сборки .NET (т.е. воспользоваться ранним связыванием), тогда как же им удалось обеспечить в приложении необходимые методы-ловушки? Ниже описан один из возможных способов решения этой проблемы. • Во-первых, любое расширяемое приложение должно обязательно обладать каким- нибудь механизмом для ввода, который даст возможность пользователю указать подключаемый модуль (например, диалоговым окном или соответствующим флагом командной строки). Это требует применения динамической загрузки. • Во-вторых, любое расширяемое приложение должно обязательно быть способным определять, поддерживает ли модуль функциональные возможности (например, набор интерфейсов), необходимые для его подключения к среде. Это требует применения рефлексии. • В-третьих, любое расширяемое приложение должно обязательно получать ссылку на требуемую инфраструктуру (например, набор типов интерфейсов) и вызывать ее члены для приведения в действие лежащих в ее основе функций. Это может требовать применения позднего связывания. Если расширяемое приложение изначально программируется так, чтобы запрашивать определенные интерфейсы, оно получает возможность определять во время выполнения, может ли активизироваться интересующий тип, и после успешного прохождения типом такой проверки позволять ему поддерживать дополнительные интерфейсы и получать доступ к их функциональным возможностям полиморфным образом. Именно такой подход и предприняли разработчики Visual Studio 2010, причем ничего особо сложного в нем нет.
576 Часть IV. Программирование с использованием сборок .NET Создание расширяемого приложения В следующих подразделах будет рассмотрен завершенный пример создания расширяемого приложения Windows Forms, которое позволяет наращивать функциональные возможности за счет использования внешних сборок. Детали построения самих приложений Windows Forms здесь рассматриваться не будут (обзор API-интерфейса Windows Forms можно найти в приложении А). На заметку! API-интерфейс Windows Forms предлагался для разработки настольных приложений в .NET первоначально. После выхода версии .NET 3.0, однако, предпочитаемой графической платформой для разработки таких приложений стал API-интерфейс Windows Presentation Foundation (WPF). Несмотря на это, API-интepфeйcWlndows Forms применяется при разработке пользовательских интерфейсов в ряде рассмотренных в книге примеров клиентских приложений, поскольку его код более понятен, чем аналогичный код WPF Те, кто не знаком с процессом построения приложений Windows Forms, могут просто открыть прилагаемый демонстрационный код и изучать его по ходу прочтения дальнейшего текста. Для удобства ниже перечислены все сборки, которые потребуется создать для разрабатываемого расширяемого приложения. • CommonSnappableTypes.dll. Сборка с определениями типов, используемых каждой интегрируемой оснасткой, на которую напрямую ссылается приложение Windows Forms. • CSharpSnapIn.dll. Интегрируемая оснастка на С#, которая будет использовать типы из сборки CommonSnappableTypes . dll. • Vb Snap In. dll. Интегрируемая оснастка на Visual Basic, которая будет использовать типы из сборки CommonSnappableTypes . dll. • MyExtendableApp. ехе. Приложение Windows Forms, функциональные возможности которого должны расширяться каждой интегрируемой оснасткой. Конечно же, в этом приложении будут использоваться динамическая загрузка, рефлексия и позднее связывание, чтобы обеспечить возможность динамического получения функциональности сборок, о которых приложению заранее ничего не известно. Создание сборки CommonSnappableTypes. dll В первую очередь необходимо создать сборку с типами, которые должна обязательно использовать каждая оснастка, чтобы иметь возможность подключаться к расширяемому приложению Windows Forms. Для этого создадим проект типа Class Library (Библиотека классов), назовем его CommonSnappableTypes и определим в нем два следующих типа: namespace CommonSnappableTypes { public interface IAppFunctionality { void Dolt () ; } [AttributeUsage(AttributeTargets.Class) ] public sealed class CompanylnfoAttribute : System.Attribute { public string CompanyName { get; set; } public string CompanyUrl { get; set; } }
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 577 Интерфейс IAppFunctionality предоставляет полиморфный интерфейс для всех оснасток, которые могут подключаться к расширяемому приложению Windows Forms. Поскольку рассматриваемый пример является демонстрационным, в этом интерфейсе определяется единственный метод по имени Dolt (). В реальном проекте интерфейс мог бы генерировать код сценария, визуализировать изображение в окне инструментов приложения, интегрироваться в главное меню соответствующего приложения и т.п. Тип CompanyInfoAttribute — это специальный атрибут, который может применяться к любому классу, желающему подключиться к контейнеру. Как не трудно догадаться по определению класса, [Companylnfo] позволяет разработчику оснастки предоставлять общие сведения о месте происхождения компонента. Создание оснастки на С# Далее потребуется создать тип, реализующий интерфейс IAppFunctionality. Чтобы не усложнять пример создания расширяемого приложения, давайте сделаем этот тип простым. Создадим новый проект типа Class Library на С# по имени CSharpSnapln и определим в нем тип класса по имени CSharpModule. При этом нужно не забыть добавить ссылку на сборку CommonSnappableTypes, чтобы этот класс мог использовать определенные в ней типы (а также на сборку System. Windows . Forms . dll для того, чтобы он мог отображать осмысленное сообщение). Ниже показан необходимый код. using System; using System.Collections.Generic; using System.Linq; using System.Text; using CommonSnappableTypes; using System.Windows.Forms; namespace CSharpSnapln { [Companylnfo(CompanyName = "My Company", CompanyUrl = "www.MyCompany.com")] public class CSharpModule : IAppFunctionality { void IAppFunctionality.Dolt() { MessageBox. Show ("You have just used the C# snap in!"); } } } Обратите внимание, что для обеспечения поддержки интерфейса IAppFunctionality был выбран вариант его реализации явным образом. Применять именно такой вариант вовсе не необязательно; главное понимать, что единственной частью системы, которая нуждается в непосредственном взаимодействии с данным типом интерфейса, является обслуживающее приложение Windows. Благодаря явной реализации этого интерфейса, метод Dolt () не становится доступным прямо из типа CSharpModule. Создание оснастки на Visual Basic Чтобы сымитировать стороннего производителя, предпочитающего использовать не С#, a Visual Basic, создадим новый проект типа Class Library (Библиотека классов) на Visual Basic по имени Vb Snap In и добавим в него ссылки на те же самые внешние сборки, что и в предыдущем проекте CSharpSnapln. На заметку! По умолчанию папка References (Ссылки) для проекта на Visual Basic в окне Solution Explorer отображаться не будет, поэтому для добавления ссылок в этот проект необходимо пользоваться пунктом Add Reference (Добавить ссылку) в меню Project (Проект).
578 Часть IV. Программирование с использованием сборок .NET Необходимый код выглядит просто: Imports System.Windows.Forms Imports CommonSnappableTypes <CompanyInfo (CompanyName:=Chucky's Software, CompanyUrl:=www.ChuckySoft.com)> Public Class VbSnapIn Implements IAppFunctionality Public Sub DoIt() Implements CommonSnappableTypes.IAppFunctionality.Dolt MessageBox. Show (You have just used the VB snap in!) End Sub End Class Обратите внимание, что в синтаксисе Visual Basic для применения атрибутов используются не квадратные ([ ]), а угловые (о) скобки; кроме того, для реализации типов интерфейсов в любом конкретном классе или структуре применяется ключевое слово Implements. Создание расширяемого приложения Windows Forms И, наконец, последний шаг заключается в создании самого расширяемого приложения Windows Forms (MyExtendableApp), которое позволит пользователю выбирать желаемую оснастку с помощью стандартного диалогового окна открытия файлов Windows. Если раньше не приходилось создавать приложение Windows Forms, знайте, что для начала необходимо выбрать в диалоговом окне New Project (Новый проект) в Visual Studio 2010 проект типа Windows Forms Application (Приложение Windows Forms), как показано на рис. 15.7. New Project и • , Sort by. Default .NET Framework 4 aCJfl Windows Forms Application Visual C* ; ^f I WPF Application Visual C* jH Console Application Visual C* 5] Class Library Visual C* Pcjij WPF Browser Application Visual C# l*cjj] Empty Project Vrsual C* GO! I A project for creating an application with a Windows Forms user interface Рис. 15.7. Создание нового проекта Windows Forms в Visual Studio 2010 Теперь нужно добавить в него ссылку на сборку CommonSnappableTypes . dll, но йена библиотеки кода CSharp Snap In. dll и VbSnapIn. dll. Кроме того, необходимо импортировать в главный файл кода формы (для его открытия щелкните правой кнопкой мыши в визуальном конструкторе формы и выберите в контекстном меню пункт View Code (Просмотреть код)) пространства имен System.Reflection и CommonSnappableTypes. Вспомните, что цель создания данного приложения состоит в том, чтобы увидеть, как использовать позднее связывание и рефлексию для проверки отдельных двоичных файлов, создаваемых другими производителям, на предмет их способности выступать в роли подключаемых оснасток.
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 579 Вдаваться в детали разработки приложения Windows Forms пока не будем (это описано в приложении А), а просто разместим в окне конструктора форм компонент MenuStrip и определим для него одно главное меню File (Файл) с единственным подменю Snap In Module (Подключить модуль). Добавим в главное окно элемент ListBox (переименовав его в IstLoadedSnapIns), чтобы отображать в нем имена загружаемых пользователем оснасток. На рис. 15.8 показано, как должен выглядеть в конечном итоге графический пользовательский интерфейс разрабатываемого приложения. Рис. 15.8. Графический пользовательский интерфейс приложения MyExtendableApp Код обработки события Click, генерируемого при выборе в меню File пункта Snap In Module (который создается двойным щелчком на этом пункте меню в визуальном конструкторе форм), должен отображать диалоговое окно File Open (Открытие файла) и извлекать путь к выбранному файлу. Предполагая, что пользователь не выбрал сборку CommonSnappableTypes.dll (поскольку она обеспечивает просто инфраструктуру), этот путь затем должен пересылаться на обработку вспомогательной функции LoadExternalModule (), реализация которой показана ниже. Эта функция будет возвращать false, если не удается найти класс, реализующий IAppFunctionality. private void snapInModuleToolStripMenuItem_Click(object sender, EventArgs e) { // Позволить пользователю выбрать подлежащую загрузке сборку. OpenFileDialog dig = new OpenFileDialog(); if (dlg.ShowDialogO == DialogResult.OK) { if (dig.FileName.Contains("CommonSnappableTypes")) // CommonSnappableTypes не содержит оснасток. MessageBox.Show(ConunonSnappableTypes has no snap-ins!); else if(!LoadExternalModule(dig.FileName)) // He удается обнаружить класс, реализующий IAppFunctionality. MessageBox.Show(Nothing implements IAppFunctionality!); } } Метод LoadExternalModule () должен решать следующие задачи: • динамически загружать выбираемую сборку в память; • определять, содержатся ли в этой сборке типы, реализующие IAppFunctionality; • создавать соответствующий тип с использованием позднего связывания. Если тип, реализующий IAppFunctionality, найдет, вызывается метод Dolt () и полностью уточненное имя этого типа добавляется в ListBox (обратите внимание, что
580 Часть IV. Программирование с использованием сборок .NET в цикле f oreach осуществляется проход по всем типам, имеющимся в сборке, на случай наличия в одной сборке нескольких оснасток). private bool LoadExternalModule(string path) { bool foundSnapIn = false; Assembly theSnapInAsm = null; try { II Динамическая загрузка выбранной сборки. theSnapInAsm = Assembly.LoadFrom(path); } catch(Exception ex) { MessageBox.Show(ex.Message); return foundSnapIn; } // Получение информации обо всех совместимых // с IAppFunctionality классах в сборке. var theClassTypes = from t in theSnapInAsm.GetTypes() where t.IsClass && (t.Getlnterface("IAppFunctionality") ' = null) select t; // Создание объекта и вызов метода DoIt(). foreach (Type t in theClassTypes) { foundSnapIn = true; // Использование позднего связывания для создания типа. IAppFunctionality itfApp = (IAppFunctionality)theSnapInAsm.Createlnstance (t.FullName, true); ltfApp.DoIt(); IstLoadedSnapIns.Items.Add(t.FullName); } return foundSnapIn; } Теперь можно попробовать запустить приложение. В случае выбора сборки CSharpSnapIn.dll или VbSnapIn.dll должно появляться соответствующее сообщение. И, наконец, осталось лишь обеспечить отображение метаданных, предоставляемых атрибутом [Companylnfo]. Для этого модифицируем метод LoadExternalModule () так, чтобы перед выходом из контекста foreach в нем вызывалась еще одна вспомогательная функция по имени DisplayCompanyData (), принимающая параметр System.Type: private bool LoadExternalModule (string path) foreach (Type t in theClassTypes) { // Отображение информации о компании. DisplayCompanyData (t); } return foundSnapIn; } Теперь, имя переданный во входном параметре тип, применим в отношении атрибута [Companylnfo] рефлексию:
Глава 15. Рефлексиятипов.позднеесвязываниеипрограммированиесиспользованиематрибутов 581 private void DisplayCompanyData (Type t) { // Получение данных из атрибута [CompanyInfo]. var complnfo = from ci in t .GetCustomAttributes (false) where (ci.GetType () == typeof(CompanylnfoAttribute)) select ci; // Отображение этих данных. foreach (CompanylnfoAttribute с in complnfo) { MessageBox.Show (c.CompanyUrl, string.Format("More info about {0} can be found at", с.CompanyName)); } } На рис. 15.9 показан один из возможных вариантов вывода построенного приложе- ■У My Extensible App! File You have just used the VB snap in! ~i3QjD_sJ Рис. 15.9. Подключение внешних сборок На этом рассмотрение примера создания расширяемого приложения завершено. Благодаря этому примеру, должно стать понятно, когда изложенные в главе приемы могут оказаться полезными в реальном мире, причем не только для разработчиков специальных инструментов. Исходный код. Проекты CommonSnappableTypes, CSharpSnapIn, VbSnapIn и MyExtendableApp доступны в подкаталоге Chapter 15 внутри папки ExtendableApp. Резюме Рефлексия является очень интересным аспектом надежной среды объектно-ориентированного программирования. В мире .NET все, что касается служб рефлексии, главным образом связано с классом System. Type и пространством имен System. Reflection. Как было показано в настоящей главе, под рефлексией понимается процесс помещения типа во время выполнения под "увеличительное стекло" для получения детальной информации о его характеристиках и возможностях. Позднее связывание представляет собой процесс создания типа и вызова его членов без знания заранее того, как конкретно выглядят имена этих членов. Довольно часто оно является непосредственным результатом динамической загрузки, которая позволяет программно загружать сборку .NET в память. В демонстрировавшемся в настоящей главе примере создания расширяемого приложения было показано, что подобная методика является очень мощной и используется не только разработчиками инструментов, но и их потребителями. Кроме того, в главе рассказывалось о роли программирования с применением атрибутов. Снабжение типов атрибутами позволяет включать в исходную сборку дополнительные метаданные.
ГЛАВА 16 Процессы, домены приложений и контексты объектов Вцредыдущих двух главах рассказывалось о том, какие шаги предпринимает CLR- среда для определения местонахождения упоминаемых внешних сборок, а также о том, какую роль в .NET играют метаданные. В настоящей главе будет показано, каким образом сборки обслуживаются в CLR-среде, и описаны отношения между процессами, доменами приложений и контекстами объектов. По сути, доменом приложения (Application Domain — AppDomain) называется логический подраздел внутри отдельного процесса, в котором обслуживается набор связанных между собой сборок .NET. Как здесь будет показано, каждый домен приложения, в свою очередь, делится на отдельные так называемые контекстные границы (context boundaries), которые применяются для группирования вместе подобных .NET-объектов. Понятие контекста позволяет CLR-среде обеспечивать надлежащую обработку объектов с особыми требованиями во время выполнения. Хотя при решении многих повседневных задач программирования может не понадобиться напрямую иметь дело с процессами, доменами приложении и контекстами приложений, понимание этих тем чрезвычайно важно при работе с многочисленными API-интерфейсами .NET, в том числе Windows Communication Foundation (WCF), многопоточная и параллельная обработка и сериализация объектов. Роль процесса Windows Понятие "процесса" существовало в операционных системах Windows задолго до появления платформы .NET. Попросту говоря, под процессом понимается выполняющаяся программа. Однако формально процесс — это концепция уровня операционной системы, которая используется для описания набора ресурсов (таких как внешние библиотеки кода и главный поток) и необходимой памяти, используемой выполняющимся приложением. Для каждого загружаемого в память файла * . ехе в операционной системе создается отдельный изолированный процесс, который используется на протяжении всего времени его существования. Благодаря такой изоляции приложений, исполняющая среда получается гораздо более надежной и стабильной, поскольку выход из строя одного процесса никак не сказывается на работе других процессов.
Глава 16. Процессы, домены приложений и контексты объектов 583 Более того, доступ напрямую к данным в одном процессе из другого процесса невозможен, если только не применяется API-интерфейс распределенных вычислений, такой как Windows Communication Foundation. Из-за всех этих моментов процесс может считаться фиксированной и безопасной границей выполняющегося приложения. Каждый процесс Windows получает уникальный идентификатор процесса (Process ID — PID) и может независимо загружаться й выгружаться операционной системой (в том числе программно). Как уже наверняка известно, в окне Window&Task Manager (Диспетчер задач) имеется вкладка Processes (Процессы), на которой можно просматривать различные статические данные о выполняющихся на данной машине процессах, в том числе их PID-идентификаторы и имена образов. Чтобы открыть окно диспетчера задач (рис. 16.1), нажмите комбинацию клавиш <Ctrl+Shift+Esc>. На заметку! По умолчанию столбец РЮ (Идентификатор процесса) на вкладке Processes не отображается. Для его включения необходимо выбрать в меню View (Вид) пункт Select Columns (Выбрать столбцы) и отметить флажок PID (Process Identifier) (ИД (PID)). Applications ! Process ' Image Kame System Ide Process System iexptere.exe 2 smss.exe csrss.exe svchost.exe wtimit.exe csrss.exe wlntogon.exe services.exe Isass.exe Ism.exe svchost.exe svchost.exe svchost.exe pro' 4 108 268 -32 448 472 504 536 560 584 591 684 760 332 User Name SYSTEM SYSTEM Andrev .. SYSTEM SYSTEM LOCAL... SYSTEM SYSTEM SYSTEM SYSTEM SYSTEM SYSTEM SYSTEM NETWO... NETWO... CPU 99 'I: 00 00 00 00 00 00 00 00 00 00 00 00 00 Memory (... 24 К n0 К 8,124К 292K 1,084 К 3,664 К 636 К 1,364 К 916 К 2,560 К 2,072 К 840 К 1,836 К 2,468 К 4,232 К resr'pt.o" Per cen ta... NT Kernel... Internet... Windows ... Client Ser... HostProc... Windows... Oent Ser... Windows... Services... Local Sec... Local Ses... HostProc... HostProc... HostProc... J - Е Show processes from all users end Process CPU Usage: l^c Physical Memory: 32% Рис. 16.1. Вкладка Processes в окне диспетчера задач Windows Роль потоков В каждом процессе Windows содержится первоначальный "поток", который является входной точкой для приложения. Детали построения многопоточных приложений в .NET будут рассматриваться в главе 19, а пока для понимания излагаемого здесь материала необходимо ознакомиться с несколькими рабочими определениями. Прежде всего, потоком называется используемый внутри процесса путь выполнения. Формально поток, который создается первым во входной точке процесса, называется главным потоком (primary thread). В любой исполняемой программе .NET (консольном приложении, приложении Windows Forms, приложении WPF и т.д.) входная точка обозначается как метод Main (). При вызове этого метода главный поток создается автоматически. Процессы, в которых содержится единственный главный поток выполнения, изначально являются безопасными к потокам (thread safe), поскольку в каждый отдельный момент времени доступ к данным приложения в них может получать только один по-
584 Часть IV. Программирование с использованием сборок .NET ток. Однако подобные однопоточные процессы (особенно с графическим пользовательским интерфейсом) часто замедленно реагируют на действия пользователя, когда их единственный поток выполняет какую-то сложную операцию (вроде вывода на печать длинного текстового файла, сложных математических вычислений или подключения к удаленному серверу). Из-за такого потенциального недостатка однопоточных приложений, API-интерфейс Windows (а также платформа .NET) предоставляет возможность для главного потока порождать дополнительные вторичные потоки (также называемые рабочими потоками). Это делается с применением набора функций из API-интерфейса Windows, таких как CreateThreadO . Каждый поток (первичный или вторичный) в процессе становится уникальным путем выполнения и может параллельно получать доступ ко всем разделяемым элементам данных внутри соответствующего процесса. Как не трудно догадаться, разработчики обычно создают дополнительные потоки для улучшения общей степени восприимчивости программы к действиям пользователя. Многопоточные процессы обеспечивают иллюзию того, что выполнение многочисленных действий происходит примерно в одно и то же время. Например, дополнительный рабочий поток может порождаться в приложении для выполнения какой-нибудь трудоемкой задачи (подобной выводу на печать большого текстового файла). После начала выполнения задачи вторичным потоком основной поток все равно не утрачивает способности реагировать на действия пользователя, что дает всему процессу возможность сопровождаться куда более высокой производительностью. Однако такого может и не происходить: в случае использования слишком большого количества потоков в одном процессе его производительность может даже ухудшаться из-за возникновения у ЦП необходимости переключаться между активными потоками в процессе (что отнимает определенное время). На некоторых машинах многопоточность по большей части представляет собой не более чем просто обеспечиваемую операционной системой иллюзию. Машины с одним (не поддерживающим гиперпотоки) процессором буквально не имеют никакой возможности обрабатывать множество потоков в точности в одно и то же время. Вместо этого они выполняют по одному потоку за единицу времени (называемую квантом), основываясь отчасти на приоритете потока. По истечении выделенного кванта времени выполнение существующего потока приостанавливается для предоставления другому потоку возможности выполнить свою задачу. Чтобы поток не забывал, на чем он работал перед тем, как его выполнение было приостановлено, каждому потоку предоставляется возможность записывать данные в локальное хранилище потоков (Thread Local Storage — TLS) и выделяется отдельный стек вызовов, как показано на рис. 16.2. Одиночный процесс Windows Разделяемые данные Поток А Локальное хранилище потоков (TLS) Стек вызовов Поток В Локальное хранилище потоков(TLS) Стек вызовов Рис. 16.2. Отношения между потоками и процессами в Windows
Глава 16. Процессы, домены приложений и контексты объектов 585 Если тема потоков является для вас новой, не стоит беспокоиться по поводу деталей. На данном этапе главное запомнить, что любой поток представляет собой просто уникальный путь выполнения внутри процесса Windows, а каждый процесс обязательно имеет главный поток (который создается в точке входа в приложение) и может содержать дополнительные потоки, создаваемые программным образом. Взаимодействие с процессами в рамках платформы .NET Хотя в самих процессах и потоках нет ничего нового, способ, которым с ними можно взаимодействовать в рамках платформы .NET, довольно прилично изменился (в лучшую сторону). Чтобы проложить себе путь к пониманию приемов построения многопоточных сборок (о которых речь пойдет в главе 19), начнем с того, что посмотрим, каким образом можно взаимодействовать с процессами за счет применения библиотек базовых классов .NET. В пространстве имен System. Diagnostics поставляется набор типов, которые позволяют программно взаимодействовать с процессами и различными связанными с диагностикой средствами вроде системного журнала событий и счетчиков производительности. В настоящей главе нас интересуют только те типы, которые позволяют взаимодействовать с процессами. Некоторые наиболее важные из них перечислены в табл. 16.1. Таблица 16.1. Некоторые типы из пространства имен System.Diagnostics Типы в System.Diagnostics, которые позволяют Описание взаимодействовать с процессами Process Предоставляет доступ к локальным и удаленным процессам, а также позволяет запускать и останавливать процессы программным образом ProcessModule Представляет модуль (* . dll или * . ехе), который должен загружаться в определенный процесс. Важно понимать, что этот тип может применяться для представления любого модуля — COM-, .NET- или традиционного двоичного на базе С ProcessModuleCollection Позволяет создавать строго типизированную коллекцию объектов ProcessModule ProcessStartlnf о Позволяет указывать набор значений, которые должны использоваться при запуске процесса посредством метода Process.Start () ProcessThread Представляет поток внутри определенного процесса. Следует иметь в виду, что этот тип применяется для диагностики набора потоков в процессе, но не для ответвления внутри него новых потоков ProcessThreadCollection Позволяет создавать строго типизованную коллекцию объектов ProcessThread Класс System. Diagnostics . Process позволяет анализировать процессы, которые выполняются на какой-то определенной машине (локальной или удаленной). Кроме того, в нем есть члены, которые позволяют программно запускать и останавливать процессы, просматривать приоритет процесса, а также получать список активных потоков
586 Часть IV. Программирование с использованием сборок .NET и/или модулей, которые были загружены в данный процесс. В табл. 16.2 перечислены некоторые наиболее важные свойства класса System. Diagnostics . Process. Таблица 16.2. Избранные свойства класса Process Свойство Описание ExitTime Handle Id MachmeName MainWmdowTitle Modules ProcessName Responding StartTime Threads Это свойство позволяет извлекать значение даты и времени, ассоциируемое с процессом, который завершил свою работу (и представленное типом DateTime). Это свойство возвращает дескриптор (представляемый с помощью IntPtr), который был назначен процессу операционной системой. Может оказаться полезным при создании приложений .NET, нуждающихся во взаимодействии с неуправляемым кодом. Это свойство позволяет получать идентификатор (PID) соответствующего процесса. Это свойство позволяет получать имя компьютера, на котором выполняется соответствующий процесс. Это свойство позволяет получать заголовок главного окна процесса (если у процесса нет главного окна, возвращается пустая строка). Это свойство позволяет получать доступ к строго типизованной коллекции ProcessModuleCollection, представляющей набор модулей (* . dll или * . ехе), которые были загружены в рамках текущего процесса. Это свойство позволяет получать имя процесса (которое совпадает с именем самого приложения) Это свойство позволяет получать значение, показывающее, реагирует ли пользовательский интерфейс соответствующего процесса на действия пользователя (и не находится ли он в текущий момент в "зависшем" состоянии). Это свойство позволяет получать информацию о том, когда был запущен соответствующий процесс (представленную типом DateTime). Это свойство позволяет получать информацию о том, какие потоки выполняются в рамках соответствующего процесса (в виде коллекции объектов ProcessThread). Помимо перечисленных выше свойств, класс System. Diagnostics . Process имеет несколько полезных методов, часть из которых описана в табл. 16.3. Таблица 16.3. Избранные методы класса Process Метод Описание CloseMainWindow() GetCurrentProcess() GetProcesses() Kill() Start () Этот метод позволяет завершать процесс, обладающий пользовательским интерфейсом, за счет отправки в его главное окно сообщения о закрытии. Этот статический метод возвращает новый объект Process, представляющий процесс, который является активным в текущий момент. Этот статический метод возвращает массив новых объектов Process, которые выполняются на текущей машине. Этот метод позволяет немедленно останавливать соответствующий процесс. Этот метод позволяет запускать процесс.
Глава 16. Процессы, домены приложений и контексты объектов 587 Перечисление выполняющихся процессов Чтобы увидеть, как манипулировать объектами Process, создадим новый проект типа Console Application (Консольное приложение) на С# по имени ProcessManipulator и определим в нем внутри класса Program следующий вспомогательный статический метод (не забыв, конечно же, импортировать пространство имен System.Diagnostics): public static void ListAllRunningProcesses() { // Получение списка всех процессов, которые выполняются //на текущей машине, упорядоченных по PID. var runmngProcs = from proc in Process.GetProcesses (". ") orderby proc.Id select proc; // Отображение идентификатора и имени каждого процесса. foreach (var p in runmngProcs) { string info = string.Format("-> PID: {0}\tName: {1}", p.Id, p.ProcessName); Console.WriteLine (info); } Console WriteLine(n************************************\n") • } Обратите внимание, что статический метод Process .GetProcesses () возвращает массив объектов Process, которые представляют выполняющиеся на целевой машине процессы (используемая здесь нотация в виде точки свидетельствует о том, что в данном случае целевой машиной является локальный компьютер). После получения массива объектов Process можно обращаться к любому члену, которые описаны в табл. 16.2 и 16.3. В примере просто выводятся идентификаторы (PID) и имена всех процессов в порядке следования их PID-идентификаторов. Добавив в Main () вызов метода ListAllRunningProcesses (): static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Processes *****\n"); ListAllRunningProcesses (); Console.ReadLine (); } можно будет увидеть список имен и PID-идентификаторов всех процессов, выполняющихся в текущий момент на локальном компьютере. Ниже показано, как может выглядеть вывод: ***** pun with Processes ***** -> -> -> -> -> -> -> -> -> -> -> -> '-> -> PID: PID: PID: PID: PID: PID: PID: PID: PID: PID: PID: PID: PID: PID: 0 4 108 268 432 448 472 504 536 560 584 592 660 684 Name: Name : Name: Name: Name: Name: Name: Name: Name: Name: Name: Name: Name : Name: Idle System lexplore smss csrss svchost wininit csrss winlogon services lsass Ism devenv svchost
588 Часть IV. Программирование с использованием сборок .NET > PID: > PID: > PID: > PID: > PID: > PID: > PID: > PID: > PID: 760 832 844 856 900 924 956 1116 1136 Name: Name: Name: Name: Name: Name: Name: Name: Name: svchost svchost svchost svchost svchost svchost VMwareService spoolsv ProcessManipulator.vshost •••••••••••••••••••••••••••••••••••• Изучение конкретного процесса Помимо полного списка всех выполняющихся на конкретной машине процессов, статический метод Process. GetProcessByld () позволяет получать информацию и по конкретному объекту Process за счет указания ассоциируемого с ним идентификатора (PID). В случае запроса несуществующего PID генерируется исключение ArgumentException. Например, чтобы получить информацию об объекте Process, представляющем процесс с РШ-идентификатором 987, можно написать следующий код: // Если процесс с PID 987 не существует, сгенерируете* исключение. static void GetSpeciflcProcess () { Process theProc = null; try { theProc = Process.GetProcessByld(987); } catch(ArgumentException ex) Console.WriteLine(ex.Message); } К этому моменту уже было показано, как получать список всех процессов и сведения о конкретном процессе на машине, выполняя поиск по PID-идентификатору. Класс Process также позволяет просмотреть набор текущих потоков и библиотек, используемых в заданном процессе. Давайте посмотрим, как это делается. Изучение набора потоков процесса Набор потоков представляется в виде строго типизованной коллекции ProcessThread Collection, в которой содержится определенное количество отдельных объектов ProcessThread. Для примера предположим, что в текущее приложение добавлена следующая вспомогательная статическая функция: static void EnumThreadsForPid (int pID) { Process theProc = null; try { theProc = Process.GetProcessByld(pID); } catch(ArgumentException ex) { Console.WriteLine(ex.Message); return;
Глава 16. Процессы, домены приложений и контексты объектов 589 // Отображение статистических данных по каждому потоку //в указанном процессе. Console.WriteLine("Here are the threads used by: {0}", theProc.ProcessName); ProcessThr.eadCollection theThreads = theProc.Threads; foreach(ProcessThread pt in theThreads) { string info = string.Format("-> Thread ID: {0}\tStart Time {1}\tPriority {2}", pt.Id , pt.StartTime.ToShortTimeString (), pt. PnontyLevel) ; Console.WriteLine(info); } Console.WriteLine (n************* ^ ********************** \^\и) • } Как не трудно заметить, свойство Threads в System. Diagnostics . Process предоставляет доступ к классу ProcessThreadCollection. Здесь с его помощью обеспечивается отображение информации о назначенном идентификаторе, времени запуска и уровне приоритета каждого из потоков, используемых в указанном клиентом процессе. Давайте теперь обновим метод Main () в классе Program так, чтобы он запрашивал у пользователя PID-идентификатор интересующего процесса: static void Main(string [ ] args) // Запрашивание у пользователя PID-идентификатора и вывод информации //о соответствующих активных потоках в окне консоли. Console.WriteLine ("***** Enter PID of process to investigate *****"); Console.Write("PID: "); string pID = Console.ReadLine(); int theProcID = int.Parse(pID); EnumThreadsForPid(theProcID); Console.ReadLine(); } Запустив приложение, можно вводить PID-идентификатор любого процесса на машине и просматривать используемые внутри него потоки. Ниже показан пример вывода в случае просмотра информации о потоках, используемых в процессе с PID- идентификатором 108, который (так получилось) отвечает за обслуживание Microsoft Internet Explorer: ***** Enter PID of process to investigate ***** PID: 108 * Here are the threads used by: lexplore > > > > > > > > > > > > > > Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread ID: ID: ID: ID: ID: ID: ID: ID: ID: ID: ID: ID: ID: ID: 680 2040 880 3380 3376 3448 3476 2264 2380 2384 2308 3096 3600 1412 Start Start Start Start Start Start Start Start Start Start Start Start Start Start Time: Time: Time: Time: Time: Time: Time: Time: Time: Time: Time: Time: Time: Time: 9: 9: 9: 9: 9: 9: 9: 9: 9: 9: 9: 9: 9: :05 :05 :05 :05 :05 :05 :05 :05 :05 :05 :05 :07 :45 AM AM AM AM AM AM AM AM AM AM AM AM AM 10:02 AM Priority: Priority: Priority: Priority: Priority: Priority: Priority: Priority: Priority: Priority: Priority: Priority: Priority: Priority: Normal Normal Normal Normal Normal Normal Normal Normal Normal Normal Normal Highest Normal Normal
590 Часть IV. Программирование с использованием сборок .NET Помимо Id, StartTime и PriorityLevel, класс ProcessThread имеет дополнительные члены. Некоторые наиболее интересные из них перечислены в табл. 16.4. Таблица 16.4. Избранные члены класса ProcessThread Член Описание CurrentPriority Позволяет получить информацию о текущем приоритете потока Id Позволяет получить уникальный идентификатор потока IdealProcessor Позволяет указать предпочитаемый процессор для выполнения данного потока PriorityLevel Позволяет получить или задать уровень приоритета потока ProcessorAf f inity Позволяет указать процессоры, на которых может выполняться соответствующий поток Start Address Позволяет узнать, по какому адресу в памяти операционная система вызывала функцию, приведшую к запуску данного потока StartTime Позволяет узнать, когда операционная система запустила поток ThreadState Позволяет узнать текущее состояние данного потока TotalProcessorTime Позволяет узнать, сколько всего времени данный поток использовал процессор WaitReason Позволяет узнать причину, по которой поток находится в состоянии ожидания Прежде чем двигаться дальше, необходимо четко уяснить, что тип ProcessThread не является сущностью, применяемой для создания, приостановки и уничтожения потоков в .NET. Он скорее представляет собой средство, позволяющее получать диагностическую информацию по активным потокам Windows внутри выполняющегося процесса. Более подробно о том, как создавать многопоточные приложения с применением пространства имен System.Threading, речь пойдет в главе 19 Изучение набора модулей процесса Теперь давайте посмотрим, как реализовать проход по загруженным модулям, которые обслуживаются в рамках конкретного процесса. При обсуждении процессов модуль — это общий термин, используемый для описания заданной сборки * .dll (или * . ехе), которая обслуживается в определенном процессе. При получении доступа к ProcessModuleCollection через свойство Process .Modules можно извлечь список всех модулей, которые обслуживаются внутри процесса: .NET-, COM- и традиционных основанных на С библиотек. Для примера создадим показанную ниже дополнительную вспомогательную функцию, способную перечислять модули в конкретном процессе на основе предоставляемого PID-идентификатора: static void EnumModsForPid (int pID) { Process theProc = null; try { theProc = Process.GetProcessByld(pID); }
Глава 16. Процессы, домены приложений и контексты объектов 591 catch(ArgumentException ex) { Console.WriteLine(ex.Message); return; } Console.WriteLine ("Here are the loaded modules for: {0}", theProc.ProcessName); try { ProcessModuleCollection theMods = theProc.Modules; foreach(ProcessModule pm in theMods) { string info = string.Format("-> Mod Name: {0}", pm.ModuleName); Console.WriteLine(info); } Чтобы посмотреть на вывод, давайте попробуем узнать, как будут выглядеть загружаемые модули для процесса, обслуживающего текущее демонстрационное приложение (ProcessManipulator). Для этого сначала необходимо запустить приложение, выяснить, какой PID-идентификатор был присвоен ProcessManipulator.exe (с помощью окна Task Manager), и передать это значение методу EnumModsForPid () (разумеется, не забыв перед этим соответствующим образом обновить метод Main ()). После этого можно будет оценить, насколько много модулей * .dll (GDI 32 .dll, USER32 .dll, ole32.dll и т.д.) используется для довольно простого консольного приложения: Here are the loaded modules for: ProcessManipulator -> Mod Name: ProcessManipulator.exe -> Mod Name: ntdll.dll -> Mod Name: MSCOREE.DLL -> Mod Name: KERNEL32.dll -> Mod Name: KERNELBASE.dll -> Mod Name: ADVAPl32.dll -> Mod Name: msvcrt.dll -> Mod Name: sechost.dll -> Mod Name: RPCRT4.dll -> Mod Name: SspiCli.dll -> Mod Name: CRYPTBASE.dll -> Mod Name: mscoreei.dll -> Mod Name: SHLWAPI.dll -> Mod Name: GDI32.dll -> Mod Name: USER32.dll -> Mod Name: LPK.dll -> Mod Name: USP10.dll -> Mod Name: IMM32.DLL -> Mod Name: MSCTF.dll -> Mod Name: clr.dll -> Mod Name: MSVCR100_CLR04 00.dll -> Mod Name: mscorlib.ni.dll -> Mod Name: nlssorting.dll -> Mod Name: ole32.dll -> Mod Name: clrjit.dll -> Mod Name: System.ni.dll -> Mod Name: System.Core.ni.dll -> Mod Name: psapi.dll -> Mod Name: shfolder.dll -> Mod Name: SHELL32.dll ' ************************************
592 Часть IV. Программирование с использованием сборок .NET Запуск и останов процессов программным образом И, наконец, последними членами класса System. Diagnostics . Process, которые осталось рассмотреть, являются методы Start () и Kill (). Эти методы позволяют, соответственно, программно запускать и завершать процесс. В качестве примера создадим вспомогательный статический метод StartAndKi 11 Process (), код которого показан ниже. На заметку! Для того чтобы запускать новые процессы, необходимо запускать Visual Studio 2010 от имени администратора. В противном случае во время выполнения будет возникать ошибка static void StartAndKillProcess () { Process leProc = null; // Запустить Internet Explorer и перейти на страницу facebook.com. try { leProc = Process . Start ("IExplore . exe", "www.facebook.com11); } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); } Console.Write("--> Hit enter to kill {0}...", leProc.ProcessName); Console.ReadLine(); // Уничтожить процесс iexplore.exe. try { ieProc.Kill(); } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); } } Статический метод Process . Start () имеет несколько перегруженных версий. Как минимум, необходимо указать дружественное имя запускаемого процесса (такое как iexplore.exeB случае Microsoft Internet Explorer). В данном примере используется версия метода Start (), которая позволяет указывать любое количество дополнительных аргументов, подлежащих передаче в точку входа программы (т.е. метод Main ()). После вызова метода Start () возвращается ссылка на только что запущенный процесс. Чтобы завершить процесс, нужно вызывать на уровне соответствующего экземпляра метод Kill () . Вызовы Start () и Kill () помещены внутрь блока try/catch, и предусмотрена обработка любых ошибок InvalidOperationException. Это особенно важно при вызове метода Kill (), поскольку такая ошибка будет обязательно возникать в случае завершения процесса до вызова Kill (). Управление запуском процесса с использованием класса ProcessStartlnfо Метод Start () может принимать тип System. Diagnostics . ProcessStartlnf о и предоставлять дополнительные фрагменты информации относительно запуска определенного процесса. Ниже показано частичное определение ProcessStartlnfo (полное определение можно найти в документации .NET Framework 4.0 SDK):
Глава 16. Процессы, домены приложений и контексты объектов 593 public sealed class ProcessStartlnfo : object { public ProcessStartlnfo (); public ProcessStartlnfo(string fileName); public ProcessStartlnfo(string fileName, string arguments); public string Arguments { get; set; } public bool CreateNoWindow { get; set; } public StringDictionary EnvironmentVariables { get; } public bool ErrorDialog { get; set; } public IntPtr ErrorDialogParentHandle { get; set; } public string FileName { get; set; } public bool LoadUserProfile { get; set; } public SecureString Password { get; set; } public bool RedirectStandardError { get; set; } public bool RedirectStandardlnput { get; set; } public bool RedirectStandardOutput { get; set; } public Encoding StandardErrorEncoding { get; set; } public Encoding StandardOutputEncoding { get; set; } public bool UseShellExecute { get; set; } public string Verb { get; set; } public string [] Verbs { get; } public ProcessWindowStyle WindowStyle { get; set; } public string WorkingDirectory { get; set; } } Чтобы просмотреть, как с помощью этого класса настроить запуск процесса, изменим метод StartAndKillProcess () так, чтобы он теперь предусматривал не только загрузку браузера Microsoft Internet Explorer и переход на страницу www. f acebook. com, но и отображение окна этого браузера в развернутом виде: static void StartAndKillProcess () { Process leProc = null; // Запуск браузера Internet Explorer и переход на страницу // facebook.com с отображением окна в развернутом виде. try { ProcessStartlnfo startlnfo = new ProcessStartlnfo ("IExplore.exe", "www.facebook.com11) ; startlnfo.WindowStyle = ProcessWindowStyle.Maximized; leProc = Process.Start(startlnfo) ; } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); } Теперь, когда известна роль процессов Windows и способы взаимодействия с ними в коде С#, можно спокойно переходить к изучению доменов приложений .NET. Исходный код. Проект ProcessManipulator доступен в подкаталоге Chapter 16.
594 Часть IV. Программирование с использованием сборок .NET Домены приложений .NET В .NET исполняемые файлы не обслуживаются прямо внутри процесса Windows, как это происходит в случае традиционных неуправляемых приложений. Вместо этого они обслуживаются в отдельном логическом разделе внутри процесса, который называется доменом приложения (Application Domain — AppDomain). Как будет показано, в единственном процессе может содержаться несколько доменов приложений, каждый из которых обслуживает свой исполняемый файл .NET. Такое дополнительное подразделение традиционного процесса Windows предоставляет ряд преимуществ, главные из которых описаны ниже. • Домены приложений играют ключевую роль в обеспечении нейтральности платформы .NET по отношению к операционной системе из-за того, что такое логическое разделение стирает отличия в способе представления загружаемого исполняемого файла лежащей в основе операционной системой. • Домены приложений являются гораздо менее дорогостоящими в плане потребления вычислительных ресурсов и памяти по сравнению с полноценными процессами. Благодаря этому CLR-среде удается загружать и выгружать домены приложений намного быстрее, чем формальные процессы, и тем самым значительно улучшать масштабируемость серверных приложений. • Домены приложений обеспечивают более глубокий уровень изоляции для обслуживания загружаемого приложения. В случае выхода из строя какого-то одного домена приложения внутри процесса, остальные домены приложений все равно остаются работоспособными. Как упоминалось выше, в одном процессе может обслуживаться любое количество доменов с полной изоляцией каждого из них от всех остальных доменов приложений внутри этого же (или любого другого) процесса. Следует очень четко осознавать, что приложение, выполняющееся в одном домене приложения, не может получать данные любого рода (глобальные перемененные или статические поля) из другого домена приложения, если только не будет использоваться какой-то протокол распределенного программирования (наподобие Windows Communication Foundation). Хотя в одном процессе может обслуживаться множество доменов приложений, обычно такого не происходит. Как минимум, в любом процессе операционной системы всегда обслуживается так называемый домен приложения по умолчанию (default application domain). Этот специальный домен приложения создается автоматически CLR- средой во время запуска процесса. После этого CLR-среда создает все остальные дополнительные домены приложений по мере необходимости. Класс System. AppDomain Платформа .NET позволяет программно осуществлять мониторинг доменов приложений, создавать новые домены приложений (или выгружать их) во время выполнения, загружать в домены приложений различные сборки и решать целый ряд других задач с применением класса AppDomain из пространства имен System, которое находится в сборке mscorlib. dll. В табл. 16.5 перечислены наиболее полезные методы этого класса (полную информацию об этом классе и его членах можно, как обычно, найти в документации .NET Framework 4.0 SDK).
Глава 16. Процессы, домены приложений и контексты объектов 595 Таблица 16.5. Некоторые методы класса AppDomain Член Описание CreateDomain() Createlnstance() ExecuteAssembly() GetAssemblies() GetCurrentThreadld() Load() Unload() Этот статический метод позволяет создавать новый домен приложения в текущем процессе Этот метод позволяет создавать экземпляр типа из внешней сборки после загрузки соответствующей сборки в вызывающий домен приложения Этот метод позволяет запускать сборку * . ехе внутри домена приложения за счет предоставления имени ее файла Этот метод позволяет узнать, какие сборки .NET были загружены в данный домен приложения (двоичные файлы СОМ и С игнорируются) Этот статический метод возвращает идентификатор потока, который является активным в текущем домене приложения Этот метод применяется для динамической загрузки сборки в текущий домен приложения Этот статический метод позволяет выгрузить определенный домен приложения из конкретного процесса На заметку! Платформа .NET не позволяет производить выгрузку конкретной сборки из памяти. Единственным способом для осуществления выгрузки библиотек программным образом является разрушение обслуживающего домена приложения с помощью метода Unload (). Кроме того, класс AppDomain имеет свойства, которые могут быть полезны для проведения мониторинга за каким-то доменом приложения. Наиболее интересные свойства такого рода кратко описаны в табл. 16.6. Таблица 16.6. Некоторые свойства класса AppDomain Событие Описание BaseDirectory CurrentDomain FriendlyName MonitoringIsEnabled Setuplnformation Позволяет извлечь путь к каталогу, который преобразователь адресов использует для поиска сборок Представляет собой статическое свойство и позволяет узнать домен приложения, используемый для выполняющегося в текущий момент потока Позволяет получить дружественное имя текущего домена приложения Позволяет получить или установить значение, указывающее, должна ли работать функция мониторинга за использованием ресурсов ЦП и памяти для текущего процесса. После включения функции мониторинга для процесса отключить ее нельзя Позволяет извлечь детали конфигурации определенного домена приложения, которые предоставляются в виде объекта AppDomainSetup И, наконец, класс AppDomain поддерживает набор событий, которые отражают различные аспекты жизненного цикла домена приложения. Некоторые наиболее полезные из них перечислены в табл. 16.7.
596 Часть IV. Программирование с использованием сборок .NET Таблица 16.7. Некоторые события класса AppDomain Событие Описание AssemblyLoad Возникает при загрузке сборки в память AssemblyResolve Возникает, когда преобразователю адресов сборок не удается обнаружить место расположения требуемой сборки DomainUnload Возникает перед началом выгрузки домена приложения из обслуживающего процесса FirstChanceException Позволяет получать уведомление о том, что в домене приложения было сгенерировано какое-то исключение, перед началом выполнения CLR-средой поиска подходящего оператора catch ProcessExit Возникает в используемом по умолчанию домене приложения тогда, когда его родительский процесс завершает работу UnhandledException Возникает при отсутствии обработчика, способного перехватить данное исключение Взаимодействие с используемым по умолчанию доменом приложения Вспомните, что при запуске исполняемого файла .NET среда CLR автоматически помещает его в используемый по умолчанию домен приложения внутри обслуживающего процесса. Это происходит автоматически и прозрачно, потому писать какой-то специальный код не понадобится. К используемому по умолчанию домену приложения можно получить доступ в своем приложении с применением статического свойства AppDomain.CurrentDomain. Затем можно привязываться к любым интересующим событиям и использовать любые желаемые методы и свойства AppDomain для проведения диагностики во время выполнения. Чтобы посмотреть на взаимодействие с используемым по умолчанию доменом приложения, давайте создадим новый проект типа Console Application по имени Def aultAppDomainApp. Модифицируем класс Program, включив в него приведенный ниже код, который предусматривает вывод детальных сведений об используемом по умолчанию домене приложения с помощью набора членов класса AppDomain. class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Fun with the default app domain *****\n"); DisplayDADStats(); Console.ReadLine(); } private static void DisplayDADStats() { // Получение доступа к домену приложения, // используемому для текущего потока по умолчанию. AppDomain defaultAD = AppDomain.CurrentDomain; // Вывод в окно консоли статистических данных об этом домене. Console.WriteLine("Name of this domain: {0}", defaultAD.FriendlyName); // Дружественное имя Console.WriteLine("ID of domain in this process: {0}", defaultAD.Id); // Идентификатор
Глава 16. Процессы, домены приложений и контексты объектов 597 Console.WriteLine ("Is this the default domain?: {0}", defaultAD.IsDefaultAppDomain()); // Используется ли по умолчанию Console.WriteLine("Base directory of this domain: {0}", defaultAD.BaseDirectory); // Базовый каталог } } Ниже показано, как будет выглядеть вывод в результате выполнения этого кода: ***** Fun with the default app domain ***** Name of this domain: DefaultAppDomainApp.exe ID of domain in this process: 1 Is this the default domain?: True Base directory of this domain: E:\MyCode\DefaultAppDomainApp\bin\Debug\ Обратите внимание, что имя используемого по умолчанию домена будет совпадать с именем обслуживаемого внутри него исполняемого файла (в данном примере DefaultAppDomainApp), а значение базового каталога, которое будет использоваться для поиска требуемых внешних приватных сборок — с текущим месторасположением этого исполняемого файла. Перечисление загружаемых сборок С применением на уровне экземпляра метода GetAssemblies () можно просмотреть все сборки .NET, загруженные в заданный домен приложения. Этот метод будет возвращать массив объектов типа Assembly, который, как было показано в предыдущей главе, является членом пространства имен System. Reflection (и потому требует импорта этого пространства имен в файл кода С#). Чтобы увидеть все это на примере, определим в классе Program новый вспомогательный метод по имени ListAllAssembliesInAppDomain (), который будет получать список всех загружаемых сборок и отображать дружественное имя и номер версии каждой из них в окне консоли. static void ListAllAssembliesInAppDomain () { // Доступ к домену приложения по умолчанию для текущего потока. AppDomain defaultAD = AppDomain.CurrentDomain; // Извлечение списка всех сборок, загруженных в этот домен приложения. Assembly[] loadedAssemblies = defaultAD.GetAssemblies(); Console.WriteLine("***** Here are the assemblies loaded in {0} *****\n", defaultAD.FriendlyName); foreach(Assembly a in loadedAssemblies) { Console.WriteLine("-> Name: {0}", a.GetName().Name); Console.WriteLine("-> Version: {0}\n", a.GetName() .Version) ; } } Добавив в метод Main () вызов этого нового члена, можно просмотреть все сборки .NET, используемые в домене приложения, который обслуживает исполняемый файл: ***** Here are the assemblies loaded in DefaultAppDomainApp.exe ***** -> Name: mscorlib -> Version: 4.0.0.0 -> Name: DefaultAppDomainApp -> Version: 1.0.0.0 Важно понимать, что список используемых сборок при написании другого кода на С# может приобретать совершенно иной вид. Например, изменим метод
598 Часть IV. Программирование с использованием сборок .NET ListAllAssembliesInAppDomain () так, чтобы в нем использовался LINQ-запрос, группирующий сборки по имени: static void ListAllAssembliesInAppDomain() { // Доступ к домену приложения по умолчанию для текущего потока. AppDomain defaultAD = AppDomain.CurrentDomain; // Извлечение списка всех сборок, загруженных в этот домен приложения. var loadedAssemblies = from a in defaultAD.GetAssemblies() orderby a.GetName().Name select a; Console.WriteLine ("***** Here are the assemblies loaded in {0} *****\n", defaultAD.FriendlyName); foreach (var a in loadedAssemblies) { Console.WriteLine("-> Name: {0}", a.GetName().Name); Console.WriteLine("-> Version: {0}\n", a.GetName().Version); } } Запустив приложение, можно увидеть, что в память также загружены сборки System.Core.dll и System.dll, поскольку они требуются для работы API-интерфейса LINQ to Objects: ***** Here are the assemblies loaded in DefaultAppDomainApp.exe ***** -> Name: DefaultAppDomainApp -> Version: 1.0.0.0 -> Name: mscorlib -> Version: 4.0.0.0 -> Name: System -> Version: 4.0.0.0 -> Name: System.Core -> Version: 4.0.0.0 Получение уведомлений о загрузке сборок Для получения уведомлений от CLR-среды при загрузке новой сборки в определенный домен приложения необходимо организовать обработку события AssemblyLoad. Это событие имеет тип делегата AssemblyLoadEventHandler, который может указывать на любой метод, принимающий System. Ob j ect в первом параметре и AssemblyLoadEventArgs — во втором. Для примера добавим в текущий класс Program еще один метод по имени InitDAD (), который будет инициализировать используемый по умолчанию домен приложения за счет обработки события AssemblyLoad с помощью подходящего лямбда-выражения: private static void InitDAD() { // Эта логика будет выводить в окно консоли имя любой сборки, // загружаемой в домен приложения после его создания. AppDomain defaultAD = AppDomain.CurrentDomain; defaultAD.AssemblyLoad += (o, s) => { Console.WriteLine("{0} has been loaded!", s.LoadedAssembly.GetName() .Name); }; } Как и следовало ожидать, после этого при загрузке любой новой сборки будет появляться соответствующее уведомление. В примере просто выводится дружественное имя сборки с использованием свойства LoadedAssembly входящего параметра AssemblyLoadedEventArgs.
Глава 16. Процессы, домены приложений и контексты объектов 599 Исходный код. Проект DefaultAppDomainApp доступен в подкаталоге Chapter 16. Создание новых доменов приложений Вспомните, что один процесс способен обслуживать множество доменов приложений посредством статического метода Арр Domain. Create Domain (). И хотя необходимость в создании новых доменов приложений на лету в большинстве приложений .NET возникает крайне редко, важно в общем понимать, как это делается. Например, в главе 17 будет показано, что создаваемые динамические сборки должны устанавливаться в специальный домен приложения. Вдобавок многие API-интерфейсы, связанные с безопасностью .NET, требуют понимания того, каким образом создавать новые домены приложения для изолирования сборок на основе предоставляемых учетных данных безопасности. Давайте рассмотрим пример создания специальных доменов приложений налету (и загрузки в них новых сборок), для чего создадим проект типа Console Application (Консольное приложение) по имени CustomAppDomains. Метод AppDomain.CreateDomain () имеет несколько перегруженных версий. Как минимум, ему требуется предоставить дружественное имя создаваемого домена приложения. Поместим в класс Program показанный ниже код. В этом коде используется метод ListAllAssembliesInAppDomain () из предыдущего примера, но на этот раз ему для анализа в качестве входного аргумента передается объект AppDomain. class Program { static void Main(string [ ] args) { Console.WriteLine ("***** Fun with Custom App Domains *****\n"); // Отображение всех сборок, которые были загружены в // используемый по умолчанию домен приложения. AppDomain defaultAD = AppD^nain.CurrentDomain; ListAllAssembliesInAppDomain(defaultAD); // Создание нового домена приложения. MakeNewAppDomain(); Console.ReadLine(); } private static void MakeNewAppDomain () { // Создание нового домена приложения в текущем процессе и // перечисление всех загруженных в него сборок. AppDomain newAD = AppDomain.CreateDomain("SecondAppDomain"); ListAllAssembliesInAppDomain(newAD); } static void ListAllAssembliesInAppDomain(AppDomain ad) { // Теперь получение списка всех загруженных сборок, которые // присутствуют в используемом по умолчанию домене приложения. var loadedAssemblies = from a in ad.GetAssemblies() orderby a.GetName().Name select a; Console.WriteLine("***++ Here are the assemblies loaded in {0} *****\n", ad.Fri^ndlyNane); foreach (var a in loadedAssemblies) { Console. WriteLine ("-"• Name: {0}", a.GetName().Name); Console. WriteLine ("--• Version: {0}\n", a . GetName () .Version) ;
600 Часть IV. Программирование с использованием сборок .NET Запустив приложение, можно увидеть, что в используемый по умолчанию домен (CustomAppDomains.exe) будут загружены сборки mscorlib.dll, System.dll, System. Core. dll и СиstomAppDomains . ехе, что связано с кодовой базой С# текущего проекта. В новый домен приложения будет загружена только сборка ms cor lib .dll, которая является одной из сборок .NET, загружаемых CLR-средой в каждый домен приложения. ***** Fun with Custom App Domains ***** ***** Here are the assemblies loaded in CustomAppDomains.exe ***** -> Name: CustomAppDomains -> Version: 1.0.0.0 -> Name: mscorlib -> Version: 4.0.0.0 -> Name: System -> Version: 4.0.0.0 -> Name: System.Core -> Version: 4.0.0.0 ***** Here are the assemblies loaded in SecondAppDomain ***** -> Name: mscorlib -> Version: 4.0.0.0 На заметку! Запустив отладку данного проекта (нажатием <F5>), можно увидеть, что в каждый из доменов приложений будет загружаться много дополнительных сборок, которые необходимы Visual Studio для поддержки процесса отладки. Если просто запустить проект на выполнение (нажатием <Ctrl+F5>), будут выводиться только сборки, загруженные в каждый домен приложения. Тем, кто привык работать с традиционными приложениями Windows, подобное поведение может показаться нелогичным (ведь оба домена приложений имеют доступ к одинаковому набору сборок). Однако следует помнить, что любая сборка загружается в домен приложения, а не напрямую в процесс. Загрузка сборок в специальные домены приложений CLR-среда будет всегда загружать сборки в используемый по умолчанию домен приложения по мере необходимости. Однако в случае создания вручную специальных доменов приложений, эти сборки можно загружать в данные домены с помощью метода AppDomain. Load () . Кроме того, существует метод AppDomain.ExecuteAssembly (), который позволяет загрузить сборку * . ехе и выполнить метод Main (). Чтобы увидеть все это на примере, давайте представим, что необходимо обеспечить загрузку сборки CarLibrary.dll в новый вторичный домен приложения. Скопировав эту библиотеку в папку bin\Debug текущего приложения, можно модифицировать метод MakeNewAppDomain () (не забыв импортировать пространство имен System. 10 для получения доступа к классу FileNotFoundException), как показано ниже: private static void MakeNewAppDomain () { // Создание нового домена приложения в текущем процессе. AppDomain newAD = AppDomain.CreateDomain("SecondAppDomain"); try { // Загрузка в новый домен сборки CarLibrary.dll. newAD.Load("CarLibrary"); } catch (FileNotFoundException ex) { Console. WnteLine (ex.Message); }
Глава 16. Процессы, домены приложений и контексты объектов 601 // Вывод списка всех сборок. ListAllAssembliesInAppDomain(newAD); } На этот раз вывод приложения выглядит следующим образом (обратите внимание на добавление сборки CarLibrary.dll): ***** Fun with Custom App Domains ***** ***** Here are the assemblies loaded in CustomAppDomains.exe ***** -> Name: CustomAppDomains -> Version: 1.0.0.0 -> Name: mscorlib -> Version: 4.0.0.0 -> Name: System -> Version: 4.0.0.0 -> Name: System.Core -> Version: 4.0.0.0 ***** Here are the assemblies loaded in SecondAppDomain ***** -> Name: CarLibrary -> Version: 2.0.0.0 -> Name: mscorlib -> Version: 4.0.0.0 На заметку! Не забывайте, что при выполнении отладки приложения в каждый из доменов приложения будет также загружаться множество дополнительных библиотек. Выгрузка доменов приложений программным образом Важно отметить, что выгружать отдельные сборки .NET в CLR-среде не разрешено. Однако с помощью метода App Domain. Unload () можно производить избирательную выгрузку определенного домена приложения из обслуживающего процесса. В этом случае вместе с доменом приложения будут выгружаться и все содержащиеся в нем сборки. Вспомните, что тип AppDomain имеет событие DomainUnload, которое срабатывает при выгрузке специального домена приложения из содержащего его процесса. Еще одним интересным событием является ProcessExit, которое срабатывает при выгрузке из процесса используемого по умолчанию домена (что, вполне очевидно, влечет за собой завершение самого процесса). Чтобы реализовать программную выгрузку домена newAD из обслуживающего процесса с получением уведомления о его уничтожении, модифицируем метод MakeNewAppDomain (), добавив в него следующую логику: private static void MakeNewAppDomain () { // Создание нового домена приложения в текущем процессе. AppDomain newAD = AppDomain.CreateDomain("SecondAppDomain"); newAD.DomainUnload += (o, s) => { Console.WriteLine("The second app domain has been unloaded!"); }; try { // Загрузка в новый домен сборки CarLibrary.dll. newAD.Load("CarLibrary"); }
602 Часть IV. Программирование с использованием сборок .NET catch (FileNotFoundException ex) { Console.WriteLine(ex.Message); } // Вывод списка всех сборок. ListAllAssembliesInAppDomain(newAD); // Уничтожение домена приложения. АррDomain . Unln=id (newAD) ; } Чтобы включить уведомление при выгрузке используемого по умолчанию домена приложения, в методе Main () необходимо предусмотреть обработку события ProcessEvent этого домена: static void Main(string[] args) { Console.WriteLine ("***** Fun with Custom App Domains *****\nM); // Вывод списка всех сборок, которые были загружены //в используемый по умолчанию домен приложения. AppDomain defaultAD = AppDomain.CurrentDomain; defaultAD.ProcessExit += (о, s) => { Console.WriteLine("Default AD unloaded!"); }; ListAllAssembliesInAppDomain(defaultAD); MakeNewAppDomain(); Console.ReadLine (); } На этом рассмотрение доменов приложений в NET завершено. Напоследок в главе рассмотрим еще один уровень деления, применяемый для группирования объектов в контекстные границы. Исходный код. Проект CustomAppDomains доступен в подкаталоге Chapter 16. Границы контекстов объектов Выше было показано, что домены приложений представляют собой логические разделы внутри процесса, которые используются для обслуживания сборок .NET. Однако на этом дело не заканчивается, поскольку каждый домен приложения может быть дополнительно разделен на многочисленные контексты. Вкратце, контекст в .NET предоставляет возможность закреплять за конкретным объектом "определенное место" в одном домене приложения. На заметку! Следует отметить, что если понимание концепции процессов и доменов приложений является довольно важным, то необходимость работы с контекстами объектов в большинстве приложений .NET возникает очень редко. Материал по контекстам объектов включен в настоящую главу лишь для предоставления более полной картины. Используя контекст, CLR-среда обеспечивает надлежащую и согласованную обработку объектов, которые предъявляют специальные требования к этапу выполнения. Она перехватывает вызовы методов, производимых внутри и за пределами конкретного контекста. Этот уровень перехвата позволяет CLR-среде подстраивать текущий вызов метода так, чтобы он соответствовал контекстным настройкам конкретного объекта. Например, в случае определения на С# класса, требующего автоматическо-
Глава 16. Процессы, домены приложений и контексты объектов 603 го обеспечения безопасности в отношении потоков (за счет использования атрибута [Synchronization]), CLR-среда будет создавать во время его размещения так называемый "синхронизированный контекст". Точно так же, как для каждого процесса создается свой используемый по умолчанию домен приложения, для каждого домена приложения создается применяемый по умолчанию контекст. Этот контекст (иногда еще называемый нулевым из-за того, что он всегда создается в любом домене приложения первым) применяется для группирования вместе объектов .NET, которые не обладают никакими специфическими или уникальными контекстными потребностями. Как не трудно догадаться, подавляющее большинство объектов .NET загружается именно в нулевой контекст. В случае если CLR-среда определяет, что у создаваемого нового объекта имеются специальные потребности, она создает внутри отвечающего за его обслуживание домена приложения новые границы контекста. На рис. 16.3 схематично показаны отношения между процессом, доменами и контекстами. Процесс.NET Домен приложения используемый по умолчанию Контекст по умолчанию Контекст 1 Контекст 2 ' Домен приложения AppDomainl Контекст по умолчанию Контекст 1 Контекст 2 Домен приложения AppDomain2 Контекст по умолчанию Рис. 16.3. Взаимоотношения между процессами, доменами приложений и контекстными границами Контекстно-свободные и контекстно-зависимые типы Объекты .NET, которые не требуют никакого особого контекстного сопровождения, называются контекстно-свободными (context-agile). К таким объектам доступ может быть получен из любого места внутри обслуживающего домена приложения без нарушения их требований времени выполнения. Создание контекстно-свободных объектов не требует приложения никаких особых усилий, поскольку при этом вообще ничего не нужно делать (ни снабжать их какими-то контекстными атрибутами, ни наследовать от базового класса System.ContextBoundObject): // Контекстно-свободный объект загружается в нулевой контекст. class SportsCar { } С другой стороны, объекты, которые действительно требуют выделения отдельного контекста, называются контекстно-зависимыми (context-bound). Они должны обязательно наследоваться от базового класса System.ContextBoundObject. Этот базовый класс отражает тот факт, что интересующий объект может функционировать надлежащим образом только в рамках контекста, в котором он был создан. Из-за роли контекста в .NET должно стать совершенно понятно, что в случае попадания контекстно-зависи-
604 Часть IV. Программирование с использованием сборок .NET мого объекта каким-то образом в несоответствующий контекст, обязательно произойдет что-то плохое, причем в самый неподходящий момент. Помимо наследования от System. ContextBoundObject, контекстно-зависимые объекты должны обязательно сопровождаться специальными атрибутами .NET, которые называются контекстными атрибутами. Все контекстные атрибуты наследуются от базового класса ContextAttribute. Теперь давайте рассмотрим конкретный пример. Определение контекстно-зависимого объекта Предположим, что требуется определить класс (SportsCarTS), который автоматически обеспечивает безопасность в отношении потоков, даже без помещения внутрь реализации его членов жестко закодированной логики синхронизации потоков. Чтобы получить такой класс, необходимо просто унаследовать его от ContextBoundObject и применить к нему атрибут [Synchronization], как показано ниже. using System.Runtime.Remoting.Contexts; // Этот контекстно-зависимый тип будет загружаться только // в синхронизированный (т.е. безопасный к потокам) контекст. [Synchronization] public class SportsCarTS : ContextBoundObject {} Типы, которые сопровождаются атрибутом [Synchronization], всегда загружаются в безопасный к потокам контекст. Зная о специальных контекстных потребностях класса MyThreadSafeObject, теперь представим, какие проблемы будут возникать в случае перемещения соответствующего объекта из синхронизированного контекста в не синхронизированный. Объект сразу же перестанет быть безопасным к потокам и, следовательно, станет кандидатом на массовое повреждение данных, поскольку доступ к нему будут пытаться получить одновременно множество потоков. Наследование SportsCarTS от ContextBoundObject дает гарантию, что CLR-среда никогда не будет пытаться перемещать объекты SportsCarTS за пределы синхронизированного контекста. Инспектирование контекста объекта Хотя необходимость в программном взаимодействии с контекстом возникает в приложениях очень редко, давайте все-таки рассмотрим один показательный пример, как это делается. Создадим новый проект типа Console Application по имени ObjectContextApp и определим в нем контекстно-свободный (SportsCar) и контекстно- зависимый (SportsCarTS) класс: using System; using System.Runtime.Remoting.Contexts; // Для типа Context. using System.Threading; // Для типа Thread. // SportsCar не имеет никаких специальных контекстных // потребностей и будет загружаться в создаваемый по // умолчанию контекст внутри домена приложения. class SportsCar { public SportsCar() { // Получение информации о контексте //и вывод его идентификатора. Context ctx = Thread.CurrentContext; Console.WriteLine("{0} object in context {1}", this.ToString(), ctx.ContextID);
Глава 16. Процессы, домены приложений и контексты объектов 605 foreach(IContextProperty ltfCtxProp in ctx.ContextProperties) Console.WriteLine ("-> Ctx Prop: {0}", itfCtxProp.Name); } // Тип SportsCarTS требует загрузки //в синхронизированный контекст. [Synchronization] class SportsCarTS : ContextBoundObject { public SportsCarTS () { // Получение информации о контексте //и вывод его идентификатора. Context ctx = Thread.CurrentContext; Console.WriteLine ("{0} object in context {1}", this.ToString (), ctx.ContextID); foreach(IContextProperty itfCtxProp in ctx.ContextProperties) Console.WriteLine ("-> Ctx Prop: {0}", itfCtxProp.Name); } } Обратите внимание, что каждый конструктор получает объект Context из текущего потока выполнения посредством статического свойства Thread. CurrentContext. Используя объект Context, легко отображать статистические данные о контексте, такие как назначенный ему идентификатор, а также набор дескрипторов, получаемых через свойство Context. ContextProperties. Это свойство возвращает объект, реализующий интерфейс IContextProperty, который предоставляет доступ к каждому дескриптору через свойство Name. Добавим в метод Main () код для размещения экземпляра каждого из этих классов в памяти. static void Main(string[] args) { Console.WriteLine ("***** Fun with Object Context *****\n"); // При создании этих объектов будет отображаться // информация об их контексте. SportsCar sport = new SportsCar (); Console.WriteLine(); SportsCar sport2 = new SportsCar (); Console.WriteLine(); SportsCarTS synchroSport = new SportsCarTS (); Console.ReadLine (); } По мере создания объектов конструкторы классов будут выводить в окне консоли различные фрагменты касающейся их контекста информации (выводимое свойство LeaseLifeTimeServiceProperty представляет собой низкоуровневую деталь уровня удаленной обработки .NET, поэтому на него можно не обращать внимания). ***** Fun with Object Context ***** ObjectContextApp.SportsCar object in context 0 -> Ctx Prop: LeaseLifeTimeServiceProperty ObjectContextApp.SportsCar object in context 0 -> Ctx Prop: LeaseLifeTimeServicePropertfy ObjectContextApp.SportsCarTS object in context 1 -> Ctx Prop: LeaseLifeTimeServiceProperty -> Ctx Prop: Synchronization
606 Часть IV. Программирование с использованием сборок .NET Из-за того, что класс SportsCar не был снабжен атрибутом контекста, CLR-среда разместила объекты sport и sport2 в контексте с идентификатором 0 (т.е. в контексте по умолчанию). Однако объект SportsCarTS загружен в уникальный контекст (с идентификатором 1), поскольку его контекстно-зависимый класс был снабжен атрибутом [Synchronization]. Исходный код. Проект ObjectContextApp доступен в подкаталоге Chapter 16. Итоговые сведения о процессах, доменах приложений и контекстах К этому моменту должно стало гораздо понятнее, каким образом сборка .NET обслуживается в CLR-среде. Ниже для удобства перечислены ключевые моменты, которые следует вынести из всего предыдущего материала. • В каждом процессе .NET может обслуживаться один и более доменов приложений. В каждом из этих доменов приложения, в свою очередь, может обслуживаться любое количество взаимосвязанных сборок .NET. Все домены приложений могут по отдельности загружаться и выгружаться CLR-средой (или программным образом с помощью класса System.AppDomain). • Любой отдельно взятый домен приложения может включать в себя один и более контекстов. За счет применения контекста CLR-среде удается помещать информацию об "особых потребностях" объекта в логический контейнер и тем самым гарантировать принятие их во внимание на этапе выполнения. Если изложенный в настоящей главе материал показался слишком сложным, не стоит переживать. По большей части исполняющая среда .NET способна самостоятельно разбираться в деталях процессов, доменов приложений и контекстов. С другой стороны, приведенные сведения являются хорошей основой для понимания способов разработки многопоточных приложений в рамках платформы .NET. Резюме Главной задачей настоящей главы было показать, как обслуживаются исполняемые сборки .NET. Давно уже существующее понятие процесса Windows было внутренне изменено и адаптировано под потребности CLR. Любой отдельно взятый процесс (которым на программном уровне можно управлять за счет применения типа System. Diagnostics . Process) теперь включает в себя один или более доменов приложений, которые представляют собой изолированные и независимые границы внутри данного процесса. Кроме того, вы узнали, что в каждом процессе может обслуживаться несколько доменов приложений, в каждом из которых, в свою очередь, может обслуживаться и выполнятся любое количество связанных между собой сборок. Более того, в каждом домене приложения также может содержаться любое количество контекстов. Благодаря применению такого дополнительного уровня изоляции типов, CLR-среда обеспечивает надлежащую обработку объектов с особыми потребностями на этапе выполнения.
ГЛАВА 17 Язык CIL и роль динамических сборок При разработке полнофункционального приложения .NET наверняка будет применяться язык С# (или подобный управляемый язык вроде Visual Basic), из-за присущей ему продуктивности и удобства в использовании. Как рассказывалось в самой первой главе, роль управляемого компилятора состоит в преобразовании файлов кода * . cs в инструкции на языке CIL, метаданные типов и манифест сборки. CIL является полнофункциональным языком программирования .NET, который обладает собственным синтаксисом, семантикой и компилятором (ilasm. ехе). В настоящей главе предлагается краткий экскурс по этому родному языку .NET. Здесь будет показано, в чем состоят различия между директивой, атрибутом и кодом операции в CIL, а также роль прямого и обратного проектирования сборки .NET и различных инструментов программирования на CIL. Также будет показано, как определять пространства имен, типы и члены с использованием грамматики CIL. В завершение главы рассматривается роль пространства имен System.Ref lection .Emit и реализация (с помощью инструкций CIL) динамического создания сборок во время выполнения. Разумеется, необходимость иметь дело непосредственно с кодом на CIL в повседневной работе будет возникать только у очень немногих программистов. Глава начинается с описания причин, по которым изучение синтаксиса и семантики этого низкоуровневого языка .NET может оказаться полезным. Причины для изучения грамматики языка CIL Язык CIL является самым настоящим "родным" языком для платформы .NET. При создании .NET-сборки с помощью того или иного языка (С#, VB, COBOL.NET и т.д.) соответствующий компилятор всегда преобразует исходный код на этом языке в код на CIL. Как и в любом другом языке программирования, в CIL поддерживается множество связанных со структурированием и реализацией лексем. Поскольку CIL представляет собой просто еще один язык программирования .NET, не должен удивлять тот факт, что сборки .NET можно создавать непосредственно на CIL и компилировать их с помощью компилятора ilasm. ехе, который входит в состав .NET Framework 4.0 SDK. Хотя желание построить все приложение .NET непосредственно на CIL действительно будет возникать у очень немногих, все равно этот язык является чрезвычайно интересным объектом для изучения. Хорошие знания грамматики языка CIL позволяют совершенствовать приемы разработки .NET-приложений. Разработчики, разбирающиеся в CIL, способны делать следующее.
608 Часть IV. Программирование с использованием сборок .NET • Точно знать, на какие лексемы в CIL отображаются ключевые слова из различных языков программирования .NET. • Дизассемблировать существующие .NET-сборки, редактировать лежащий в их основе CIL-код и заново компилировать обновленную кодовую базу в новый двоичный файл .NET. Например, некоторые сценарии могут требовать внесения изменений в CIL-код для взаимодействия с расширенными средствами СОМ. • Строить динамические сборки с использованием пространства имен System. Reflection .Emit. Этот API-интерфейс позволяет генерировать сборку .NET в памяти, которая впоследствии может быть сохранена на диске. • Использовать такие возможности CTS (Common Type System — общая система типов), которые в управляемых языках более высокого уровня не поддерживаются, а на уровне CIL действительно доступны. На самом деле CIL является единственным языком .NET, который позволяет получать доступ ко всем возможностям CTS. Например, используя чистый код CIL, можно создавать определения глобальных членов и полей (чего в С# делать не разрешено). Следует еще раз подчеркнуть, что овладеть навыками работы с С# и библиотеками базовых классов .NET можно и без изучения деталей CIL-кода. Во многих отношениях знание языка CIL для программиста, работающего с .NET, аналогично знанию языка ассемблера для программиста, работающего на С (C++). Те, кто разбирается в низкоуровневых деталях, способны создавать более совершенные решения для существующих задач и лучше понимают, как работает базовая среда программирования (и выполнения). Поэтому всем, кому интересно, предлагаем приступить к изучению основных аспектов CIL. На заметку! Важно отметить, что всестороннее и исчерпывающее описание синтаксиса и семантики CIL в настоящей главе не предлагается. Полная информация по данной теме приведена в официальной спецификации ЕСМА (ecma-335.pdf), доступной на веб-сайте ЕСМА International по адресу http://www.ecma-international.org. Директивы, атрибуты и коды операций в CIL В начале изучения любого низкоуровневого языка вроде CIL обязательно встречаются новые (и часто пугающие) названия для хорошо знакомых понятий. Например, на текущий момент в данной книге приведенный ниже список элементов: {new, public, this, base, get, set, explicit, unsafe, enum, operator, partial} почти наверняка покажется набором ключевых слов языка С# (и это правильно). Однако внимательней присмотревшись к элементам в этом списке, можно заметить, что хотя каждый из них действительно представляет собой ключевое слово С#, их семантика радикально отличается. Например, ключевое слово enum позволяет определять производный от System. Enum тип, а ключевые слова this и base — ссылаться, соответственно, на текущий объект и его родительский класс. Ключевое слово unsafe используется для создания блока кода, который не должен подвергаться непосредственному мониторингу со стороны CLR-среды, а ключевое слово operator —для создания скрытого (имеющего специальное имя) метода, который должен вызываться при применении какой-то операции С# (например, знака "плюс"). В отличие от такого высокоуровневого языка, как С#, в CIL не поставляется простой общий набор ключевых слов. Вместо этого набор лексем, распознаваемых компилятором CIL, семантически разделен на три следующих основных категории:
Глава 17. Язык CIL и роль динамических сборок 609 • директивы CIL; • атрибуты CIL; • коды операций CIL. Лексемы CIL каждой из этих категорий представляются с помощью определенного синтаксиса и комбинируются для получения полноценной .NET-сборки. Роль директив CIL Прежде всего, в CIL имеется ряд хорошо известных лексем, которые применяются для описания общей структуры .NET-сборки. Эти лексемы называются директивами. Директивы CIL позволяют информировать компилятор CIL о том, каким образом должны определяться пространства имен, типы и члены, входящие в состав сборки. Синтаксически директивы представляются с использованием префикса в виде точки (.), например, .namespace, .class, .publickeytoken, .override, .method, .assembly и т.д. Следовательно, если в файле с расширением * .il (принятое по соглашению расширение для файлов CIL-кода) есть одна директива .namespace и три директивы .class, компилятор CIL будет генерировать сборку, в которой определено единственное пространство имен, содержащее три типа классов .NET. Роль атрибутов CIL Во многих случаях сами по себе директивы CIL оказываются недостаточно описательными для того, чтобы полностью отражать определение того или иного типа или члена типа .NET. В таких случаях они могут сопровождаться различными атрибутами CIL, которые уточняют то, каким образом они должны обрабатываться. Например, директива .class может сопровождаться атрибутами public (уточняющим видимость типа), extends (явно указывающим базовый класс типа) и implements (позволяющим перечислить интерфейсы, поддерживаемые данным типом). На заметку! Не путайте совершенно разные понятия "атрибут CIL" и "атрибут .NET" (см. главу 15). Роль кодов операций CIL После определения сборки, пространства имен и набора типов на CIL с помощью различных директив и соответствующих атрибутов, последнее, что останется сделать — это предоставить для каждого из типов логику реализации. Для решения этой задачи в CIL поддерживаются так называемые коды операций (operation codes — opcodes). Как и в других низкоуровневых языках программирования, коды операций в CIL обычно имеют непонятный и нечитабельный вид. Например, для загрузки в память переменной string в CIL должен применяться код операции не с удобным для восприятия именем вроде LoadString, а со сложным для произношения именем ldstr. Некоторые коды операций в CIL отображаются вполне естественным образом на соответствующие аналоги в С# (например, box, unbox, throw и sizeof). Как будет показано далее в главе, коды операций в CIL всегда применяются только в рамках реализации членов и, в отличие от директив, никогда не сопровождаются префиксом в виде точки. Разница между кодами операций и их мнемоническими эквивалентами в CIL Как было только что сказано, коды операций вроде ldstr применяются для реализации членов конкретного типа. Однако на самом деле лексемы, подобные ldstr, явля-
610 Часть IV. Программирование с использованием сборок .NET ются мнемоническими эквивалентами (mnemonics) самих двоичных кодов операций в CIL. Чтобы увидеть, в чем состоит отличие, давайте представим, что был написан следующий метод на С#: static int Add(int x, int у) { return x + у; } Код операции сложения двух чисел в CIL выглядит как 0X58. Аналогично, кодом операции вычитания является 0X59, а кодом операции размещения нового объекта в управляемой куче — 0X7 3. Получается, что "код CIL', который обрабатывается JIT-компилятором, фактически принимает вид просто больших объектов двоичных данных. К счастью, для каждого двоичного кода операции в CIL существует соответствующий мнемонический эквивалент. Например, вместо кода 0X58 может использоваться его мнемонический эквивалент add, вместо кода 0X59 — эквивалент sub, а вместо 0X73 — newobj. Декомпиляторы CIL вроде ildasm.exe будут всегда преобразовывать все присутствующие в сборке двоичные коды операций в соответствующие им мнемонические эквиваленты. Например, ниже показано, как ildasm.exe преобразует приведенный выше метод Add () на С# в CIL: .method privatehidebysig static int32 Add(int32 x, int32 y) cil managed { // Code size 9 @x9) // Размер кода 9 @x9) .maxstack 2 .locals init ([0] int32 CS$1$0000) IL_0000 IL_0001 IL 0002 IL_0003 IL_0004 fIL_0005 IL_0007 IL 0008 nop ldarg.O ldarg.1 add stloc.O br.s IL_0007 ldloc.O ret Разумеется, у тех, кто не занимается разработкой низкоуровневого программного обеспечения .NET (такого как специальные управляемые компиляторы), никогда не возникает необходимость иметь дело с числовыми двоичными кодами операций CIL. Обычно, когда программисты приложений .NET говорят о "кодах операций CILT, они подразумевают их удобные для восприятия, строковые мнемонические эквиваленты, а не базовые числовые значения (так делается и в книге). Помещение и извлечение данных из стека в CIL В языках .NET более высокого уровня (вроде С#) низкоуровневые детали CIL обычно насколько возможно скрываются из виду. Одной из особенно хорошо скрываемых деталей является тот факт, что CIL на самом деле является языком программирования, основанным на использовании стека. При рассмотрении связанных с коллекциями пространств имен (в главе 10) уже рассказывалось о том, что существует класс Stack<T>, который может применяться для помещения значения в стек, а также извлечения самого верхнего значения из стека для последующего использования. Конечно, разработчики на CIL не используют в буквальном смысле объект типа Stack<T> для загрузки и выгрузки вычисляемых значений, но применяют стиль помещения и извлечения из стека.
Глава 17. Язык CIL и роль динамических сборок 611 Формально сущность, используемая для хранения набора вычисляемых значений, называется виртуальным стеком выполнения (virtual execution stack). Далее в главе будет показано, что в CIL поддерживается набор таких кодов операций, которые могут применяться для помещения значения в стек; процесс помещения значения в стек называется загрузкой. Также в CIL поддерживаются дополнительные коды операций, служащих для перемещения самого верхнего значения из стека в память (например, в локальную переменную); процесс перемещения значения из стека в память называется сохранением. В мире CIL получать доступ к элементам данных, в том числе к определенным локально переменным, входным аргументам методов и данным полей типа, не разрешено. Вместо этого требуется явно загружать их в стек и затем извлекать их оттуда для последующего использования (очень важно запомнить это, поскольку именно оно позволит понять, почему некоторые блоки кода CIL выглядят несколько громоздко). На заметку! Вспомните, что CIL-код не выполняется напрямую, а компилируется по требованию. Во время компиляции CIL-кода многие излишние детали реализации оптимизируются. Более того, если включена опция оптимизации кода для текущего проекта (на вкладке Build (Сборка) окна свойств проекта в Visual Studio), компилятор будет еще и удалять излишние детали CIL Чтобы увидеть, каким образом в CIL функционирует основанная на использовании стека модель обработки, давайте создадим на С# простой метод PrintMessage (), не принимающий аргументов и возвращающий void, и предусмотрим в его реализации вывод в стандартный поток значения локальной переменной: public void PrintMessage () { string myMessage = "Hello."; Console.WriteLine(myMessage); } CIL-код, в который компилятор С# преобразовал этот метод, содержит в методе PrintMessage () директиву . locals, определяющую ячейку для хранения локальной переменной. Локальная строка затем загружается и сохраняется в этой локальной переменной с использованием кодов операций ldstr (загрузка строки) и stloc. О (сохранение текущего значения в локальной переменной в ячейке 0). После этого значение (по индексу 0) загружается в память с помощью кода операции ldloc. О (загрузка локального аргумента с индексом 0) для использования в вызове метода System. Console .WriteLine () (представленного кодом операции call). И, наконец, код операции ret обеспечивает возврат из функции. Ниже показан полный CIL-код, который компилятор С# генерирует для метода PrintMessage () (с соответствующими комментариями). .method public hidebysig instance void PrintMessage () cil managed { .maxstack 1 // Определение локальной строковой переменной (с индексом 0) . .locals init ([0] string myMessage) // Загрузка в стек строки со значением Hello. ldstr "Hello." // Сохранение строкового значения из стека в локальной переменной. stloc.О // Загрузка значения с индексом 0. ldloc.О
612 Часть IV. Программирование с использованием сборок .NET // Вызов метода с текущим значением. call void [mscorlib] System.Console : :WnteLine (string) ret } На заметку! Как не трудно заметить, в CIL для комментариев поддерживается синтаксис в виде двойной косой черты (а также синтаксис в виде /* . . . */). Как и компилятор С#, компилятор CIL полностью игнорирует все комментарии. Теперь, когда известно, что собой в общем представляют директивы, атрибуты и коды операций в CIL, давайте приступим непосредственно к программированию на CIL, начав с рассмотрения методики двунаправленного проектирования. Двунаправленное проектирование В главе 1 было показано, как использовать утилиту ildasm.exe для просмотра генерируемого компилятором С# кода CIL. Эта утилита также позволяет сбрасывать CIL- код, содержащийся внутри загруженной сборки, во внешний файл. Полученный подобным образом CIL-код можно легко редактировать и затем компилировать с помощью компилятора CIL (ilasm.exe). На заметку! Вспомните, что для просмотра CIL-кода любой отдельно взятой сборки, а также его преобразования в примерную кодовую базу на С# также можно применять утилиту reflector.exe. Формально такой подход называется двунаправленным проектированием (round- trip engineering) и может оказываться полезным в перечисленных ниже ситуациях. • Необходимость изменить сборку, исходный код которой больше не доступен. • Необходимость изменить неэффективный (или предельно некорректный) CIL-код, полученный в результате использования далекого от идеала компилятора языка .NET. • Необходимость при создании сборок, взаимодействующих с СОМ, вернуть некоторые из СОМ-атрибутов на языке IDL (Interface Definition Language — язык описания интерфейсов), которые были утрачены в процессе преобразования (вроде СОМ-атрибута [helpstring]). Чтобы попробовать процесс двунаправленного проектирования на практике, создадим в простом текстовом редакторе новый файл кода С# (HelloProgram.cs) и определим в нем показанный ниже класс (при желании можно создать проект Console Application в Visual Studio 2010 и удалить из него файл Assemblylnfo. cs, уменьшив объем генерируемого CIL-кода). // Простое консольное приложение на языке С#. using System; // Обратите внимание, что для упрощения генерируемого CIL-кода // класс не помещается ни в какое пространство имен. class Program { static void Main(string [ ] args) { Console.WriteLine("Hello CIL code1"); Console.ReadLine (); } }
Глава 17. Язык CIL и роль динамических сборок 613 Сохраним этот файл в удобном месте (например, в каталоге С: \RoundTrip) и скомпилируем программу с помощью esc . ехе: esc HelloProgram.cs Откроем полученный в результате файл HelloProgram.exe в утилите ildasm.exe и выберем в меню File (Файл) пункт Dump (Сбросить), чтобы сохранить CIL-код в новом файле с расширением * . il (HelloProgram.il) в той же папке, где находится скомпилированная сборка (не изменяя значений, предлагаемых по умолчанию в диалоговом окне сохранения). На заметку! При сбросе содержимого сборки в файл утилита ildasm.exe генерирует и файл * . res. Файлы подобного рода можно спокойно игнорировать (и удалять), поскольку они использоваться не будут. Теперь можно просмотреть этот файл в любом текстовом редакторе. Ниже показано его содержимое (для удобства представления формат был немного изменен, а также добавлены соответствующие комментарии). // Внешние сборки, на которые имеются ссылки в текущей сборке. .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 EO 89 ) .ver 4:0:0:0 } // Текущая сборка. .assembly HelloProgram { /**** Данные TargetFrameworkAttribute удалены для ясности! ****/ .hash algorithm 0x00008004 .ver 0:0:0:0 } .module HelloProgram.exe .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 .corflags 0x00000003 // Определение класса Program. .class private auto ansi beforefleldinit Program extends [mscorlib]System.Object { .method private hidebysig static void Main (string[] args) cil managed { // Назначение данного метода входной точкой //в этой исполняемой сборке. .entrypoint .maxstack 8 IL_0000: nop IL_0001: ldstr "Hello CIL code!" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: call string [mscorlib]System.Console::ReadLine() IL_0011: pop IL 0012: ret
614 Часть IV. Программирование с использованием сборок .NET // Конструктор по умолчанию. .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 IL_0000 IL_0001 IL 0006 ldarg.O call instance void [mscorlib]System.Object::.ctor() ret Прежде всего, обратите внимание, что файл * . il начинается с объявления всех внешних сборок, на которые ссылается текущая скомпилированная сборка. В данном случае присутствует только одна лексема .assembly extern, которая указывает на всегда добавляемую внешнюю сборку mscorlib. dll. Разумеется, если бы в библиотеке классов использовались типы из каких-то других сборок, здесь бы присутствовали и другие директивы .assembly extern. Далее идет формальное определение самой сборки HelloProgram.exe, с назначенным по умолчанию номером версии 0.0.0.0 (поскольку никакой номер версии с помощью атрибута [Assembly Vers ion] не был указан). Это определение сопровождается несколькими директивами CIL (.module, . imagebase и т.д.). После списка внешних сборок и определения текущей сборки идет определение типа Program. Обратите внимание на наличие внутри директивы . class различных атрибутов (многие из которых необязательны), таких как extends, который указывает базовый класс для типа: .class private auto ansi beforefleldinit Program extends [mscorlib]System.Object { ... } Большую часть остального CIL-кода занимает описание реализации конструктора, который должен применяться для данного класса по умолчанию, и метода Main (); оба они определяются (частично) с помощью директивы .method. После определения этих членов с помощью соответствующих директив и атрибутов, они реализуются с помощью различных кодов операций. Очень важно запомнить, что при взаимодействии с типами .NET (такими как System. Console) в CIL должно всегда использоваться полностью квалифицированное имя типа. Более того, к этому полностью квалифицированному имени необходимо всегда в качестве префикса присоединять дружественное имя сборки, в которой находится его определение (в квадратных скобках). Например, рассмотрим следующую реализацию метода Main () в CIL: .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 8 IL_0000 IL_0001 IL_0006 IL_000b IL_000c IL_0011 IL 0012 nop ldstr "Hello CIL code1" call void [mscorlib] System.Console: :WnteLine (string) nop call string [mscorlib] System. Console : .-ReadLine () pop ret При реализации конструктора по умолчанию в CIL-коде используется еще одна инструкция, связанная с загрузкой — ldarg. 0. В данном случае значение, загружаемое
Глава 17. Язык CIL и роль динамических сборок 615 в стек, представляет собой не специальную указанную нами переменную, а ссылку на текущий объект (подробнее об этом — чуть позже). Обратите внимание, что в конструкторе по умолчанию явным образом производится вызов конструктора базового класса, которым в данном случае является хорошо знакомый класс System.Object. .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 IL_0000: ldarg.O IL_0001: call instance void [mscorlib]System.Object::.ctor() IL 0006: ret } Роль меток в коде CIL Наверняка вам бросилось в глаза, что каждая строка в коде реализации сопровождается префиксом вида ILXXX: (IL0000:, IL0001: и т.д.). Префиксы такого вида называются метками кода и могут иметь произвольный формат (главное, чтобы они не дублировались в рамках одного и того же члена). При сбрасывании содержимого сборки в файл утилита ildasm. ехе автоматически генерирует метки кода в формате ILXXX: в соответствии с принятым для них соглашением об именовании. При желании их легко изменить и сделать более описательными: .method private hidebysig static void Main(string [ ] args) cil managed { .entrypoint .maxstack 8 Nothing_l: nop Load_String: ldstr "Hello CIL code!" PrintToConsole: call void [mscorlib]System.Console::WriteLine(string) Nothing_2: nop WaitFor_KeyPress : call string [mscorlib]System.Console::ReadLine () RemoveValueFromStack: pop Leave_Function: ret } В действительности метки кода по большей части совершенно не обязательны. Единственный случай, когда их действительно требуется применять — при создании CIL-кода с различными конструкциями ветвления или циклов, поскольку они позволяют указать, куда должен быть направлен поток логики. В текущем примере можно вообще удалить все эти автоматически сгенерированные метки безо всяких последствий: .method private hidebysig static void Main(string [ ] args) cil managed { .entrypoint .maxstack 8 nop ldstr "Hello CIL code!" call void [mscorlib]System.Console::WriteLine(string) nop call string [mscorlib]System.Console::ReadLine() pop ret }
616 Часть IV. Программирование с использованием сборок .NET Взаимодействие с CIL: модификация файла *. il Теперь, когда стало более понятно, как изнутри выглядит типичный файл CIL-кода, давайте завершим эксперимент с двунаправленным проектированием. Для этого предположим, что требуется обновить CIL-код в существующем файле * .il, следующим образом: • добавить ссылку на сборку System. Windows . Forms . dll; • загрузить локальную строку в методе Main (); • вызвать метод System. Windows . Forms .MessageBox. Show (), принимающий локальную строковую переменную в качестве аргумента. В первую очередь понадобится добавить новую директиву .assembly (с атрибутом extern), указывающую на то, что нашей сборке требуется сборка System. Windows . Forms .dll. Для этого достаточно вставить в файл * . il сразу же после ссылки на внешнюю сборку ms со г lib следующий код: .assembly extern System.Windows.Forms { .publickeytoken = (B7 7A 5C 56 19 34 EO 89) .ver 4:0:0:0 } Значение, присваиваемое директиве .ver, зависит о установленной версия платформы .NET. Здесь используемая сборка System.Windows.Forms.dll относится к версии 4.0.0.0 и имеет значение открытого ключа В77А5С5 61934Е08 9. Открыв кэш GAC (см. главу 14) и отыскав там версию сборки System. Windows . Forms . dll, можно подставить нужный номер версии и значение открытого ключа. Теперь необходимо изменить текущую реализацию метода Main (). Для этого следует найти этот метод внутри файла * . i 1 и удалить текущий код его реализации (оставив только директивы .maxstackH .entrypoint): .method private hidebysig static void Main(string [ ] args) cil managed { .entrypoint .maxstack 8 // Здесь следует написать новый CIL-код. } Требуется поместить новую переменную string в стек и вызвать метод MessageBox. Show () (вместо Console. WriteLine ()). Вспомните, что при указании имени внешнего типа должно использоваться его полностью квалифицированное имя (вместе с дружественным именем сборки). Обратите внимание, что в CIL при вызове каждого метода необходимо обязательно полностью указывать возвращаемый тип. В результате метод Main () должен приобрести следующий вид: .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 8 ldstr "CIL is way cool" call valuetype [System.Windows.Forms] System.Windows.Forms.DialogResult [System.Windows.Forms] System.Windows.Forms.MessageBox::Show(string) pop ret }
Глава 17. Язык CIL и роль динамических сборок 617 По сути, CIL-код модифицирован так, что он соответствует такому определению класса С#: public class Program { static void Main(string [ ] args) { System.Windows.Forms.MessageBox.Show("CIL is way cool"); } } Компиляция CIL-кода с помощью ilasm. exe После сохранения измененного файла * . i 1 можно приступать к компиляции новой сборки .NET за счет применения утилиты ilasm.exe (компилятора CIL). Эта утилита поддерживает множество опций командной строки (для их просмотра укажите при запуске опцию -?), наиболее интересные из которых описаны в табл. 17.1. Таблица 17.1. Опции командной строки, наиболее часто применяемые при работе с утилитой ilasm. exe Опция Описание /debug Включение отладочной информации (такую как имена локальных переменных и аргументов, а также номера строк) /dll Генерация в качестве вывода файла * . dll /exe Генерация в качестве вывода файлТ * . ехе. Используется по умолчанию, поэтому может быть опущена /key Компиляция сборки со строгим именем с использованием заданного файла * . snk /output Имя и расширение выходного файла. Если флаг /output не указан, результирующее имя файла (без расширения) совпадает с именем первого исходного файла Чтобы скомпилировать измененный файл HelloProgram.il в новую .NET-сборку *.ехе, необходимо ввести в окне командной строки Visual Studio 2010 следующую команду: ilasm /exe HelloProgram.il /output=NewAssembly.ехе Если все прошло успешно, на экране должен быть следующий отчет: Microsoft (R) .NET Framework IL Assembler. Version 4.0.21006.1 Copyright (c) Microsoft Corporation. All rights reserved. Assembling 'HelloProgram.il' to EXE --> 'NewAssembly.exe' Source file is UTF-8 Assembled method Program::Main Assembled method Program::.ctor Creating PE file Emitting classes: Class 1: Program Emitting fields and methods: Global Class 1 Methods: 2; Emitting events and properties: Global Class 1 Writing PE file Operation completed successfully
618 Часть IV. Программирование с использованием сборок .NET После этого новое приложение можно запустить. Вполне очевидно, что теперь сообщение будет отображаться не в окне консоли, а в отдельном окне сообщения. Хотя вывод этого простого примера не является столь уж впечатлякэщим, один из практических способов применения двунаправленного программирования на языке CIL он действительно иллюстрирует. Создание CIL-кода с помощью SharpDevelop В главе 2 кратко рассказывалось о такой распространяемой бесплатно IDE-среде, как SharpDevelop (http: //www.sharpdevelop.com). Выбрав в меню File (Файл) пункт New Solution (Новое решение)) в этой среде, вдобавок к шаблонам проектов на языках С# и VB можно увидеть также и проект CIL (рис. 17.1). New Project Categories: ;-"Ш BOO ч Q С» !■ ;_>■ ILAam _j Python [ CM Setup ■ £i SharpDevelop B-fli VBNet A project that creates a command Ine apptcation. Name FunWthCJLCode Location C:\L^NAi<^Trod^VDo(xrr^s\ihaipDe^ Prqjed» SoJutton Name: f^vW^LCode Create directory for solution Project wl be created at CA...\AndrewTroetoenMDocurnert3\Sh.wpDevelop ; Create II, Cancel | Рис. 17.1. Шаблон проекта CIL в IDE-среде SharpDevelop Хотя поддержка средства IntelliSense для проектов CIL в SharpDevelop не предусмотрена, лексемы CIL здесь все равно снабжаются цветовой кодировкой, а приложение можно компилировать и запускать непосредственно в IDE-среде (вместо запуска утилиты ilasm.exe в командной строке). При создании проекта CIL в SharpDevelop первоначально предоставляется файл * . il, показанный на рис. 17.2. / MamCtaseJ |_ 11 3 1 ■ !~ 9 1С 11 12 ;... 3 4 1 • 17 < : .assembly HelloWorld { } .namespace FunWithCILCode { -class private auto ansi beforefieldinit MainClass { .method public hidebysig static void Main(stringГ1 args) cil managed { •entrypoint .maxstack 1 ldstr "Hello World!" call void [mscorlibjSystem.Console: :WriteLinei,'string) ret } } > _^_ Z» -...- ит -^.- I . - X *i ■ 4 tit : , ► Рис. 17.2. Редактор CIL-кода в IDE-среде SharpDevelop
Глава 17. Язык CIL и роль динамических сборок 619 При проработке примеров в остальной части главы рекомендуется применять для создания CIL-кода среду SharpDevelop. Помимо выделения элементов кода цветом эта IDE-среда позволяет быстро обнаруживать опечатки в коде посредством специального окна Errors (Ошибки). На заметку! В IDE-среде MonoDevelop, которая поставляется бесплатно и предназначена для разработки приложений .NET на платформе Mono, также поддерживается шаблон проекта CIL. Дополнительные сведения о MonoDevelop ищите в приложении Б. Роль peverifу.ехе При создании либо изменении сборок за счет редактирования CIL-кода рекомендуется проверять, правильно ли оформлен скомпилированный двоичный образ согласно требованиям .NET, с помощью утилиты командной строки peverify.exe: peverifу NewAssembly.ехе Эта утилита производит проверку корректности кодов операций CIL внутри указанной сборки. Например, в рамках CIL-кода стек вычислений должен всегда опустошаться перед выходом из функции. Даже если забыть извлечь оставшиеся значения, компилятор ilasm. ехе сгенерирует сборку (поскольку компиляторы заботит только синтаксис]. С другой стороны, утилита peverif у. ехе контролирует правильность семантики, и потому если стек не был очищен перед выходом из функции, она обязательно уведомит об этом, тем самым позволяя выявить проблему до запуска кодовой базы. Исходный код. Пример RoundTrip доступен в подкаталоге Chapter 17. Использование директив и атрибутов в CIL Теперь, когда было показано, как применять утилиты ildasm.exe и ilasm.exe для выполнения двунаправленного проектирования, можно переходить к более детальному изучению синтаксиса и семантики CIL. В следующих подразделах будет поэтапно рассмотрен весь процесс создания специального пространства имен с набором типов. Для простоты пока что логика реализации членов к этим типам не добавляется. Разобравшись с созданием простых типов, можно переключить внимание на процесс определения "реальных" членов с помощью кодов операций CIL. Добавление ссылок на внешние сборки в CIL Создадим с помощью предпочитаемого редактора новый файл по имени CilTypes . il. Первым шагом в любом CIL-проекте является перечисление внешних сборок, которые будут использоваться в текущей сборке. В рассматриваемом примере применяются только типы из сборки mscorlib.dll. Следовательно, потребуется добавить ссылку только на эту сборку. Для этого необходимо указать в новом файле директиву . assembly с уточняющим атрибутом external. При добавлении ссылок на сборки, обладающие строгими именами, наподобие mscorlib.dll, должны также указываться директивы .publickeytoken и .ver. .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 EO 89 ) .ver 4:0:0:0 }
620 Часть IV. Программирование с использованием сборок .NET На заметку! Строго говоря, явно добавлять ссылку на сборку ms cor lib. dll не обязательно, поскольку ilasm.exe добавит ее автоматически. Для всех остальных внешних библиотек .NET, требуемых в проекте CIL, обязательно должны быть предусмотрены соответствующие директивы .assembly extern. Определение текущей сборки в CIL Следующий шаг состоит в определении интересующей сборки с использованием директивы . assembly. В самом простом варианте при определении сборки можно указывать только дружественное имя соответствующего двоичного файла: // Наша сборка. .assembly CILTypes { } Хотя это вполне допустимое определение новой сборки .NET, обычно внутри такого объявления размещаются дополнительные директивы. В рассматриваемом примере определение сборки необходимо снабдить номером версии 1.0.0.0 с помощью директивы .ver (обратите внимание, что числа в номере версии разделяются двоеточиями, а не точками, как в С#): // Наша сборка. .assembly CILTypes { .ver 1:0:0:0 } Из-за того, что CILTypes представляет собой однофайловую сборку, в конце определения нужно добавить одну директиву .module и указать в ней официальное имя двоичного файла .NET — CILTypes . dll: .assembly CILTypes { .ver 1:0:0:0 } // Модуль нашей однофайловой сборки, .module CILTypes.dll Помимо . assembly и .module, существуют и другие СIL-директивы, которые позволяют дополнительно уточнять обитую структуру создаваемого двоичного файла .NET. В табл. 17.2 перечислены некоторые наиболее часто применяемые из них. Таблица 17.2. Дополнительные директивы, применяемые для определения сборки Директива Описание . mresources Если сборка должна использовать внутренние ресурсы (такие как растровые изображения или таблицы строк), эта директива позволяет указать имя файла, в котором содержатся ресурсы, подлежащие включению в сборку . subsystem Эта CIL-директива служит для указания предпочитаемого пользовательского интерфейса, внутри которого должна выполняться сборка. Например, значение 2 указывает, что сборка должна выполняться в приложении с графическим пользовательским интерфейсом, а значение 3 — в консольном приложении Определение пространств имен в CIL После определения внешнего вида и поведения сборки (и требуемых внешних ссылок) можно переходить к созданию пространства имен .NET (MyNamespace), используя директиву .namespace:
Глава 17. Язык CIL и роль динамических сборок 621 // Наша сборка имеет единственное пространство имен. .namespace MyNamespace {} Как и в С#, в языке CIL можно создавать корневые пространства имен. Хотя корневое пространство имен в рассматриваемом примере, в принципе, не нужно, ради интереса посмотрим, как определить корневое пространство имен под названием MyCompany: .namespace MyCompany { .namespace MyNamespace {} } Подобно С#, в CIL можно определять вложенные пространства имен: // Определение вложенного пространства имен. .namespace MyCompany.MyNamespace{} Определение типов классов в CIL Пустые пространства имен не особо полезны, поэтому давайте посмотрим, как в CIL определяются типы классов. Для определения нового типа класса в CIL применяется директива .class. Эта простая директива может сопровождаться многочисленными дополнительными атрибутами, позволяющими уточнить природу типа. Добавим в рассматриваемое пространство имен общедоступный класс по имени MyBaseClass. Как и в С#, если базовый класс явно не указан, тип будет автоматически наследоваться от System.Object: .namespace MyNamespace { // Предполагается, что базовым классом является System.Object. .class public MyBaseClass {} } При создании типа класса, унаследованного не от System.Object, применяется атрибут extends. Для ссылки на тип, определенный внутри той же самой сборки, в CIL должно использоваться полностью квалифицированное имя (хотя если базовый тип находится в пределах той же сборки, допускается не указывать префикс, представляющий дружественное имя сборки). Таким образом, попытка расширить MyBaseClass так, как показано ниже, приведет к ошибке на этапе компиляции: // Этот код не компилируется' .namespace MyNamespace { .class public MyBaseClass {} .class public MyDerivedClass extends MyBaseClass {} } Чтобы правильно определить родительский класс для MyDerivedClass, необходимо указать полностью квалифицированное имя MyBaseClass: // Правильный код. .namespace MyNamespace { .class public MyBaseClass {} .class public MyDerivedClass extends MyNamespace.MyBaseClass {} } Помимо атрибутов public и extends, определение класса CIL может иметь множество дополнительных спецификаторов, уточняющих видимость типа, компоновку полей и т.п.
622 Часть IV. Программирование с использованием сборок .NET В табл. 17.3 описаны некоторые атрибуты, используемые вместе с директивой .class. Таблица 17.3. Некоторые атрибуты, которые могут использоваться вместе с директивой . class Атрибут Описание public, private, Эти атрибуты применяются для указания степени видимости nested assembly, типа. Как не трудно заметить, в CIL предлагается много других nested f amandassem, возможностей помимо тех, что доступны в С#. За дополнитель- nested family, ными сведениями обращайтесь в документ ЕСМА 335 nested famorassem, nested public, nested private abstract, Эти два атрибута могут быть присоединены к директиве . class sealed для определения, соответственно, абстрактного или запечатанного класса auto, Эти атрибуты позволяют указать CLR-среде, как данные полей sequential, должны размещаться в памяти. Для типов классов подходящим explicit является флаг auto, используемый по умолчанию. Его изменение может быть полезно в случае использования P/lnvoke для обращения к неуправляемому коду на С extends, Эти атрибуты позволяют определять базовый класс для типа implements (extends) и реализовать для него интерфейс (implements) Определение и реализация интерфейсов в CIL Как ни странно, но типы интерфейсов в CIL тоже определяются с использованием директивы .class. Однако за счет ее сопровождения атрибутом interface тип реализуется как один из типов интерфейсов CTS. После определения интерфейс может привязываться к какому-нибудь типу класса или структуры с применением атрибута implements. .namespace MyNamespace { // Определение интерфейса. .class public interface IMylnterface {} //Простой базовый класс .class public MyBaseClass {} // Теперь MyDerivedClass реализует интерфейс // IMylnterface и расширяет класс MyBaseCLass. .class public MyDerivedClass extends MyNamespace.MyBaseClass implements MyNamespace.IMylnterface {} На заметку! Конструкция extends должна обязательно предшествовать конструкции implements. Кроме того, в конструкции implements может быть предоставлен разделенный запятыми список интерфейсов. Как рассказывалось в главе 9, интерфейсы могут выступать в роли базовых для других типов интерфейсов, позволяя строить иерархии интерфейсов. Вопреки возможным ожиданиям, однако, использовать атрибут extends для наследования интерфейса А от
Глава 17. Язык CIL и роль динамических сборок 623 интерфейса В в CIL нельзя. Этот атрибут разрешено применять только для указания базового класса типа. Для расширения интерфейса должен использоваться атрибут implements: // Расширение интерфейсов в CIL. .class public interface IMylnterface {} .class public interface IMyOtherlnterface implements MyNamespace.IMylnterface {} Определение структур в CIL Директива .class может использоваться для определения любой структуры CTS при условии расширения ее типом System. ValueType. В этом случае она должна обязательно сопровождаться атрибутом sealed (поскольку структуры никогда не могут выступать в роли базовых для других типов-значений). В случае несоблюдения этого требования ilasm.exe будет сообщать об ошибке на этапе компиляции. // При определении структуры должен быть указан атрибут sealed. .class public sealed MyStruct extends [mscorlib]System.ValueType{} В CIL также предусмотрен сокращенный вариант синтаксиса для определения типа структуры. В случае использования атрибута value новый тип будет автоматически наследоваться от типа [mscorlib] System. ValueType. Следовательно, тип MyStruct можно было бы определить и так: // Сокращенный вариант объявления структуры. .class public value MyStructU Определение перечислений в CIL Перечисления .NET унаследованы от класса System.Enum, который, в свою очередь, наследуется от System.ValueType (и потому должен обязательно сопровождаться атрибутом sealed). Чтобы определить перечисление в CIL, необходимо расширить [mscorlib] System.Enum показанным ниже образом: // Определение перечисления. .class public sealed MyEnum extends [mscorlib]System.Enum{ } Как и для структур, для перечислений тоже поддерживается сокращенный синтаксис определения, предусматривающий использование атрибута enum: // Сокращенный вариант определения перечисления. .class public sealed enum MyEnum{} Определение пар имен и значений в перечислении рассматривается чуть позже. На заметку! Делегаты, которые являются еще одним фундаментальным типом в .NET, тоже имеют свое специфическое представление в CIL За дополнительными деталями обращайтесь в главу 11. Определение обобщений в CIL Обобщенные типы также имеют свое представление в синтаксисе CIL. Вспомните из главы 10, что каждый обобщенный тип или член может иметь один и более параметров типа. Например, тип List<T> обладает одним единственным параметром, а Dictionary<TKey/ TValue> — двумя. В CIL количество параметров типа указывается с использованием символа обратной одиночной кавычки (ч) со следующим за ним числом. Как и в С#, сами значения параметров типа заключаются в квадратные скобки.
624 Часть IV. Программирование с использованием сборок .NET На заметку! На большинстве клавиатур символ ч находится на клавише, расположенной над клавишей <ТаЬ> (слева от клавиши <1>). Например, предположим, что необходимо создать переменную List<T>, где на месте Т должно находиться значение типа System. Int32. В CIL это делается следующим образом (в области действия любого метода CIL): // В С#: List<int> mylnts = new List<int>() ; newobj instance void class [mscorlib] System.Collections.Generic.Listчl<int32>::.ctor() Обратите внимание, что данный обобщенный класс определен как List4 Kint32>, поскольку List<T> имеет только один параметр типа. А вот как можно определить тип Dictionary<string/ int>: // В С#: Dictionary<string, int> d = new Dictionary<string, int>(); newob] instance void class [mscorlib] System.Collections.Generic.Dictionaryч2<string,int32>::.ctor() В качестве еще одного примера предположим, что возникла необходимость создать обобщенный тип, использующий в качестве параметра типа еще один обобщенный тип. Соответствующий CIL-код выглядит следующим образом: // В С#: List<List<int» mylnts = new List<List<int»() ; newob] instance void class [mscorlib] System.Collections.Generic.Listч Kclass [mscorlib] System. Collections .Generic . List4Kint32>>: : . ctor () Компиляция файла ClLTypes. il Несмотря на то что ни члены, ни код реализации в определенные ранее типы не добавлялись, данный файл * . il уже можно компилировать в .NET-сборку с расширением . dll (это является единственным доступным вариантом, поскольку метод Main () не был определен). Для этого необходимо открыть окно командной строки и ввести следующую команду для запуска утилиты ilasm.exe: llasm /dll CilTypes.il После выполнения этой команды можно загрузить полученный двоичный файл в ildasm. exe и просмотреть его (рис. 17.3). /7 CifTypes-dll - IL DASM | File View Help |в vHi ! ► MANIFEST а Щ MyNamespace ffi U My Namespace. IMy Interface Ф £ My Namespace. MyBaseClass ffi J£ MyNamespace.MyDerivedClass А-Щ: MyNamespace.MyEnum ffi {3 MyNamespace. MyGenericClassN 1<T> *™"~" .assembly ClLTypes < 1 ' L [ CZD j В Ы"—1 ' * * - Рис. 17.3. Содержимое сборки ClLTypes . dll
Глава 17. Язык CIL и роль динамических сборок 625 Просмотрев содержимое сборки, можно запустить в отношении нее утилиту peverify.ехе: peverify CilTypes.dll Обратите внимание, что это приведет к выдаче ряда сообщений об ошибках, так как все типы совершенно пусты. Ниже показан частичный вывод: Microsoft (R) .NET Framework PE Verifier. Version 4.0.21006.1 Copyright (с) Microsoft Corporation. All rights reserved. [MD]: Error: Value class has neither fields nor size parameter. [token:0x02000005] [MD] : Ошибка: Класс значений не имеет ни полей, ни параметра размера. [token:0x02000005] [MD] : Error: Enum has no instance field, [token:0x02000006] [MD] : Ошибка: Перечисление не имеет полей экземпляра, [token:0x02000006] Перед тем как приступить к заполнению типа содержимым, необходимо сначала узнать, какие типы данных поддерживаются в CIL. Соответствия между типами данных в библиотеке базовых классов .NET, C# и CIL В табл. 17.4 показаны соответствия между ключевыми словами С# и базовыми классами .NET, а также представления этих ключевых слов в CIL. Кроме того, для каждого типа CIL приведены сокращенные константные обозначения. Именно на такие константы часто ссылаются многие коды операций CIL. Таблица 17.4. Соответствия между базовыми классами .NET, ключевыми словами С# и их представлениями в CIL Базовый класс .NET System.SByte System.Byte System.Intl6 System.UIntl6 System.Int32 System.UInt32 System.Int64 System.UInt64 System.Char System.Single System.Double System.Boolean System.String System.Object System.Void Ключевое слово в C# sbyte byte short ushort int uint long ulong char float double bool string object void Представление CIL int8 unsigned intl6 unsigned int32 unsigned int64 unsigned char float32 float64 bool string object void int8 intl6 int32 int64 Константная нотация CIL 11 Ul 12 U2 14 U4 18 U8 CHAR R4 R8 BOOLEAN - - VOID
626 Часть IV. Программирование с использованием сборок .NET На заметку! Типам System. IntPtr и System. UlntPtr соответствуют ключевые слова int и unsigned int (это полезно знать, поскольку во многих сценариях взаимодействия с СОМ и P/lnvoke они используются очень широко). Определение членов типов в CIL Как уже известно, типы в .NET могут иметь различные члены. Так, перечисления могут иметь набор пар имен и значений, а структуры и классы — конструкторы, поля, методы, свойства, статические члены и т.д. В предшествующих главах уже было показано, как выглядят определения упомянутых элементов на CIL, но давайте еще раз кратко рассмотрим, как различные члены отображаются на примитивы CIL. Определение полей данных в CIL Перечисления, структуры и классы поддерживают поля данных. В каждом случае для их определения должна использоваться директива .field. Например, добавим в перечисление MyEnum три пары имен и значений (обратите внимание, что значения указываются в круглых скобках): .class public sealed enum MyEnum{ .field public static literal valuetype MyNamespace.MyEnum A = int32@) .field public static literal valuetype MyNamespace.MyEnum В = int32(l) .field public static literal valuetype MyNamespace.MyEnum С = int32B)) } Здесь видно, что поля, размещаемые в рамках любого типа, который наследуется от System.Enum, квалифицируются атрибутами static и literal. Как не трудно догадаться, эти атрибуты указывают, что данные этих полей должны представлять собой фиксированное значение, доступное только из самого типа (например, MyEnum. А). На заметку! Значения, присваиваемые полям в перечислении, могут быть предоставлены в шест- надцатеричном формате, и в этом случае сопровождаться префиксом Ох. Конечно, если нужно определить какое-то поле данных внутри класса или структуры, использовать только общедоступные статические литеральные данные вовсе не обязательно. Например, можно было бы модифицировать класс MyBaseClass таким образом, чтобы он поддерживал два приватных поля данных уровня экземпляра со значениями по умолчанию: .class public MyBaseClass { .field private string stringField = "hello!" .field private int32 intField = mt32D2) } Как и в С#, поля данных в классе будут автоматически инициализироваться соответствующими значениями по умолчанию. Чтобы предоставить пользователю объекта возможность задавать свои значения во время создания приватных полей данных, потребуется создать специальные конструкторы.
Глава 17. Язык CIL и роль динамических сборок 627 Определение конструкторов для типов в CIL В CTS поддерживается создание конструкторов, действующих как на уровне всего экземпляра, так и на уровне конкретного класса (статических конструкторов). В CIL первые представляются с помощью лексемы . с tor, а вторые — посредством лексемы . cctor (class constructor — конструктор класса). Обе лексемы в CIL должны обязательно сопровождаться атрибутами rtspecialname (return type special name — специальное имя возвращаемого типа) и specialname. Попросту говоря, эти атрибуты применяются для обозначения специфической лексемы CIL, которая может интерпретироваться уникальным образом в любом отдельно взятом языке .NET. Например, в С# возвращаемый тип в конструкторах не определяется, но в CIL оказывается, что возвращаемым значением конструктора на самом деле является void: .class public MyBaseClass { .field private string stringField .field private int32 intField .method public hidebysig specialname rtspecialname instance void .ctor(string s, int32 1) cil managed { // Добавить код реализации. . . } } Обратите внимание, что здесь директива .ctor снабжена атрибутом instance (поскольку в данном случае конструктор не является статическим). Атрибуты cil managed указывают на то, что в рамках данного метода содержится код CIL, а не неуправляемый код, который может использоваться при выполнении запросов на вызов платформы. Определение свойств в CIL Свойства и методы тоже имеют специальный формат представления в CIL. Например, чтобы модифицировать класс MyBaseClass с целью поддержки общедоступного свойства по имени TheString, можно написать следующий CIL-код (обратите внимание на использование атрибута specialname): .class public MyBaseClass { .method public hidebysig specialname instance string get_TheString() cil managed // Добавить код реализации... .method public hidebysig specialname instance void set_TheString(string 'value') cil managed // Добавить код реализации... .property instance string TheString () .get instance string MyNamespace.MyBaseClass::get_TheString() .set instance void MyNamespace.MyBaseClass::set_TheString (string) } }
628 Часть IV. Программирование с использованием сборок .NET Вспомните, что в CIL каждое свойство отображается на пару методов, снабженных префиксами get_ и set_. В директиве .property используются соответствующие директивы .get и .set для отображения синтаксиса свойств на корректные "специально именованные" методы. На заметку! Входной параметр, передаваемый методу set в определении свойства, заключен в одинарные кавычки и представляет собой имя лексемы, которая должна использоваться в правой части операции присваивания в рамках метода. Определение параметров членов В целом аргументы в CIL задаются (более или менее) тем же образом, что и в С#. Например, каждый аргумент в CIL определяется за счет указания типа данных, к которому он относится, и затем самого его имени. Более того, как и в С#, в CIL можно определять входные, выходные и передаваемые по ссылке параметры. В CIL допускается определять аргументы, представляющие массивы параметров (равнозначно тому, что в С# делается с помощью ключевого слова params), и необязательные параметры (которые в С# не поддерживаются, но зато поддерживаются в VB). Рассмотрим процесс определения параметров в CIL на примере. Предположим, что требуется создать метод, принимающий один параметр int32 (по значению), второй параметр int32 (по ссылке), параметр [mscorlib] System. Collection .ArrayList и еще один выходной параметр (тоже типа mt32). В С# этот метод выглядел бы примерно так: public static void MyMethod(int inputlnt, ref int reflnt, ArrayList ar, out int outputlnt) { outputlnt = 0; // Чтобы удовлетворить требования компилятора С#. . . } Отобразив этот метод на CIL, можно увидеть, что ссылочные параметры С# в CIL снабжаются символом амперсанда (&), который присоединяется в виде суффикса к типу параметра (int32&); выходные параметры тоже снабжаются суффиксом &, но при этом дополнительно помечаются лексемой [out]; когда параметр представляет собой параметр ссылочного типа (подобно [mscorlib] System. Collections .ArrayList), перед названием его типа данных в качестве префикса добавляется лексема class (которую ни в коем случае не следует путать с директивой .class). .method public hidebysig static void MyMethod(int32 inputlnt, int32& reflnt, class [mscorlib]System.Collections.ArrayList ar, [out] int32& outputlnt) cil managed { } Изучение кодов операций в CIL Последний аспект CIL-кода, который мы рассмотрим в этой главе, связан с ролью, которую играют различные коды операций. Вспомните, что под кодом операции понимается лексема CIL, используемая для построения логики реализации члена. Все поддерживаемые в CIL коды операций (которых немало) могут быть разделены на три основных категории:
Глава 17. Язык CIL и роль динамических сборок 629 • коды операций, позволяющие управлять выполнением программы; • коды операций, позволяющие вычислять выражения; • коды операций, позволяющие получать доступ к значениям в памяти (через параметры, локальные переменные и т.д.). В табл. 17.5 приведены некоторые наиболее полезные коды операций, имеющие непосредственное отношение к логике реализации членов (для удобства они сгруппированы по функциональности). Таблица 17.5. Различные коды операций в CIL, которые имеют отношение к реализации членов Коды операций Описание add, sub, mul, Позволяют выполнять сложение, вычитание, умножение и деление двух зна- div, rem чений (rem возвращает остаток от деления) and, or, Позволяют выполнять соответствующие побитовые операции над двумя not, xor значениями ceq, cgt, clt Позволяют сравнивать два значения в стеке различными способами: ceq — сравнение на предмет равенства; cgt — сравнение на предмет того, является ли одно из них больше другого; clt — сравнение на предмет того, является ли одно из них меньше другого box, unbox Применяются для преобразования ссылочных типов в типы значения и наоборот ret Применяется для выхода из метода и (если необходимо) возврата значения вызывающему коду beq, bgt, ble, Применяются (вместе с другими похожими кодами операций) для управления bit, switch логикой ветвления внутри метода: beq — позволяет переходить к определенной метке в коде, если при проверке значения оказываются равными; bgt — позволяет переходить к определенной метке в коде, если при проверке одно из значений оказывается больше другого; Ые — позволяет переходить к определенной метке в коде, если при проверке одно из значений оказывается меньше и равным другому; bit — позволяет переходить к определенной метке в коде, если при проверке одно из значений оказывается меньше другого. Все связанные с ветвлением коды операций требуют указания в CIL-коде метки, к которой должен осуществляться переход в случае, если результат проверки оказывается истинным call Применяется для вызова члена определенного типа newarr, Позволяют размещать в памяти, соответственно, новый массив или новый объект newobj Коды операций следующей обширной категории (часть из которых перечислена в табл. 17.6) применяются для загрузки (заталкивания) аргументов в виртуальный стек выполнения. Обратите внимание, что все эти ориентированные на выполнение загрузки коды операций сопровождаются префиксом Id (который означает "load" — загрузка).
630 Часть IV. Программирование с использованием сборок NET Таблица 17.6. Основные коды операций CIL, предназначенные для загрузки в стек Код операции Описание ldarg (и его многочисленные варианты) ldc (и его многочисленные варианты) ldfld (и его многочисленные варианты) ldloc (и его многочисленные варианты) ldobj ldstr Позволяет загружать в стек аргумент метода. Помимо основного варианта ldarg (который работает с индексом, представляющим аргумент), существует множество других вариантов. Например, есть варианты ldarg, которые работают с числовым суффиксом (ldarg_0) и позволяют жестко кодировать загружаемый аргумент. Также есть варианты, позволяющие жестко кодировать как только один тип данных, к которому относится аргумент, с помощью константной нотации CIL из табл. 17.4 (например, ldarg_I4 для int32), так и тип данных и значение вместе (например, ldarg_I4_5, позволяющий загружать int32 со значением 5) Позволяет загружать в стек значение константы Позволяет загружать в стек значение поля уровня экземпляра Позволяет загружать в стек значение локальной переменной Позволяет получать все значения размещаемого в куче объекта и помещать их в стек Позволяет загружать в стек строковое значение Помимо кодов операций, связанных с загрузкой, в CIL поддерживаются коды операций, которые позволяют явным образом извлекать из стека самое верхнее значение. Как уже было показано в нескольких примерах ранее в главе, извлечение значения из стека обычно подразумевает его сохранение во временном локальном хранилище с целью дальнейшего использования (например, параметра для последующего вызова метода). Из-за этого многие коды операций, которые позволяют извлекать текущее значение из виртуального стека выполнения, сопровождаются префиксом st (от "store" — сохранить); в табл. 17.7 перечислены некоторые наиболее часто используемые из шгх. Таблица 17.7. Различные коды операций CIL, предназначенные для извлечения из стека Код операции Описание pop starg stloc (и его многочисленные варианты) stobj stsfld Позволяет удалять значение, которое в текущий момент находится на верхушке стека вычислений, но не сохранять его Позволяет сохранять самое верхнее значение из стека в аргументе метода с определенным индексом Позволяет извлекать текущее значение из верхушки стека вычислений и сохранять его в списке локальных переменных с определенным индексом Позволяет копировать значение определенного типа из стека вычислений в память по определенному адресу Позволяет заменять значение статического поля значением из стека вычислений
Глава 17. Язык CIL и роль динамических сборок 631 Следует принимать во внимание, что различные коды операций в CIL могут предусматривать неявное извлечение значений из стека во время решения поставленной перед ними задачи. Например, при вычитании одного числа из другого с использованием кода операции sub должно быть очевидным, что перед собственно вычислением sub должна извлечь из стека два следующих доступных значения. Результат вычисления будет снова помещен в стек. Директива .maxstack При написании кода реализации методов непосредственно в CIL необходимо помнить об одной особой директиве, которая называется .maxstack. Эта директива позволяет указать максимальное количество переменных, которое может помещаться в стек в любой момент во время выполнения метода. Директива имеет значение по умолчанию (8), подходящее для подавляющего большинства создаваемых методов. При желании можно вручную вычислять количество локальных переменных в стеке и указывать его явно: .method public hidebysig instance void Speak() cil managed { //Во время выполнения данного метода требуется, // чтобы в стеке находилось ровно одно значение // (строковый литерал) . .maxstack 1 ldstr "Hello there.. call void [mscorlib]System.Console::WriteLine(string) ret } Объявление локальных переменных в CIL Теперь давайте посмотрим, как в CIL объявлять локальную переменную. Для этого предположим, что необходимо создать в CIL метод по имени MyLocalVariables (), не принимающий аргументов и возвращающий void, и определить внутри него три локальных переменных типа System.String, System.Int32 и System.Object. В С# этот метод выглядел бы следующим образом (вспомните, что локальные переменные не получают значений по умолчанию и потому перед использованием должны обязательно инициализироваться): public static void MyLocalVariables () { string myStr = "CIL code is fun!11; int mylnt = 33; object myObj = new object (); } Ha CIL его можно написать так: .method public hidebysig static void MyLocalVariables () cil managed { .maxstack 8 // Определение трех локальных переменных. .locals init ([0] string myStr, [1] int32 mylnt, [2] object myObj) // Загрузка строки в виртуальный стек выполнения. ldstr "CIL code is fun1" // Извлечение текущего значения и его сохранение в локальной переменной [0] . stloc.O
632 Часть IV. Программирование с использованием сборок .NET // Загрузка константы типа i4 (сокращенный вариант для типа t32) //со значением 33. Idc.i4 33 // Извлечение текущего значения и его сохранение //в локальной переменной [1] . stloc.1 // Создание нового объекта и его помещение в стек. newob] instance void [mscorlib]System.Object::.ctor () // Извлечение и сохранение текущего значения //в локальной переменной [2] . stloc.2 ret } Как здесь видно, в первую очередь для размещения локальных переменных в CIL должна использоваться директива .locals вместе с атрибутом init. Внутри соответствующих скобок с каждой переменной необходимо ассоциировать определенный числовой индекс (в примере это [0], [1] и [2]). Далее вместе с каждым из этих индексов понадобится указать тип данных (обязательно) и имя переменной (необязательно). После определения локальных переменных останется только загрузить значения в стек (с помощью различных кодов операций, предназначенных для загрузки) и сохранить их в этих локальных переменных (посредством различных кодов операций, предназначенных для сохранения значений). Отображение параметров на локальные переменные в CIL Объявление локальных переменных непосредственно в CIL с использованием директивы .local init уже было показано, а теперь необходимо ознакомиться с отображением входных параметров на локальные методы. Рассмотрим следующий статический метод на С#: public static int Add(int a, int b) { return a + b; } В CIL этот метод, который настолько просто выглядит в С#, потребует массы дополнений. Чтобы представить его на CIL, понадобится, во-первых, разместить входные аргументы (а и Ь) в виртуальном стеке выполнения с помощью кода операции ldarg, во-вторых, использовать код операции add для извлечения двух значений из стека, вычисления их суммы и затем опять ее сохранения в стеке, и, в-третьих, извлечь эту сумму из стека и вернуть ее вызывающему коду с использованием кода операции ret. Если просмотреть этот метод на С# в утилите ildasm. ехе, то видно, что esc. exe вставил массу дополнительных лексем, хотя основная часть CIL-кода выглядит довольно просто: .method public hidebysig static int32 Add(int32 a, int32 b) cil managed { .maxstack 2 ldarg.0 // Загрузка а в стек. ldarg.1 // Загрузка b в стек. add // Сложение обоих значений. ret }
Глава 17. Язык CIL и роль динамических сборок 633 Скрытая ссылка this Обратите внимание, что в CIL-коде для ссылки на входные аргументы (а и Ь) используются их индексные позиции @ и 1), причем нумерация этих позиций в виртуальном стеке выполнения начинается с нуля. При изучении и создании CIL-кода нужно помнить о том, что каждый нестатический метод, который принимает входные аргументы, автоматически получает дополнительный входной параметр, представляющий собой ссылку на текущий объект (похожую на ключевое слово this в С#). Если, например, метод Add () определен не как статический: // Более не является статическим! public int Add(int a, int b) { return a + b; } то входные аргументы а и b будут загружаться с помощью ldarg. 1 и ldarg. 2 (а не ldarg.O и ldarg. 1, как ожидалось). Объясняется это тем, что в ячейке с номером О будет содержаться неявная ссылка this. Ниже приведен псевдокод, который позволит удостовериться в этом. // Это ТОЛЬКО псевдокод' .method public hidebysig static int32 AddTwoIntParams( MyClass_HiddenThisPointer this, int32 a, int32 b) cil managed { ldarg.O // Загрузка MyClass_HiddenThisPointer в стек. ldarg . 1 // Загрузка а в стек. ldarg. 2 // Загрузка b в стек. } Представление итерационных конструкций в CIL Итерационные конструкции в языке программирования С# представляются с помощью таких ключевых слов, как for, foreach, while и do, каждое из которых имеет специальное представление в CIL. Для примера рассмотрим следующий классический цикл for: public static void CountToTenO { for (int i = 0; l < 10; i++) } Вспомните, что для управления ходом выполнения программы на основе условий в CIL применяются коды операций br (br, bit и т.д.). В данном примере условие гласит, что выполнение цикла должно завершаться тогда, когда значение локальной переменной i становится больше или равно 10. С каждым проходом к значению i добавляется 1, после чего проверяемое условие вычисляется заново. Кроме того, при использовании любого из связанных с ветвлением кодов операций в CIL должна быть определена специальная метка (или две) для обозначения места, куда будет произведен переход в случае удовлетворения условия. С учетом всего вышесказанного взглянем на следующий (расширенный) код CIL, сгенерированный в ildasm.exe (вместе с соответствующими метками):
634 Часть IV. Программирование с использованием сборок .NET .method public hidebysig static void CountToTen () cil managed .maxstack 2 .locals IL 0000 IL 0001 IL 0002 IL 0004 IL 0005 IL 0006 IL 0007 IL 0008 IL 0009 IL 000b IL OOOd init ([0] int32 i) ldc.i4.0 stloc.O br.s IL 0008 ldloc.O ldc.i4.1 add stloc.O ldloc.O ldc.i4.s 10 blt.s IL 0004 ret // Инициализация локальной // целочисленной переменной i. // Загрузка этого значения в стек. // Сохранение этого значения с индексом 0. // Переход к метке IL_0008. // Загрузка значения переменной с индексом 0. // Загрузка значения 1 в стек. // Добавление значения this в стеке с индексом 0. // Загрузка значения с индексом 0. // Загрузка значения 10 в стек. // Меньше? Если да, то перейти обратно к IL_0004. Этот CIL-код начинается с определения локальной переменной int32 и ее загрузки в стек. После этого осуществляется переход туда и обратно между метками IL0008 и IL0004, во время каждого из которых значение i увеличивается на 1 и проверяется на предмет того, по-прежнему ли оно меньше 10. Если нет, происходит выход из метода. Исходный код. Пример CilTypes доступен в подкаталоге Chapter 17. Создание сборки .NET на CIL (Ознакомившись с синтаксисом и семантикой языка CIL, пришла пора закрепить изученный материал, создав .NET-приложение с использованием одной только утилиты ilasm. exe и предпочитаемого текстового редактора. Это приложение будет состоять из приватно развертываемой однофайловой сборки *.dll, содержащей определения двух типов классов, и консольной сборки * . ехе, взаимодействующей с этими типами. Создание CILCars. dll В первую очередь необходимо создать сборку * .dll, которую будет использовать клиент. Для этого откройте текстовый редактор и создайте новый файл *. il по имени CILCars. il. В этой однофайловой сборке будут использоваться две внешних сборки .NET. Поэтому для начала понадобится модифицировать файл кода следующим образом: // Добавление ссылок на mscorlib.dll // и System.Windows.Forms.dll. .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 4:0:0:0 } .assembly extern System.Windows.Forms { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 4:0:0:0 } // Определение однофайловой сборки. .assembly CILCars
Глава 17. Язык CIL и роль динамических сборок 635 .hash algorithm 0x00008004 .ver 1:0:0:0 } .module riLCarc.dll В этой сборке будет содержаться два типа класса. Первый называется CILCar и имеет два поля данных и специальный конструктор, а второй — CarlnfoHelper и имеет единственный статический метод по имени DisplayCarlnfo (), принимающий CILCar в качестве параметра и возвращающий void. Оба они должны размещаться в пространстве имен CILCars. Класс CILCar может быть реализован в CIL следующим образом: // Реализация типа CILCars.CILCar. .namespace CILCars { .class public unto anci tef nr.^f lei ilini t CILCar extends [mscorlib]System.Object { // Определение полей данных в CILCar. .field public string petUame .field public int32 currSpeed // Определение специального конструктора, позволяющего // вызывающему коду присваивать полям данных определенные значения. .method public hidebybig specialname rtspecialname instance ' uid .ctor(int32 c, string p) cil managed { .ma.-stack Я // Загрузка первого аргумента в стек и // вызов конструктора базового класса. ldarg.O // объект this, а не int32' call mstdiice void [mscorlib] System. Object::. ctor () // Загрузки первого и второго аргумента в стек. ldarg.O // объект this ldarg.l // аргумент int32 // Сохранение самого верхнего элемента //из стека (int 32) в поле currSpeed. stfld int32 CILCars.CILCar::currSpeed // Загрузка аргумента string и его // сохранение в поле petName. ldarg.O // объект this ldarg.2 // аргумент string stfld string CILCars.CILCar:rpetName ret Памятуя о том, что первым аргументом для любого нестатического члена является ссылка на текущий объект, в первом блоке CIL просто загружается ссылка на текущий объект и вызывается конструктор базового класса. Далее входные аргументы конструктора помещаются в стек и сохраняются в соответствующих полях данных с помощью кода операции stfld. Теперь реализуем второй тип класса в данном пространстве имен — CILCarlnfo. Главным в этом типе является статический метод Display (). Роль этого метода состоит в том, чтобы принимать в качестве входного параметра тип CILCar, извлекать значения из его полей данных и отображать их в окне сообщений Windows Forms. Ниже показано, как должен выглядеть необходимый для реализации CILCarlnfo код:
636 Часть IV. Программирование с использованием сборок .NET .class public auto ansi beforefieldinit CILCarlnfo extends [mscorlib]System.Object { .method public hidebysig static void Display(class CILCars.CILCar c) cil managed { .maxstack 8 // Необходима какая-нибудь локальная строковая переменная. .locals init ([0] string caption) // Загрузка строки и входного параметра CILCar в стек. ldstr " { 0 } ' s speed is:11 ldstr "Скорость {0} составляет: " ldarg.O // Помещение в стек значения поля petName из CILCar // и вызов статического метода String.Format(). ldfld string CILCars.CILCar:ipetName call string [mscorlib]System.String::Format (string, object) stloc.0 // Загрузка значения поля currSpeed и получение его строкового // представления (обратите внимание на вызов ToStringO) . ldarg.O ldflda int32 CILCars.CILCar::currSpeed call instance string [mscorlib]System.Int32::ToString() ldloc.O // Вызов метода MessageBox.Show() с загруженными значениями. call valuetype [System.Windows.Forms] System.Windows.Forms.DialogResult [System.Windows.Forms] System.Windows.Forms.MessageBox::Show(string, string) pop ret } } Хотя данный CIL-код по объему немного больше, чем код реализации CILCar, все равно в нем все выглядит вполне понятно. Из-за того, что на этот раз определяется статический метод, беспокоиться о скрытой ссылке на текущий объект больше не требуется (поскольку в таком случае код операции Idarg. 0 будет действительно приводить к загрузке входного аргумента CILCar). Метод начинается с загрузки в стек строки " { 0 } ' s speed is " и следом за ней аргумента CILCar. После этого производится загрузка значения petName и вызов статического метода System. String. Format () для подстановки на месте метки-заполнителя внутри фигурных скобок дружественного имени CILCar. Обработка поля currSpeed в целом производится по такой же процедуре, но на этот раз используется код операции ldflda для загрузки в стек адреса аргумента. После этого вызывается метод System. Int32 . ToString () для преобразования находящегося по указанному адресу значения в строку. И, наконец, после завершения необходимого форматирования обеих строк вызывается метод MessageBox. Show (). Теперь можно скомпилировать новую сборку * .dll с помощью ilasm.exe: llasm /dll CILCars.il и проверить содержащийся внутри нее CIL-код на предмет правильности с семантической точки зрения с помощью утилиты peverify.exe: peverify CILCars.dll
Глава 17. Язык CIL и роль динамических сборок 637 Создание CILCarClient. ехе Далее можно создать простую сборку * . ехе с методом Main () внутри, который будет создавать объект CILCar и передавать его статическому методу CILCarlnf о. Display (). Для этого создадим новый файл CarClient.il, добавим в него ссылки на внешние сборки mscorlib.dll и CILCars .dll (не забыв поместить копию последней в каталог клиентского приложения) и определим в нем единственный тип (Program), в котором будут производиться все необходимые манипуляции над сборкой CILCars.dll. Ниже показан полный код: // Добавление ссылок на внешние сборки. assembly extern mscorlib .publickeytoken = (B7 7A 5C 56 19 34 EO 89) .ver 4:0:0:0 assembly extern CILCars .ver 1:0:0:0 // Определение исполняемой сборки. assembly CarClient .hash algorithm 0x00008004 .ver 1:0:0:0 module CarClient.exe // Реализация типа Program. namespace CarClient .class private auto ansi beforefleldinit Program extends [mscorlib]System.Object { .method private hidebysig static void Main(string [ ] args) cil managed { // Обозначение точки входа в *.ехе. .entrypoint .maxstack 8 // Объявление локальной переменной CILCar и помещение // значений в стек для вызова конструктора. .locals init ([0] class [CILCars]CILCars.CILCar myCilCar) ldc.i4 55 ldstr "Junior" // Создание нового объекта CILCar, его сохранение //и затем загрузка ссылки на него. newobj instance void [CILCars]CILCars.CILCar::.ctor(int32, string) stloc.O ldloc.O // Вызов метода Display() и передача ему самого верхнего значения из стека. call void [CILCars] CILCars.CILCarlnfo::Display( class [CILCars]CILCars.CILCar) ret } } }
638 Часть IV. Программирование с использованием сборок .NET Единственным кодом операции, на который здесь важно обратить внимание, является .entrypoint. Как упоминалось ранее в главе, этот код применяется для обозначения того, какой из методов должен быть входной точкой в модуле * . ехе. В действительности, поскольку по .entrypoint CLR-среда определяет начальный метод для выполнения, этот метод может иметь какое угодно имя, хотя для него было использовано стандартное имя Main (). В остальной части CIL-кода этого метода Main () производятся типич- Рис. 17.4. Сборка ные операции по помещению и извлечению значений из стека. CILCar в действии Кроме того, для создания CILCar используется код операции . newob j. В связи с этим вспомните, что для вызова члена типа непосредственно в CIL применяется синтаксис в виде двойного двоеточия и, как обычно, указывается полностью квалифицированное имя типа. Теперь можно скомпилировать новый файл с помощью ilasm. exe, проверить его правильность с точки зрения семантики с помощью peverify.exe и затем запустить программу: ilasm CarClient.il peverify CarClient.exe CarClient.exe На рис. 17.4 показан результат выполнения сборки CILCar. Исходный код. Пример CilCars доступен в подкаталоге chapter 17. Динамические сборки Естественно, процесс создания сложных .NET-приложений на CIL будет довольно "неблагодарным трудом". С одной стороны, CIL представляет собой чрезвычайно выразительный язык программирования, позволяющий взаимодействовать со всеми программными конструкциями, которые предусмотрены в CTS. С другой стороны, написание кода непосредственно на CIL отнимает много времени и сил и чревато допущением ошибок. И хотя знание — это всегда сила, все же наверняка интересно, насколько в действительности важно держать правила синтаксиса CIL в голове? Ответ на этот вопрос зависит от ситуации. Конечно, в большинстве случаев при программировании .NET-приложений просматривать, редактировать или создавать CIL-код совсем необязательно. Однако знание основ CIL позволяет перейти к исследованию мира динамических сборок (в отличие от статических) и оценки роли пространства имен System.Refleetion.Emit. В первую очередь может возникнуть вопрос, чем отличаются статические и динамические сборки? По определению, статической сборкой называется такой двоичный модуль .NET, который загружается прямо из хранилища на диске, те. на момент запроса CLR-средой он находится в физическом файле (или файлах, если сборка многофайловая) где-то на жестком диске. Как не трудно догадаться, при каждой компиляции исходного кода С# в результате всегда получается статическая сборка. С другой стороны, динамическая сборка создается в памяти "на лету" за счет использования типов из пространства имен System.Ref lection .Emit. Пространство имен System. Reflection. Emit, по сути, позволяет сделать так, чтобы сборка с ее модулями, определениями типов и логикой реализации на CIL создавалась во время выполнения. После этого сборку в памяти можно сохранять в дисковый файл, превращая ее в статическую. Несомненно, для создания динамических сборок с помощью пространства имен System.Reflection.Emit нужно разбираться в природе кодов операций CIL. 1^^^'^. л» Junior's speed is: 55 L OK
Глава 17. Язык CIL и роль динамических сборок 639 Хотя процесс создания динамических сборок является довольно сложным (и нечасто применяемым) приемом программирования, он полезен в перечисленных ниже ситуациях. • Создается штструмент для программирования .NET, который должен быть способен генерировать сборки по требованию на основе вводимых пользователем данных. • Создается приложение, которое должно уметь генерировать прокси для удаленных типов на лету на основе получаемых метаданных. • Требуется возможность загрузки статической сборки и динамической вставки в ее двоичный образ новых типов. Некоторые компоненты механизма исполняющей среды .NET тоже предусматривают генерацию динамических сборок в фоновом режиме. Например, в ASP.NET эта технология применяется для отображения кода разметки и серверного сценария на объектную модель исполняющей среды. В LINQ код тоже может генерироваться "на лету" на основе различных выражений, содержащихся в запросах. Давайте посмотрим, какие типы предлагаются в пространстве имен System.Reflection.Emit. Пространство имен System.Reflection.Emit Для создания динамических сборок необходимо иметь представление о кодах операций CIL, однако типы, поставляемые в пространстве имен System. Ref lection .Emit, максимально возможно скрывают сложные детали CIL. Например, вместо того, чтобы напрямую указывать необходимые директивы и атрибуты CIL для определения типа класса, можно воспользоваться классом TypeBuilder. Еще один класс, ConstructorBuilder, позволит определить новый конструктор на уровне экземпляра, не имея дела напрямую с лексемами specialname, rtspecialname или . ctor. Ключевые члены пространства имен System. Ref lection. Emit перечислены в табл. 17.8. Таблица 17.8. Некоторые члены пространства имен System. Ref lection. Emit Член Описание AssemblyBuilder Используется для создания сборки (* . dll или * . ехе) во время выполнения. В сборках * . ехе должен обязательно вызываться метод ModuleBuilder. SetEntryPoint () для указания метода, который должен выступать в роли точки входа в модуль. Если точка входа не задана, генерируется файл * . dll ModuleBuilder Используется для определения набора модулей внутри текущей сборки EnumBuilder Используется для создания типа перечисления .NET TypeBuilder Может применяться для создания в модуле различных классов, интерфейсов, структур и делегатов во время выполнения MethodEuilder Используются для создания во время выполнения соответствую- LocalBuilder щих членов типов (таких как методы, локальные переменные, свой- PropertyBuild'ir ства, конструкторы и атрибуты) FieldBuilder ConstructorBuilder CustomAttnbuteBuilder ParameterBuilder E^entBuilder iLGenerator Генерирует необходимые коды операций CIL внутри указанного члена типа Opcodes Предоставляет множество полей, которые отображаются на коды операций CIL Используется вместе с различными членами System.Refleetion.Emit.ILGenerator
640 Часть IV. Программирование с использованием сборок .NET В целом типы из пространства имен System. Re flection. Em it позволяют представлять исходные лексемы CIL программным образом во время построения динамической сборки. Многие из них будут использоваться в приведенном ниже примере, а тип ILGenerator детально рассматривается в следующем разделе. Роль типа System.Reflection.Emit.ILGenerator Роль типа ILGenerator заключается во вставке соответствующих кодов операций CIL в заданный член типа. Напрямую создавать объекты ILGenerator нельзя, потому что этот тип не имеет никаких общедоступных конструкторов. Вместо этого объекты ILGenerator должны получаться за счет вызова конкретных методов Builder-типов (таких как MethodBuilder и ConstructorBuilder), например: // Получение ILGenerator из объекта ConstructorBuilder // по имени myCtorBuilder. ConstructorBuilder myCtorBuilder = new ConstructorBuilder(/* ...различные аргументы... */) ; ILGenerator myCILGen = myCtorBuilder.GetlLGenerator(); После получения ILGenerator можно приступать к генерации с его помощью низкоуровневых кодов операций CIL, применяя любые его методы (см. табл. 17.9). Таблица 17.9. Методы типа ILGenerator Метод Описание BeginCatchBlock() BeginExceptionBlock() BeginFinallyBlock() BeginScope() DeclareLocal() DefineLabel() Emit () EmitCall() EmitWriteLine() EndExceptionBlock() EndScope() ThrowException() UsingNamespace() Начинает блок catch Начинает блок генерации нефильтруемого исключения Начинает блок finally Начинает лексический контекст Объявляет локальную переменную Объявляет новую метку Имеет несколько перегруженных версий и позволяет генерировать коды операций CIL Помещает в поток CIL код операции call или callvirt Генерирует вызов Console. WriteLine () со значениями разных типов Завершает блок исключения Завершает лексический контекст Генерирует инструкцию для выдачи исключения Специфицирует пространство имен, которое должно использоваться при вычислении локальных и наблюдаемых значений в текущем активном лексическом контексте Главным в ILGenerator является метод Emit (), который работает вместе с типом класса System. Reflection .Emit. OpCodes. Как уже упоминалось ранее в главе, данный тип имеет множество доступных только для чтения полей, которые отображаются на исходные коды операций CIL. Полный список его членов можно найти в онлайновой справке, а примеры применения некоторых из них — чуть позже в настоящей главе.
Глава 17. Язык CIL и роль динамических сборок 641 Создание динамической сборки Чтобы увидеть, как выглядит процесс определения сборки .NET во время выполнения, давайте попробуем создать однофайловую динамическую сборку по имени MyAssembly.dll. Внутри ее главного модуля должен находиться класс HelloWorld. В этом классе поддерживается конструктор по умолчанию и специальный конструктор, который используется для установки значения приватной переменной экземпляра (theMessage) типа string. В классе также есть общедоступный метод экземпляра по имени SayHello (), который выводит приветственное сообщение в стандартный поток ввода-вывода, и еще один метод экземпляра по имени GetMsg (), возвращающий внутреннюю приватную строку. По сути, нужно сгенерировать программно следующий тип класса: // Этот класс будет создаваться во время выполнения //с помощью System. Reflection. Emit. public class HelloWorld { private string theMessage; HelloWorld() {} HelloWorld(string s) { theMessage = s; } public string GetMsg() { return theMessage;} public void SayHello () { System.Console.WriteLine ("Hello from the HelloWorld class!"); } } Создадим в Visual Studio 2010 новый проект типа Console Application (Консольное приложение) по имени MyAsmBuilder, импортируем в него пространства имен System. Reflection, System. Reflection .Emit и System. Threading и определим статический метод по имени CreateMyAsm (), который будет отвечать за решение следующих задач: • определение характеристик динамической сборки (имя, версию и т.д.); • реализация типа HelloClass; • сохранение сгенерированной в памяти сборки в физическом файле. Метод CreateMyAsm () принимает в качестве единственного параметра тип System. АррDomain и использует его для получения доступа к типу AssemblyBuilder, ассоциированному с текущим доменом приложения (домены приложений рассматривались в главе 17). Код метода CreateMyAsm () показан ниже. // Передача типа AppDomain из вызывающего кода. public static void CreateMyAsm(AppDomain curAppDomain) { // Установка общих характеристик сборки. AssemblyName assemblyName = newAssemblyName (); assemblyName.Name = "MyAssembly"; assemblyName.Version = new Version(.0.0 . 0") ; // Создание новой сборки в текущем домене приложения. AssemblyBuilder assembly = curAppDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save); // Поскольку создается однофайловая сборка, имя модуля // должно совпадать с именем самой сборки. ModuleBuilder module = assembly .Def ineDynamicModule ("MyAssembly11, "MyAssembly.dll") ;
642 Часть IV. Программирование с использованием сборок .NET // Определение общедоступного класса по имени HelloWorld. TypeBuilder helloWorldClass = module.DefineType("MyAssemblу.HelloWorld" , TypeAttributes.Public); // Определение приватной строковой переменной экземпляра по имени theMessage. FieldBuilder msgField = helloWorldClass.DefineField("theMessage", Type.GetType("System.String"), FieldAttributes.Private); // Создание специального конструктора. Type[] constructorArgs = new Type[l]; constructorArgs[0] = typeof(string) ; ConstructorBuilder constructor = helloWorldClass.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArgs); ILGenerator constructorIL = constructor.GetlLGenerator(); constructorlL.Emit(OpCodes.Ldarg_0); Type objectClass = typeof(object) ; Constructorlnfo superConstructor = objectClass.GetConstructor(new Type[0]); constructorlL.Emit(OpCodes.Call, superConstructor); constructorlL.Emit(OpCodes.Ldarg_0); constructorIL.Emit(OpCodes.Ldarg_l); constructorlL.Emit(OpCodes.StfId, msgField); constructorlL.Emit(OpCodes.Ret); // Создание конструктора по умолчанию. helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public); // Создание метода GetMsgQ . MethodBuilder getMsgMethod = helloWorldClass.DefineMethod("GetMsg", MethodAttributes.Public, typeof(string), null); ILGenerator methodIL = getMsgMethod.GetlLGenerator() ; methodIL.Emit(OpCodes.Ldarg_0); methodIL.Emit(OpCodes.LdfId, msgField); methodIL.Emit(OpCodes.Ret); // Создание метода SayHello. MethodBuilder sayHiMethod = helloWorldClass.DefineMethod("SayHello", MethodAttributes.Public, null, null); methodIL = sayHiMethod.GetlLGenerator (); methodIL.EmitWriteLine("Hello from the HelloWorld class1"); methodIL.Emit(OpCodes.Ret); // Генерация класса HelloWorld. helloWorldClass.CreateType(); // Сохранение сборки в файле (необязательно). assembly.Save("MyAssemblу.dll"); Генерация сборки и набора модулей В теле метода сначала устанавливается минимальный набор характеристик сборки с использованием типов AssemblyName и Version (из пространства имен System. Reflection). После этого получается тип AssemblyBuilder через действующий на уровне экземпляра метод AppDomain . Def ineDynamicAssembly () (вспомните, что ссылка AppDomain передается методу CreateMyAsm () из вызывающего кода).
Глава 17. Язык CIL и роль динамических сборок 643 // Установка общих характеристик сборки //и получение доступа к типу AssemblyBuilder. public static void CreateMyAsm(AppDomain curAppDomain) { AssemblyName assemblyName = new AssemblyName (); assemblyName.Name = "MyAssembly"; assemblyName.Version = new Version(.0.0.0"); // Создание новой сборки в текущем домене приложения. AssemblyBuilder assembly = curAppDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save); } При вызове AppDomain . Def ineDynamicAssembly () должен быть указан желаемый режим доступа к сборке, который может принимать любое из значений перечисления AssemblyBuilderAccess (см. табл. 17.10). Таблица 17.10. Значения перечисления AssemblyBuilderAccess Значение Описание Ref lectionOnly Указывает, что динамическая сборка может только воспроизводиться посредством рефлексии Run Указывает, что динамическая сборка может только выполняться в памяти, но не сохраняться на диске RunAndSave Указывает, что динамическая сборка может выполняться в памяти и сохраняться на диске Save Указывает, что динамическая сборка может только сохраняться на диске, но не выполняться в памяти Следующая задача состоит в определении набора модулей для новой сборки. Из-за того, что в данном случае сборка должна представлять одиночный файл, необходимо определить для нее только один модуль. При создании многофайловой сборки с использованием метода Def ineDynamicModule () понадобилось бы предоставить второй необязательный параметр, в котором указать имя модуля (например, myMod.dotnetmodule). В случае однофайловой сборки имя модуля совпадает с именем самой сборки. В результате вызова метода Def ineDynamicModule () возвращается ссылка на действительный тип ModuleBuilder: // Однофайловая сборка. ModuleBuilder module = assembly.DefineDynamicModule("MyAssembly", "MyAssembly.dll"); Роль типа ModuleBuilder Тип ModuleBuilder играет ключевую роль при разработке динамических сборок. Он имеет набор членов, позволяющих определять типы, которые должны содержаться внутри модуля (классы, интерфейсы, структуры и т.д.), а также используемые встроенные ресурсы (таблицы, изображения и т.п.). В табл. 17.11 перечислены методы ModuleBuilder, которые являются наиболее полезными при создании динам1гческих сборок. (Обратите внимание, что каждый из этих методов возвращает соответствующий тип, который представляет создаваемый тип.)
644 Часть IV. Программирование с использованием сборок .NET Таблица 17.11. Некоторые методы типа ModuleBuilder Метод Описание Def ineEnum () Используется для генерации определения перечисления .NET Def ineResource () Определяет управляемый вложенный ресурс, который должен храниться в данном модуле Def ineType () Создает объект TypeBuilder, который позволяет определить типы значения, интерфейсы и типы классов (включая делегаты) Ставным членом класса ModuleBuilder является метод Def ineType (). Помимо указания имени для типа (в виде простой строки), в нем может использоваться перечисление System. Reflection . TypeAttributes для описания формата типа. В табл. 17.12 перечислены наиболее важные члены перечисления TypeAttributes. Таблица 17.12. Некоторые члены перечисления TypeAttributes Член Описание Abstract Указывает, что тип является абстрактным Class Указывает, что тип является классом Interface Указывает, что тип является интерфейсом NestedAssembly Указывает, что класс является вложенным в область видимости сборки, а потому доступен только методам внутри этой сборки NestedFamAndAssem Указывает, что класс находится в области видимости сборки и семейства, а потому доступен только методам, которые относятся к пересечению этого семейства и сборки NestedFamily Указывает, что класс находится в области видимости семейства, а потому доступен только методам, которые содержатся внутри его собственного типа и любых подтипов NestedFamORAssem Указывает, что класс находится в области видимости семейства или сборки, а потому доступен только методам, которые присутствуют и в этом семействе, и в сборке NestedPrivate Указывает, что класс является вложенным и приватным NestedPublic Указывает, что класс является вложенным и общедоступным NotPublic Указывает, что класс не является общедоступным Public Указывает, что класс является общедоступным Sealed Указывает, что класс является конкретным и не может быть расширен Serializable Указывает, что класс может подвергаться сериализации Генерация типа HelloClass и принадлежащей ему строковой переменной Ознакомившись с ролью метода ModuleBuilder .CreateType (), теперь посмотрим, как можно сгенерировать класс HelloWorld и приватную строковую переменную: // Определение общедоступного класса по имени MyAssembly.HelloWorld. TypeBuilder helloWorldClass = module.DefineType("MyAssembly.HelloWorld", TypeAttributes.Public);
Глава 17. Язык CIL и роль динамических сборок 645 // Определение приватной строковой переменной // экземпляра по имени theMessage. FieldBuilder msgField = helloWorldClass.DefineField("theMessage", typeof(string), FieldAttributes.Private); Обратите внимание, что метод TypeBuilder. Def ineField () предоставляет доступ к типу FieldBuilder. Класс TypeBuilder имеет и другие методы, которые обеспечивают доступ к другим типам в стиле Builder. Например, метод Def ineConstructor () возвращает тип ConstructorBuilder, метод Def ineProperty () —тип PropertyBuilder, и т.д. Генерация конструкторов Как упоминалось ранее, для определения конструктора текущего типа можно использовать метод TypeBuilder . Def ineConstructor () . В рассматриваемом примере при реализации конструктора HelloClass необходимо вставить в тело конструктора низкоуровневый CIL-код, который будет отвечать за присваивание входного параметра внутренней приватной строке. Для получения типа ILGenerator понадобится вызвать метод GetlLGenerator () из соответствующего Builder-типа (в данном случае — ConstructorBuilder). Метод Emit () в классе ILGenerator представляет собой ту самую сущность, которая отвечает за помещение CIL-кода в реализацию членов. В самом методе Emit () часто используется тип класса Opcodes, который предоставляет доступ к набору кодов операций CIL посредством предназначенных только для чтения свойств. Например, свойство Opcodes .Ret сигнализирует о возврате вызова метода, Opcodes .Stfld позволяет выполнять в отношении переменной экземпляра операцию присваивания, a Opcodes . Call применяется для вызова конкретного метода (и в рассматриваемом случае может использоваться для вызова конструктора базового класса). С учетом всего вышесказанного, логика для реализации конструктора будет выглядеть следующим образом: // Создание специального конструктора, принимающего // единственный аргумент типа System.String. Type[] constructorArgs = new Type[l]; constructorArgs[0] = typeof(string); ConstructorBuilder constructor = helloWorldClass.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArgs); // Вставка в конструктор необходимого CIL-кода. ILGenerator constructorIL = constructor.GetlLGenerator (); constructorlL.Emit(OpCodes.Ldarg_0); Type objectClass = typeof(object); Constructorlnfo superConstructor = objectClass.GetConstructor(new Type [0] ) ; constructorlL.Emit(OpCodes.Call, superConstructor); // Вызов конструктора базового класса. // Загрузка указателя на текущий объект в стек. constructorIL.Emit(OpCodes.Ldarg_0); // Загрузка входного аргумента в стек //и его сохранение в msgField. constructorIL.Emit(OpCodes.Ldarg_l); constructorlL.Emit(OpCodes.Stfld, msgField); // Установка msgField. constructorlL.Emit(OpCodes.Ret); // Возврат.
646 Часть IV. Программирование с использованием сборок .NET Как известно, в результате определения специального конструктора для типа конструктор по умолчанию незаметно удаляется. Чтобы снова определить конструктор, не принимающий аргументов, достаточно просто вызвать метод Def ineDef aultConstructor () типа TypeBuilder: // Вставка конструктора по умолчанию заново. helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public) ; Этот единственный вызов генерирует стандартный CIL-код для определения конструктора по умолчанию: .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 1 ldarg.O call instance void [mscorlib]System.Object::.ctor () ret } Генерация метода SayHello () И, наконец, осталось рассмотреть только процесс генерирования метода SayHello (). Первой задачей будет получение типа MethodBuilder из переменной helloWorldClass. После этого можно определить сам метод и получить лежащий в основе тип ILGenerator для вставки необходимых CIL-инструкций: // Создание метода SayHello. MethodBuilder sayHiMethod = helloWorldClass.DefineMethod("SayHello", MethodAttributes.Public, null, null); methodIL = sayHiMethod.GetlLGenerator (); // Вывод строки в окне консоли. methodIL.EmitWriteLine ( "Hello there•"); methodIL.Emit(Opcodes.Ret); Здесь был определен общедоступный метод (MethodAttributes . Public), не принимающий параметров и ничего не возвращающий (на что указывают значения null в вызове Def ineMethod ()). Обратите внимание на вызов метода EmitWriteLine (). Этот вспомогательный член класса ILGenerator автоматически записывает строку в стандартный поток вывода с приложением минимальных усилий. Использование динамически сгенерированной сборки Имея готовую логику для создания и сохранения сборки, осталось создать только класс, который будет ее запускать. Определим в текущем проекте еще один класс по имени AsmReader. Внутри методе Main () с помощью метода Thread.GetDoMain () необходимо получить ссылку на текущий домен приложения, который будет использоваться для обслуживания динамически создаваемой сборки. После получения ссылки можно будет вызывать метод CreateMyAsm (). Чтобы сделать пример немного интереснее, после вызова CreateMyAsm () воспользуемся поздним связыванием (см. главу 15) для загрузки вновь созданной сборки в память и взаимодействия с членами класса HelloWorld. Модифицируем метод Main (), как показано ниже. static void Main(string [ ] args) { Console.WriteLine ("***** The Amazing Dynamic Assembly Builder App *****");
Глава 17. Язык CIL и роль динамических сборок 647 // Получение ссылки на домен приложения, в котором выполняется текущий поток. AppDomain curAppDomain = Thread.GetDomain(); // Создание динамической сборки с помощью вспомогательной функции f(x) . CreateMyAsm(curAppDomain); Console.WriteLine("-> Finished creating MyAssembly.dll."); // Загрузка новой сборки из файла. Console.WriteLine("-> Loading MyAssembly.dll from file."); Assembly a = Assembly. Load ("IlyAssembly" ) ; // Получение типа HelloWorld. Type hello = a.GetType("MyAssembly.HelloWorld"); // Создание объекта HelloWorld и вызов правильного конструктора. Console.Write("-> Enter message to pass HelloWorld class: "); string msg = Console.ReadLine(); object[] ctorArgs = new object[1]; ctorArgs[0] = msg; object obj = Activator.Createlnstance(hello, ctorArgs); // Вызов SayHello и отображение возвращаемой строки. Console.WriteLine ("-> Calling SayHello() via late binding."); Methodlnfo mi = hello.GetMethod("SayHello"); mi.Invoke(obi, null); // Вызов метода GetMsg. mi = hello.GetMethod("GetMsg"); Console.WriteLine(mi.Invoke(obj, null)); } Итак, создана сборка .NET, которая может генерировать и запускать другие сборки .NET во время выполнения. На этом рассмотрение CIL и роли динамических сборок завершено. Эта глава должна была помочь углубить знания системы типов .NET, а также синтаксиса и семантики языка CIL. Исходный код. Проект DynamicAsmBuilder доступен в подкаталоге Chapter 17 Резюме В этой главе был приведен краткий обзор основных деталей синтаксиса и семантики языка CIL. В отличие от управляемых языков более высокого уровня, таких как С#, в CIL используется не набор ключевых слов, а директивы (позволяющие определить структуру сборки и ее типов), атрибуты (дополнительно уточняющие каждую директиву) и коды операций (применяемые для реализации членов типов). Было продемонстрировано несколько доступных для программирования на CIL инструментов (ilasm. exe, SharpDevelop и peverify. exe), а также показано, как изменять содержимое сборки .NET за счет добавления новых СIL-инструкций с использованием методики двунаправленного проектирования. Кроме того, рассматривались способы определения в CIL текущей (и внешней сборки), пространств имен, типов и членов. Был предложен простой пример создания библиотеки кода и исполняемого файла .NET с применением только CIL и соответствующих инструментов командной строки. В конце главы приводился краткий обзор процесса создания динамической сборки. Используя пространство имен System. Ref lection .Emit, сборку .NET можно определить в памяти во время выполнения. Однако, как было указано, применение этого пространства имен требует знаний семантики CIL-кода. Хотя построение динамических сборок не является распространенной задачей при написании большинства приложений .NET, такие сборки могут быть очень полезны при разработке инструментальных средств поддержки и прочих утилит для программирования.
ГЛАВА 18 Динамические типы и исполняющая среда динамического языка В версии .NET 4.0 язык С# получил новое ключевое слово — dynamic. Это ключевое слово позволяет включать поведение, подобное сценариям, в строго типизированный мир точек с запятой и фигурных скобок. Используя эту слабую типизацию, можно значительно упростить некоторые сложные задачи кодирования и получить возможность взаимодействия с множеством динамических языков (таких как IronRuby и IronPython), которые поддерживают .NET. В этой главе будет описано ключевое слово dynamic, а также показано, как слабо типизированные вызовы отображаются на корректные объекты в памяти, благодаря DLR (Dynamic Language Runtime — исполняющая среда динамического языка). После рассмотрения служб, предоставляемых DLR, будут приведены примеры использования динамических типов для облегчения выполнения вызовов методов с поздним связыванием (через службы рефлексии) и для упрощения взаимодействия с унаследованными библиотеками СОМ. На заметку! Не путайте ключевое слово С# dynamic с концепцией динамической сборки (см. главу 17). Хотя ключевое слово dynamic может использоваться при построении динамической сборки, все же это две совершенно независимые концепции. Роль ключевого слова С# dynamic В главе 3 вы узнали о ключевом слове var, которое позволяет объявлять локальную переменную таким образом, что ее действительный тип данных определяется начальным присваиванием (это называется неявной типизацией). Как только начальное присваивание выполнено, вы получаете строго типизированную переменную, и любая попытка присвоить ей несовместимое значение приведет к ошибке компиляции. Чтобы приступить к исследованию ключевого слова С# dynamic, создадим консольное приложение по имени DynamicKeyword. После этого поместим в класс Program показанный ниже метод и удостоверимся, что финальный оператор кода действительно инициирует ошибку во время компиляции, если убрать с него символы комментария.
Глава 18. Динамические типы и исполняющая среда динамического языка 649 static void ImplicitlyTypedVariable () { // а имеет тип List<int>. var a = new List<mt> () ; a.Add(90) ; // Это вызовет ошибку во время компиляции1 // а = "Hello"; } Использование неявной типизации просто потому, что она возможна, считается плохим стилем (если известно, что нужен тип List<int>, то его и следует указывать). Однако, как было показано в главе 13, неявная типизация очень полезна в сочетании с LINQ, поскольку многие запросы LINQ возвращают перечисления анонимных классов (через проекции), которые объявить явно в коде С# не получится. Тем не менее, даже в этих случаях неявно типизированная переменная на самом деле является строго типизированной. Как уже известно из главы 6, System.Object находится на вершине иерархии классов в .NET Framework и может представлять все, что угодно. После объявления переменной типа object получается строго типизированный элемент данных, однако то, на что он указывает в памяти, может отличаться в зависимости от присваивания ссылки. Для того чтобы получить доступ к членам объекта, на который установлена ссылка в памяти, необходимо выполнять явное приведение. Предположим, что есть простой класс по имени Person, в котором определены два автоматических свойства (FirstName и LastName), инкапсулирующие string. Теперь взгляните на следующий код: static void UseObjectVarible () { // Предположим, что есть класс по имени Person. object о = new Person () { FirstName = "Mike", LastName = "Larson" }; // Для получения доступа к свойствам Person необходимо приводить object к Person. Console .WriteLme ("Person ' s first name is {0}", ( (Person)o) .FirstName) ; } В версии .NET 4.0 язык С# стал поддерживать ключевое слово dynamic. На самом высоком уровне dynamic можно рассматривать как специализированную форму System.Object, в том смысле, что типу данных dynamic может быть присвоено любое значение. На первый взгляд, это порождает ужасную путаницу, поскольку теперь получается, что доступны три способа определения данных, внутренний тип которых явно не указан в коде. Например, следующий метод: static void PrintThreeStrings () { var si = "Greetings"; object s2 = "From"; dynamic s3 = "Minneapolis"; Console.WriteLme ("si is of type: {0}", si.GetType()); Console .WriteLme ("s2 is of type: {0}", s2.GetType()); Console.WriteLme ("s3 is of type: {0}", s3 . GetType ()) ; } будучи вызванным в Main(), выведет на консоль следующее: si is of type: System.String s2 is of type: System.String s3 is of type: System.String
650 Часть IV. Программирование с использованием сборок .NET Динамическую переменную от переменной, объявленной неявно или через ссылку System.Object, значительно отличает то, что она не является строго типизированной. Другими словами, динамические данные не типизированы статически. Для компилятора С# это выглядит так, что элемент данных, объявленный с ключевым словом dynamic, может получить какое угодно начальное значение, и на протяжении времени его существования это значение может быть заменено новым (и возможно, не связанным с первоначальным). Рассмотрим следующий метод и его результирующий вывод: static void ChangeDynamicDataType () i II Объявить одиночный элемент данных dynamic по имени t. dynamic t = "Hello!"; Console.WriteLine ("t is of type: {0}", t.GetType()); t = falser- Console. WriteLine ("t is of type: {0}", t.GetType ()); t = new List<mt>(); Console.WriteLine("t is of type: {0}", t.GetType()); } Вот как выглядит вывод: t is of type: System.String t is of type: System.Boolean t is of type: System.Collections.Generic.Listч1[System.Int32] Имейте в виду, ,что приведенный выше код успешно бы скомпилировался и дал идентичные результаты, если бы переменная t была объявлена с типом System.Object. Тем не менее, как вскоре будет показано, ключевое слово dynamic предоставляет много дополнительных возможностей. Вызов членов на динамически объявленных данных Теперь, учитывая, что тип данных dynamic может на лету принимать идентичность любого типа (как переменная типа System.Object), следующий вопрос, который наверняка возник, связан с вызовом членов на динамической переменной (свойств, методов, индексаторов, регистрации событий и т.п.). В отношении синтаксиса никаких отличий нет Нужно просто применить операцию точки к динамической переменной, указать общедоступный член и передать ему необходимые аргументы. Однако (и это очень важно) корректность указываемых членов компилятором не проверяется! Помните, что в отличие от переменной, объявленной как System.Object, динамические данные не являются статически типизированными. Вплоть до времени выполнения не известно, поддерживают ли вызываемые динамические данные указанный член, переданы ли корректные параметры, правильно ли указан член, и т.д. Поэтому, как бы странно это не выглядело, следующий метод скомпилируется без ошибок: static void InvokeMembersOnDynamicData () i dynamic textDatal = "Hello"; Console.WriteLine(textDatal.ToUpper()); // Здесь следовало ожидать ошибку компилятора1 //Но все компилируется нормально. Console.WriteLine(textDatal.toupper()); Console.WriteLine(textDatal.FooA0, "ее", DateTime.Now)); } Обратите внимание, что во втором вызове WriteLine () производится обращение к методу по имени toupper () на динамической переменной. Как видите, textDatal имеет тип string, и потому известно, что у этого типа нет метода с таким именем в
Глава 18. Динамические типы и исполняющая среда динамического языка 651 нижнем регистре. Более того, тип string определенно не имеет метода по имени Foo(), который принимает int, string и DataTime! Тем не менее, компилятор С# не о каких ошибках не сообщает Однако если вызвать этот метод в Main(), возникнет ошибка времени выполнения с примерно таким сообщением: Unhandled Exception : Microsoft. CSharp. RuntimeBmder . RuntimeBinderException : 'string' does not contain a definition for 'toupper' Необработанное исключение: Microsoft. CSharp. RuntimeBmder. RuntimeBinderException: 'string' не содержит определения 'toupper' Другое значительное отличие между вызовом членов на динамических и строго типизированных данных состоит в том, что после применения операции точки к элементу динамических данных средство IntelliSense в Visual Studio 2010 не активизируется. Вместо этого отображается следующее общее сообщение (рис. 18.1). | Program.cs* X | J:$GettingDynamic.Program • ^♦UseDynemicVarQ static void UseDynamicVar() { dynamic textDatal - "Hello"; Console.WriteLine(textDatal.ToUpper()); textDatal. | ! (dynamic expression) // You wolJ This operation will be resolved at runtii* // But they impiHf JUU f'lWf. -ц Console.WriteLine(textDatal.toupper()); i"oo%" :] « gj Рис. 18.1. Динамические данные не активизируют средство IntelliSense То, что средство IntelliSense недоступно с динамическими данными, имеет смысл. Однако это означает, что при наборе кода С# с такими переменными следует соблюдать исключительную осторожность. Любая опечатка или некорректный регистр символов в имени члена приведет к ошибке времени выполнения, а именно — к генерации экземпляра класса RuntimeBinderException. Роль сборки Microsoft.CSharp.dll Сразу же после создания нового проекта С# в Visual Studio 2010 автоматически получается комплект ссылок на новую сборку .NET 4.0 по имени Microsoft.CSharp.dll (в этом легко убедиться, заглянув в папку References (Ссылки) в проводнике решений). Эта очень маленькая библиотека определяет единственное пространство имен (Microsoft. CSharp.RuntimeBinder) с двумя классами (рис. 18.2). л {) Microsoft.CSharp.RuntimeBinder i> *t$ RuntimeBinderException t> *^J RuntimeBinderlntematCompilerException :• -Ot mscorlib I Assembly Microsoft.CSharp C:\Program Files (x86)\Reference Assemblies I. |\Microsoft\FrameworkVNF^rameworlc\v4-0 Рис. 18.2. C6opKaMicrosoft.CSharp.dll Как можно догадаться по их именам, оба класса представляют собой строго типизированные исключения. Более общий класс — RuntimeBinderException — представляет ошибку, которая будет сгенерирована при попытке вызова несуществующего члена на да-
652 Часть IV. Программирование с использованием сборок .NET намическом типе данных (как в случае методов toupper () и Foo ()). Та же ошибка будет инициирована, если будут указаны неверные данные параметров для существующего члена. Поскольку динамические данные столь изменчивы, каждый вызов члена на переменной, объявленной с ключевым словом dynamic, должен быть помещен в правильный блок try/catch, и предусмотрена соответствующая обработка ошибок. static void InvokeMembersOnDynamicData () { dynamic textDatal = "Hello"; try { Console.WriteLine(textDatal.ToUpper()); Console.WriteLine (textDatal.toupper() ); Console.WriteLine (textDatal .Foo A0, "ее", DateTime.Now)); } catch (Microsoft.CSharp.RuntlmeBinder.RuntlmeBinderException ex) { Console.WriteLine(ex.Message); } } Вызвав этот метод вновь, можно увидеть, что вызов ToUpper () (обратите внимание на регистр Т" и "U") работает корректно, однако на консоль выводится следующее сообщение об ошибке: HELLO 'string1 does not contain a definition for 'toupper' HELLO 'string' не содержит определения 'toupper ' Разумеется, процесс помещения всех динамических вызовов методов в блоки try/ catch довольно утомителен. Если вы тщательно следите за написанием кода и передачей параметров, то это делать не обязательно. Однако перехват исключений удобен, когда заранее не известно, будет ли член представлен в целевом типе. Область применения ключевого слова dynamic Вспомните, что неявно типизированные данные возможны только для локальных переменных в области определения члена. Ключевое слово var никогда не может использоваться в качестве возвращаемого значения, параметра или члена класса/структуры. Однако это не касается ключевого слова dynamic. Взгляните на следующее определение класса: class VeryDynamicClass { // Поле dynamic. private static dynamic myDynamicField; // Свойство dynamic. public dynamic DynamicProperty { get; set; } // Тип возврата dynamic и тип параметра dynamic. public dynamic DynamicMethod(dynamic dynamicParam) { // Локальная переменная dynamic. dynamic dynamicLocalVar = "Local variable"; int mylnt = 10; if (dynamicParam is int) { return dynamicLocalVar; }
Глава 18. Динамические типы и исполняющая среда динамического языка 653 else 1 return mylnt; } } } Теперь можно вызывать общедоступные члены, как ожидалось, однако, при оперировании с динамическими методами и свойствами нет полной уверенности в том, каким именно будет тип данных! По правде говоря, определение VeryDynamicClass может оказаться не особенно полезным в реальном приложении, но оно иллюстрирует область применения ключевого слова dynamic. Ограничения ключевого слова dynamic Хотя с использованием ключевого слова dynamic можно определить очень много вещей, с ним связаны свои ограничения. Хотя они не так уж существенны, имейте в виду, что элементы динамических данных не могут использовать лямбда-выражения или анонимные методы С# при вызове метода. Например, следующий код всегда приводит к ошибке, даже если целевой метод на самом деле принимает параметр-делегат, который, в свою очередь, принимает значение string и возвращает void: dynamic a = GetDynamicObject () ; // Ошибка! Методы на динамических данных не могут использовать лямбда-выражения1 a.Method(arg => Console.WriteLine (arg) ) ; Чтобы обойти это ограничение, понадобится работать с лежащим в основе делегатом напрямую, используя технику, описанную в главе 11 (анонимные методы и лямбда-выражения, и т.д.). Другое ограничение состоит в том, что динамический элемент данных не может воспринимать расширяющие методы (см. главу 12). К сожалению, это касается также всех расширяющих методов из API-интерфейсов LINQ. Поэтому переменная, объявленная с ключевым словом dynamic, имеет очень ограниченное применение в рамках LINQ to Objects и других технологий LINQ: dynamic a = GetDynamicObject () ; // Ошибка1 Динамические данные не могут найти расширяющий метод Select () ! var data = from d in a select d; Практическое применение ключевого слова dynamic Учитывая тот факт, что динамические данные не являются строго типизированными, не проверяются во время компиляции, не имеют возможности инициировать средство IntelliSense и не могут быть целью запроса LINQ, совершенно корректно предположить, что использование ключевого слова dynamic только потому, что оно существует — это очень плохая программистская практика. Однако в редких случаях ключевое слово dynamic может радикально сократить объем кода, который придется вводить вручную. В частности, при построении приложения .NET, которое интенсивно использует позднее связывание (через рефлексию), ключевое слово dynamic может сэкономить время на наборе кода. Точно также, при разработке приложения .NET, которое должно взаимодействовать с унаследованными библиотеками СОМ (такими как продукты Microsoft Office), можно значительно упростить код за счет применения ключевого слова dynamic. Как с любым "сокращением", прежде чем его применять, необходимо взвесить все "за" и "против". Применение ключевого слова dynamic — это компромисс между краткостью кода и безопасностью типов. Хотя С# в основе своей является строго типизированным языком, можно выбирать, стоит ли пользоваться динамическим поведением,
654 Часть IV. Программирование с использованием сборок .NET от вызова к вызову. Помните, что вы не обязаны применять ключевое слово dynamic. Всегда можно получить тот же конечный результат, написав альтернативный код вручную (обычно существенно большего объема). Исходный код. Проект DynamicKeyword доступен в подкаталоге Chapter 18. Роль исполняющей среды динамического языка (DLR) Теперь, когда прояснилась суть "динамических данных", давайте исследуем, как они обрабатываются. В версии .NET 4.0 общеязыковая исполняющая среда (Common Language Runtime — CLR) получила дополняющую среду времени выполнения, которая называется исполняющей средой динамического языка (Dynamic Language Runtime — DLR). Концепция "динамической исполняющей среды" определенно не нова. На самом деле ее много лет используют такие языки программирования, как Smalltalk, LISP, Ruby и Python. В основе своей динамическая исполняющая среда предоставляет динамическим языкам возможность обнаруживать типы целиком во время выполнения, без каких-либо проверок при компиляции. При наличии опыта работы со строго типизированными языками (включая С# без динамических типов), может показаться нежелательным само понятие такой исполняющей среды. В конце концов, обычно, когда только возможно, лучше получать ошибки во время компиляции, а не во время выполнения. Тем не менее, динамические языки и исполняющие среды предлагают ряд интересных возможностей, включая перечисленные ниже. • Исключительно гибкая кодовая база. Можно изменять код, не внося многочисленных модификаций в типы данных. • Очень простой способ взаимодействия с разнообразными типами объектов, построенными на разных платформах и языках программирования. • Способ добавления или удаления членов типа в памяти во время выполнения. Роль DLR состоит в том, чтобы позволить различным динамическим языка работать с исполняющей средой .NET и предоставлять им возможность взаимодействия с другим кодом .NET. Два популярных динамических языка, которые используют DLR — это IronPython и IronRuby. Эти языки живут в "динамической вселенной", где типы определяются исключительно во время выполнения. К тому же эти языки имеют доступ ко всему богатству библиотек базовых классов .NET Еще лучше то, что их кодовая база может взаимодействовать с С# (и наоборот), благодаря включению ключевого слова dynamic. Роль деревьев выражений Среда DLR использует деревья выражений для описания динамического вызова в нейтральных терминах. Например, когда DLR встречает код С#, подобный следующему: dynamic d = GetSomeData (); d.SuperMethodA2); то автоматически строит дерево выражения, которое, по сути, гласит: "Вызвать метод по имени SuperMethod на объекте d, передав 12 в качестве аргумента". Эта информация (формально называемая рабочей нагрузкой (payload)) затем передается корректному средству привязки времени выполнения, которое, опять-таки, может быть динамическим средством привязки С#, динамическим средством привязки IronPython или даже (как будет показано ниже) унаследованными объектами СОМ.
Глава 18. Динамические типы и исполняющая среда динамического языка 655 Отсюда запрос отображается на необходимую структуру вызовов для целевого объекта. Замечательным в деревьях выражений является то (помимо того факта, что вам не нужно создавать их вручную), что они позволяют нам написать фиксированный оператор кода С#, не заботясь о том, что собой представляет его реальная цель (объект СОМ, код IronPython или IronRuby, и т.п.). На рис. 18.3 иллюстрируется концепция деревьев выражений на наивысшем уровне. dynamic d = GetSomeData (); d.SuperMethodA2); Средство привязки COM Средство привязки IronRuby или IronPython Средство привязки .NET Дерево выражения Исполняющая среда динамического языка (DLR) NET Общеязыковая исполняющая среда (CLR) .NET Рис. 18.3. Деревья выражений фиксируют динамические вызовы в нейтральных терминах и обрабатываются средствами привязки Роль пространства имен System.Dynamic В версии .NET 4.0 к сборке System.Core.dll было добавлено пространство имен System.Dynamic. По правде говоря, шансы, что вам когда-либо придется непосредственно использовать типы из этого пространства имен, весьма невелики. Однако если вы — разработчик языка, который желает обеспечить своему динамическому языку возможность взаимодействия с DLR, пространство имен System.Dynamic пригодится для построения специального средства привязки времени выполнения. Подробные сведения о типах System.Dynamic можно найти в документации .NET Framework 4.0 SDK. Для практических нужд просто знайте, что это пространство имен предоставляет необходимую инфраструктуру, позволяющую обеспечить динамическим языкам взаимодействие с .NET. Динамический поиск в деревьях выражений во время выполнения Как уже объяснялось, среда DLR передает деревья выражений целевому объекту, однако на этот процесс оказывает влияние несколько факторов. Если динамический тип данных указывает в памяти на объект СОМ, то дерево выражения посылается низкоуровневому интерфейсу СОМ по имени IDispatch. Как вам может быть известно, этот интерфейс представляет собой способ, которым СОМ включает собственный набор динамических служб. Объекты СОМ, однако, могут использоваться в приложении .NET без применения DLR или ключевого слова С# dynamic. Однако это (как вы убедитесь) ведет к более сложному кодированию С#. Если динамические данные не указывают на объект СОМ, то дерево выражения может быть передано объекту, реализующему интерфейс IDynamicObject. Этот интерфейс используется "за кулисами", чтобы позволить такому языку, как IronRuby, принять дерево выражения DLR и отобразить его на специфику языка Ruby. Наконец, если динамические данные указывают на объект, который не является объектом СОМ и не реализует интерфейс IDynamic Object, то это — нормальный,
656 Часть IV. Программирование с использованием сборок .NET повседневный объект .NET. В этом случае дерево выражения передается на обработку средству привязки исполняющей среды С#. Процесс отображения дерева выражений на специфику .NET включает участие служб рефлексии. Как только дерево выражения обработано определенным средством привязки, динамические данные разрешаются в реальный тип данных в памяти, после чего вызывается корректный метод со всеми необходимыми параметрами. Теперь давайте рассмотрим несколько практических применений DLR, начав с упрощения вызовов позднего связывания .NET Упрощение вызовов позднего связывания с использованием динамических типов Одним из случаев, когда имеет смысл использовать ключевое слово dynamic, может быть работа со службами рефлексии, а именно — выполнение вызовов методов позднего связывания. В главе 15 приводилось несколько примеров, когда такого рода вызовы методов могут быть очень полезны — чаще всего при построении расширяемого приложения. Там было показано, как использовать метод Activator.CreatelnstanceO для создания экземпляра object, о котором ничего не известно во время компиляции (помимо его отображаемого имени). Затем с помощью типов из пространства имен System.Reflection можно обращаться к членам через механизм позднего связывания. Вспомните следующий пример из главы 15. static void CreateUsingLateBinding(Assembly asm) { try { // Получение метаданных типа Minivan. Type miniVan = asm.GetType("CarLibrary.MiniVan"); // Создание экземпляра Minivan на лету. object ob] = Activator.Createlnstance(miniVan); // Получение информации о TurboBoost. Methodlnfo mi = miniVan.GetMethod("TurboBoost"); // Вызов метода (null означает отсутствие параметров). mi.Invoke(obj, null); } catch (Exception ex) { Console.WriteLine(ex.Message); } } Хотя этот код работает, как и ожидалось, нельзя не отметить его громоздкость. Здесь приходится вручную создавать класс Methodlnfo, вручную запрашивать метаданные и т.д. Ниже приведена версия этого метода, в которой используется ключевое слово dynamic и DLR: static void InvokeMethodWithDynamicKeyword(Assembly asm) { try { // Получение метаданных типа Minivan. Type miniVan = asm. GetType ( "CarLibrary .MiniVan11) ; // Создание экземпляра Minivan на лету и вызов метода, dynamic obj = Activator.Createlnstance(miniVan); ob].TurboBoost (); }
Глава 18. Динамические типы и исполняющая среда динамического языка 657 catch (Exception ex) { Console.WriteLine(ex.Message); } } Объявив переменную obj с ключевым словом dynamic, всю рутинную работу, связанную с рефлексией, вы возлагаете на среду DLR! Использование ключевого слова dynamic для передачи аргументов Польза от DLR становится еще более очевидной, когда нужно выполнять связанные вызовы методов, принимающих параметры. Когда используются "длинные" вызовы рефлексии, аргументы приходится упаковывать в массив элементов object, который передается методу Invoke () класса Methodlnfo. Чтобы проиллюстрировать это на примере, создадим новое консольное приложение С# по имени LateBindingWithDynamic. Добавим к текущему решению проект библиотеки классов (используя пункт меню File^Add^New Project (Файл1^Добавить1^Новый проект)) и назовите его MathLibrary. Переименуйте начальный класс Classl.cs в проекте MathLibrary на SimplaMath.cs и реализуйте класс, как показано ниже: public class SimpleMath { public int Add(int x, int y) { return x + y; } } Скомпилировав c6opKyMathLibrary.dll, поместите ее копию в папку bin\Debug проекта LateBindingWithDynamic (щелкнув на кнопке Show All Files (Показать все файлы) для каждого проекта в Solution Explorer, можно просто перетащить файл между проектами). После этого окно Solution Explorer должно выглядеть примерно как на рис. 18.4. ^ Solution LateBindingWrthDynamic' B projects) л .J] LateBindingWithDynamic t> al Properties > 03 References л £> bin л ''£% Debug _j LateBindingWithDynamic.exe j LateBindingWithDynamic,pdb Lj LateBindingWithDynamic.vshost.exe j LateBindingWithDynamic.vshostexe.manifest MathUbrary.dll I ;> Cj obj cj3 Program.cs л *yj3 MathLibrary i> Ш Properties :> _yi References л rj bin л ;.__/ Debug J MathUbrary.dll J MarthLibrary.pdb t> Hj Release _j obj cjy SimpleMath.es Рис. 18.4. Проект LateBindingWithDynamic имеет приватную копию сборки MathLibrary.dll
658 Часть IV. Программирование с использованием сборок .NET На заметку! Помните, что главная цель позднего связывания — позволить приложению создать объект, который не имеет записи MANIFEST. Именно поэтому нужно вручную скопировать сборку MathLibrary.dll в выходную папку консольного проекта, а не устанавливать ссылку на сборку через Visual Studio. Теперь импортируем пространство имен System.Reflection а файл Program.cs проекта консольного приложения. Добавим следующий метод в класс Program, который вызывает метод Add(), используя типичные вызовы API-интерфейса рефлексии: private static void AddWithReflection () { Assembly asm = Assembly.Load("MathLibrary"); try { // Получение метаданных типа SimpleMath. Type math = asm.GetType("MathLibrary.SimpleMath"); // Создание экземпляра SimpleMath на лету, object obj = Activator.Createlnstance(math); // Получение информации для метода Add. Methodlnfo mi = math.GetMethod("Add"); // Вызов метода (с параметрами). object[] args = { 10, 70 }; Console.WriteLine("Result is: {0}", mi.Invoke(obj, args)); // вывод результата } catch (Exception ex) { Console.WriteLine(ex.Message); } } Ниже показано, как предыдущая логика метода упрощается за счет использования ключевого слова dynamic: private static void AddWithDynamic () { Assembly asm = Assembly.Load("MathLibrary"); try { // Получение метаданных типа SimpleMath. Type math = asm.GetType("MathLibrary.SimpleMath"); // Создание экземпляра SimpleMath на лету. dynamic obj = Activator.Createlnstance(math); Console.WriteLine("Result is: {0}", obj.AddA0, 70)); // вывод результата } catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex) { Console.WriteLine(ex.Message); } } Выглядит неплохо! Вызов обоих методов в Main() дает идентичный вывод. Однако при использовании ключевого слова dynamic сокращается объем работ по кодированию. Для динамически определенных данных больше не нужно вручную упаковывать аргументы в массив object, запрашивать метаданные сборки и иметь дело с прочими деталями подобного рода. Исходный код. Проект LastBindingWithDynamic доступен в подкаталоге Chapter 18.
Глава 18. Динамические типы и исполняющая среда динамического языка 659 Упрощение взаимодействия с СОМ посредством динамических данных Теперь давайте рассмотрим другое полезное применение ключевого слова dynamic — в контексте проекта взаимодействия с СОМ. Если нет опыта разработки для СОМ, то имейте в виду, что следующий пример, в котором компилируется библиотека СОМ, содержит метаданные, подобно библиотеке .NET. Тем не менее, ее формат совершенно отличается. По этой причине если программа .NET нуждается в использовании объекта СОМ, то первое, что нужно сделать — это сгенерировать то, что называется "сборкой взаимодействия" (interop assembly), используя Visual Studio 2010. Делается это довольно легко. Просто откройте диалоговое окно Add Reference (Добавить ссылку), перейдите на вкладку СОМ и найдите библиотеку СОМ, которую необходимо использовать (рис. 18.5). I .NET j COM i Projects Его лse j Recent Component Name { AccessibilityCplAdmin 1.0 Type ... Active DS Type Library AgControl 3.0 Type Library AP Client 1.0 HelpPane Type Libr.. AP Client 1.0 Type Library AppIdPcficyEngineApi 1.0 Type ,.. 1 Application Host Administration... I Assistance Platform Client .1.0 Da... ATL 2.0 Type Library ATLContactPicker 1.0 Type Library AxBrowse 4 '" Type 1.0 1.0 3 0 10 1.0 10 1.0 1,0 1.0 1.0 1 j Lib Version ...-,-- Path . mv,ti ,дтг ■,- C\W«MfcwtfS-.ste C:\Windows\SysW c:\Prcgrarn Files (x C:\Windows\Sy5te C:\Windows\Helpl C:\Windows\Syste C:\Windows\systei C:\Wind owsXSyste C:\Windowc\SysW C:\Program Files (> C:\Program Files {> • Ш : J Рис. 18.5. На вкладке COM диалогового окна Add Reference отображаются все зарегистрированные на машине библиотеки СОМ В случае выбора библиотеки СОМ среда Visual Studio 2010 отреагирует генерацией совершенно нового описания .NET для метаданных СОМ. Формально они называются "сборками взаимодействия" и не содержат никакого кода реализации, помимо самого минимума, который помогает транслировать события СОМ в события .NET. Однако эти сборки взаимодействия очень полезны в том, что защищают кодовую базу .NET от сложностей внутреннего механизма СОМ. В коде С# можно напрямую работать со сборкой взаимодействия, позволяя CLR (и DLR, если используется ключевое слово dynamic) автоматически отображать типы данных .NET на типы СОМ и наоборот. "За кулисами" данные маршализуются между приложениями .NET и СОМ с использованием оболочки исполняющей среды (Runtime Callable Wrapper — RCW), которая на самом деле является динамически сгенерированным прокси. RCW маршализует и трансформирует типы данных .NET в типы СОМ, изменяя счетчик ссылок СОМ-объекта и отображая все возвращаемые СОМ значения на их эквиваленты в .NET. На рис. 18.6 показана общая картина взаимодействия .NET с СОМ. «i Add Referem
660 Часть IV. Программирование с использованием сборок .NET Неуправляемый СОМ-объект Рис. 18.6. Программы .NET взаимодействуют с объектами СОМ, используя прокси под названием RCW Роль первичных сборок взаимодействия Многие поставщики библиотек СОМ (таких как библиотеки Microsoft COM, обеспечивающие доступ к объектной модели продуктов Microsoft Office), предоставляют "официальную" сборку взаимодействия, которая называется первичной сборкой взаимодействия (primary interop assembly — PIA). Сборки PIA — это оптимизированные сборки взаимодействия, которые делают яснее (и возможно, расширяют) код, обычно генерируемый при ссылке на библиотеку СОМ через диалоговое окно Add Reference. Сборки PIA обычно перечисляются на вкладке .NET диалогового окна Add Reference, подобно базовым библиотекам .NET. Фактически, если вы ссылаетесь на библиотеку СОМ из вкладки СОМ диалогового окна Add Reference, то Visual Studio не генерирует новой библиотеки взаимодействия, как делает это обычно, а вместо этого использует предоставленную сборку PIA. На рис. 18.7 показана сборка PIA объектной модели Microsoft Office Excel, которая будет использоваться в следующем примере. Component Name Microsoft, mshtml Microsoft.Office.lnterop.Access [ Microsoft.OfficeJnteroD.Excel Microsoft. Off ice Jnterop.FrontPage Microsoft.Office.Interop.FrontPageEditor Microsoft.OfficeJnterop.Graph Microsoft. OfficeJnterop.InfoPath Microsoft.OfficeJnterop.InfoPath.SemiTrust Microsoft.Office.Interop.InfoPath.Xml Microsoft.Office.Interop.MSProject Microsoft.OfficeJnterop.Outtook «' I __J Version ж 7.0.3300.0 11.0.0.0 11.0,0,0 11.0.0.0 11.0.0.0 11.0.0.0 11.0.0.0 11.0.0.0 11.0.0.0 11.0.0.0 11.0.0.0 OK Cancel Рис. 18.7. Сборки PIA перечислены на вкладке .NET диалогового окна Add Reference
Глава 18. Динамические типы и исполняющая среда динамического языка 661 Встраивание метаданных взаимодействия До появления .NET 4.0, когда приложение С# использовало библиотеку СОМ (в виде сборки PIA или нет), нужно было обеспечить наличие на клиентской машине копии сборки взаимодействия. Это увеличивало размер установочного пакета приложения, к тому же в сценарии установки должно было проверяться существование сборки PIA и в случае ее отсутствия — установка копии в GAC. Однако в .NET 4.0 теперь можно встраивать данные взаимодействия непосредственно в скомпилированное приложение .NET. В этом случае поставлять копию сборки взаимодействия вместе с приложением .NET необязательно, поскольку все необходимые метаданные взаимодействия жестко встраиваются в приложение .NET. По умолчанию, после выбора библиотеки COM (PIA или нет) в диалоговом окне Add Reference интегрированная среда разработки автоматически устанавливает свойство Embed Interop Types (Встраивать типы взаимодействия) библиотеки в True. Чтобы увидеть эту установку, необходимо выбрать ссылаемую сборку взаимодействия в папке References (Ссылки) окна Solution Explorer и открыть ее окно свойств (рис. 18.8). [Properties 1 Microsof t.Of fice.Interop (Name) Aliases Copy Local Culture Description Bi!H EnitWpMiwni File Type 1 Identity Path Reserved Runtime Version Specific Version Excel Reference Properties 1 Embed Interop Types 1 Indicates whether types defined 1 target assembry. Microsoft.Off ice.Interop.Exce! global False H True Assembry Microsoft.Off iceJnterop.Excel C:\Program Files (xS6)\Microsoft True vl 1.4322 True * nxl irr 1 Щ 1 n this assembry will be embedded into the Рис. 18.8. Среда .NET 4.0 позволяет встраивать часть используемых сборок взаимодействия в создаваемую сборку .NET Компилятор С# включит только те части библиотеки взаимодействия, которые действительно используются. Таким образом, даже если реальная библиотека взаимодействия содержит .NET-описания сотен СОМ-объектов, будут получены определения только подмножества, которое действительно используется в написанном коде С#. Помимо сокращения размеров приложения, поставляемого клиенту, также упрощается процесс установки, поскольку не понадобится копировать лишние сборки PIA на целевую машину. Общие сложности взаимодействия с СОМ До выхода версии .NET 4.0 при написании кода С#, имеющего дело с библиотекой СОМ (через сборку взаимодействия), неизбежно возникало множество сложностей. Например, многие С ОМ-библиотеки определяют методы, принимающие необязательные аргументы, что вплоть до нынешнего выпуска в С# не поддерживалось. Это требовало указания значения Type.Missing для каждого появления необязательного аргумента. Например, если метод СОМ принимал пять аргументов, и все они были необязательны, приходилось писать следующий код С#, чтобы принять значения по умолчанию:
662 Часть IV. Программирование с использованием сборок .NET myComObj.SomeMethod( Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing) ; К счастью, в .NET 4.0 теперь можно писать упрощенный код, учитывая, что значения Type. Mis sing будут вставлены во время компиляции, если не указано реальное значение: myComObj.SomeMethod() ; В связи с этим стоит отметить, что многие методы СОМ предлагают поддержку именованных аргументов, которые, как было показано в главе 4, позволяют передавать значения членам в любом порядке. Поскольку в .NET 4.0 язык С# поддерживает и это средство, можно очень просто "пропускать" множество необязательных аргументов и устанавливать только те, которые важны в данном случае. Другая сложность взаимодействия с СОМ была связана с тем фактом, что многие методы СОМ спроектированы так, чтобы принимать и возвращать очень специфический тип данных по имени Variant. Во многом подобный ключевому слову С# dynamic, типу данных Variant может быть присвоен любой тип данных СОМ на лету (строка, интерфейсная ссылка, числовое значение и т.п.). До появления ключевого слова dynamic передача или прием элементов данных типа Variant требовал значительных ухищрений, обычно связанных с многочисленными операциями приведения. С появлением .NET 4.0 и Visual Studio 2010, кода свойство Embed Interop Types устанавливается в True, все типы Variant из СОМ автоматически отображаются на динамические данные. Это не только сокращает потребность в излишних операциях приведения при работе с типом данных Variant, но также еще более скрывает некоторые сложности, присущие СОМ, вроде работы с индексаторами СОМ. Для того чтобы продемонстрировать упрощение взаимодействия с СОМ за счет совместной работы необязательных аргументов, именованных аргументов и ключевого слово dynamic в С#, построим приложение, в котором используется объектная модель Microsoft Office. При работе с этим примером вы получите шанс применить новые средства, а также обойтись без них, и затем сравнить объем работ в обоих случаях. На заметку! В предыдущих изданиях этой книги детально рассматривалась работа с унаследованными объектами СОМ в проектах .NET с использованием пространства имен System. InteropServices. В нынешнем издании это не описано, поскольку ключевое слово dynamic обеспечивает гораздо более простое взаимодействие с объектами СОМ. Исчерпывающие сведения о взаимодействии с объектами СОМ в строго типизированной (длинной) нотации могут быть найдены в документации .NET Framework 4.0 SDK. Взаимодействие с СОМ с использованием средств языка С# 4.0 Предположим, что имеется приложение Windows Forms с графическим интерфейсом пользователя (ExportDataToOfficeApp), главная форма которого определяет элемент управления DataGridView по имени dataGridCars. В той же форме находятся два элемента управления Button, один из которых обеспечивает открытие диалогового окна для вставки новой строки данных в сетку, а другой отвечает за экспорт данных сетки в электронную таблицу Excel. Учитывая тот факт, что приложение Excel предоставляет программную модель через СОМ, к ней можно привязаться с использованием уровня взаимодействия. На рис. 18.9 показан завершенный графический интерфейс пользователя.
Глава 18. Динамические типы и исполняющая среда динамического языка 663 Add New Entry to Inventory Рис. 18.9. Графический интерфейс пользователя для примера взаимодействия с СОМ Сетка будет заполняться некоторыми начальными данными в обработчике события Load формы (класс Саг, используемый в качестве параметра типа для обобщенного List<T> — это простой класс в проекте, имеющий свойства Color, Make и PetName): public partial class MainForm : Form { List<Car> carsInStock = null; public MainForm () { InitializeComponent(); } private void MainForm_Load(object sender, EventArgs e) { carsInStock = new List<Car> { new Car {Color="Green", Make=,,VW", PetName=,,Mary" }, new Car {Color="Red", Make="Saab", PetName="Mel" }, new Car {Color=,,BlackM , Make="FordM , PetName="Hank" }, new Car {Color="YellowM, Make="BMW", PetName^'Dav/ie"} }; UpdateGridO ; } private void UpdateGridO { // Сбросить источник данных. dataGridCars.DataSource = null; dataGridCars.DataSource = carsInStock; } } В обработчике события Click кнопки Add New Entry to Inventory (Добавить новую запись в инвентарную ведомость) открывается специальное диалоговое окно, которое позволяет пользователю ввести новые данные для объекта Саг; после щелчка на кнопке О К данные добавляются в сетку. Код этого диалогового окна в книге не показан, поэтому за подробностями обращайтесь к доступному решению. Если хотите повторить пример самостоятельно, включите файлы NewCarDialog.es, IlewCarDialog.designer.cs и NewCarDialog.resx в проект (их можно найти в составе кода примеров для этой главы). Затем реализуйте обработчик щелчка на кнопке Add New Entry to Inventory, как показано ниже:
664 Часть IV. Программирование с использованием сборок .NET private void btnAddNewCar_Click(object sender, EventArgs e) { NewCarDialog d = new NewCarDialog(); if (d.ShowDialogO == DialogResult .OK) { // Добавить новый автомобиль в список. carsInStock.Add(d.theCar); UpdateGridO ; } } Ядром этого примера является обработчик события Click для кнопки Export Current Inventory to Excel (Экспортировать текущую инвентарную ведомость в Excel). На вкладке .NET диалогового окна Add Reference добавьте ссылку на первичную сборку взаимодействия Microsoft.Office.Interop.Excel.dll (как было показано ранее на рис. 18.7). Добавьте приведенный ниже псевдоним пространства имен в главный файл кода формы. Имейте в виду, что при взаимодействии с библиотеками СОМ псевдоним определять не обязательно. Однако, поступив так, вы получите удобный квалификатор для всех импортированных объектов СОМ, что очень пригодится, если некоторые из этих СОМ-объектов будут иметь имена, конфликтующие с типами .NET. // Создать псевдоним для объектной модели Excel, using Excel = Microsoft.Office.Interop.Excel; Реализуйте следующий обработчик события Click, чтобы он вызывал вспомогательную функцию по имени ExportToExcel(): private void btnExportToExcel_Click(object sender, EventArgs e) { ExportToExcel(carsInStock); } Поскольку библиотека COM была импортирована в Visual Studio 2010, сборка PIA автоматически сконфигурирована так, что используемые метаданные будут включены в приложение .NET (вспомните роль свойства Embed Interop Types). Таким образом, все СОМ-типы Variant будут реализованы как типы данных dynamic. Более того, поскольку код пишется на С# 4.0, можно использовать необязательные и именованные аргументы. С учетом всего сказанного вот как будет выглядеть реализация ExportToExcel(): static void ExportToExcel(List<Car> carsInStock) { // Загрузить Excel, затем создать новую пустую рабочую книгу. Excel.Application excelApp = new Excel.Application(); excelApp.Workbooks.Add(); // В этом примере используется единственная рабочий лист. Excel._Worksheet worksheet = excelApp.ActiveSheet; // Установить заголовки столбцов в ячейках. worksheet.Cells [1, "А"] = "Make"; worksheet.Cells[1, "В"] = "Color"; worksheet.Cells [1, "C"] = "Pet Name"; // Отобразить все данные в List<Car> на ячейки электронной таблицы. int row = 1; foreach (Car c in carsInStock) { row++; worksheet.Cells [row, "A"] = c.Make; worksheet.Cells [row, "B"] = c.Color; worksheet.Cells [row, "C"] = c.PetName; }
Глава 18. Динамические типы и исполняющая среда динамического языка 665 // Придадить симпатичный вид табличным данным. worksheet.Range["Al"].AutoFormat( Excel.XIRangeAutoFormat.xlRangeAutoFormatClassic2); // Сохранить файл, выйти из Excel и отобразить сообщение пользователю. worksheet.SaveAs(string.Format(@"{0}\lnventory .xlsx11, Environment.CurrentDirectory) ); excelApp.Quit(); MessageBox.Show("The Inventory.xslx file has been saved to your app folder", "Export complete1"); // файл Inventory.xslx сохранен в папке приложения 1 Метод начинается с загрузки приложения Excel в память, однако на рабочем столе компьютера оно не покажется. В данном приложении интересует только использование объектной модели Excel. Если же необходимо отобразить пользовательский интерфейс Excel, дополните метод следующей строкой кода: static void ExportToExcel(List<Car> carsInStock) { // Загрузить Excel, затем создать новую пустую рабочую книгу. Excel.Application excelApp = new Excel.Application (); // Сделать приложение Excel видимым. excelApp.Visible = true; } После создания пустого рабочего листа к нему добавляются три столбца, названные по именам свойств класса Саг. После этого ячейки заполняются данными List<Car> и файл сохраняется под жестко закодированным именем Inventory.xlsx. Если теперь запустить приложение, добавить несколько записей и экспортировать их в Excel, в папке bin\Debug приложения Windows Forms появится файл Inventory.xlsx, который можно открыть в приложении Excel (рис. 18.10). p^N Щ *? - С* » Inventory .xlsx - —* Home Insert Page Layout Formulas rd** Л Calibri -11 - S ш ш --J 4J В / П ' А' а' Ж Ш Ш Ш Microsoft Excel Data Review View Team General - /L ^Insert ' $ * % f j 3* Delete - . пя Styles ,.«., Тб8 i% - ^Format- H X V _ Я У z * fr- 2* 2 VW Green Mary 3 Saab Red Mel 4 ^'ord Black Hank 5 BMW Yellow Davie | 6 Saab Sliver Melvtn 7 8 С н < > м Sheetl Sheet2! / "shecitTT'tJ, Ready Рис. 18.10. Экспортированные данные в файле Excel
666 Часть IV. Программирование с использованием сборок .NET Взаимодействие с СОМ без использования средств языка С# 4.0 Если теперь выбрать сборку Microsoft.Office.Interop.Excel.dll в Solution Explorer и установить свойство Embed Interop Type в False, появятся сообщения об ошибках компиляции, поскольку СОМ-данные Variant будут трактоваться не как динамические данные, а как переменные System.Object. Это потребует добавления в ExportToExcelO нескольких операций приведения. Кроме того, если проект скомпилировать в версии Visual Studio 2008, утратятся преимущества необязательных/именованных параметров, и в этом случае понадобится явно помечать все пропущенные аргументы. Ниже показана версия метода ExportToExcelO для ранних версий С#. static void ExportToExcel2008(List<Car> carsInStock) { Excel.Application excelApp = new Excel.Application(); // Нужно пометить пропущенные параметры! excelApp.Workbooks.Add(Type.Missing); // Нужно привести Object к _Worksheet! Excel._Worksheet worksheet = (Excel._Worksheet)excelApp.ActiveSheet; // Нужно привести каждый Object к объекту Range, // затем вызвать низкоуровневое свойство Value2' ((Excel.Range)excelApp.Cells[1, "A"]).Value2 = "Make"; ((Excel.Range)excelApp.Cells [1, "B"]).Value2 = "Color"; ((Excel.Range)exc3lApp.Cells[l, "C"]).Value2 = "Pet Name"; int row = 1; foreach (Car с in carsInStock) { row++; // Нужно привести каждый Object к объекту Range, //и вызвать низкоуровневое свойство Value2! ( (Excel.Pange)worksheet.Cells[row, "A"]).Value2 = c.Make; ((Excel.Range)worksheet.Cells[row, "B"]).Value2 = c.Color; ((Excel.Range)worksheet.Cells[row, "C"]).Value2 = c.PetName; } // Нужно вызвать метод get_Range и укаэаать все пропущенные аргументы1 excelApp.get_Range ("Al", Type.Missing) .AutcFormat( Excel.XIPangeAutoFormat.xlRangeAutoFormatClassic2, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing); // Нужно указать все пропущенные аргументы* worksheet.SaveAs(string.Format(@"{0}\Inventory.xlsx", Environment.CurrentDirectory), Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing); excelApp.Quit(); MessageBox.Show("The Inventory.xslx file has been saved to your app folder", "Export complete1"); // файл Inventory.xs]а сохранен в папке приложения } Хотя конечный результат идентичен, очевидно, что данная версия метода намного более многословна. К тому же, поскольку ранние версии С# (точнее, предшествовавшие .NET 4.0) не позволяют встраивать данные взаимодействия СОМ, обнаружится, что выходная папка теперь содержит локальные копии множества сборок взаимодействия, которые должны будут поставлены на машину конечного пользователя (рис. 18.11).
Глава 18. Динамические типы и исполняющая среда динамического языка 667 £3 Solution ExportDataToOfficeApp' 0. project) л .J] ExportDataToOfficeApp jj Properties E» .^ References j 2^ bin л \ry Debug _j ExportDataToOfficeApp.exe ■ j ExportDataToOfficeApp.pdb j ExportDataToOfficeApp.vshost.exe j ExportDataToOfficeApp.vshost.exe.manrfest j Inventory.xlsx ■ Microscft.Gffice.Interop.Efccel.xml x Microsoft.7be.Interop.dll [Л officcdlli 1 rjj office.*mI j !> Pj Release > Cj obj CJ^ Car.cs j> £§] MainForm.cs t> j£gj NewCarDialog.es =jjj Program.cs Рис. 18.11. Если данные взаимодействия не встроены, потребуется поставлять автономные сборки взаимодействия На этом рассмотрение ключевого слова С# dynamic и среды DLR завершено. Наверняка вы смогли оценить, насколько новые средства .NET 4.0 могут упростить решение сложных задач программирования, и (что возможно, более важно) поняли сопутствующие компромиссы. Выбор в пользу динамических данных приводит к утере безопасности типов, поэтому код становится уязвимым для гораздо большего числа ошибок времени выполнения. Исходный код. Проект ExportDataToOfficeApp доступен в подкаталоге Chapter 18. Резюме Ключевое слово dynamic в С# 4.0 позволяет определять данные, истинная идентичность которых не известна вплоть до времени выполнения. При работе новой исполняющей среды динамического языка (DLR) автоматически создаваемое "дерево выражения" передается соответствующему средству привязки динамического языка, причем рабочие данные будут распакованы и отправлены корректному члену объекта. За счет использования динамических данных и DLR многие сложные задачи программирования С# могут быть радикально упрощены, а особенно — включение библиотек СОМ в приложения .NET. Кроме того, как было показано в этой главе, .NET 4.0 предлагает ряд дальнейших упрощений взаимодействия с СОМ (которые не имеют отношения к динамическим данным) — встраивание данных взаимодействия СОМ в разрабатываемые приложения, а также необязательные и именованные аргументы. Хотя все эти средства могут упростить код, не забывайте, что динамические данные существенно снижают безопасность кода С# в отношении типов и открывают путь для ошибок времени выполнения. Поэтому тщательно взвешивайте все "за" и "против" использования динамических данных в проектах С# и соответствующим образом тестируйте их.
ЧАСТЬ V Введение в библиотеки базовых классов .NET В этой части... Глава 19. Многопоточность и параллельное программирование Глава 20. Файловый ввод-вывод и сериализация объектов Глава 21. ADO.NET, часть I: подключенный уровень Глава 22. ADO.NET, часть II: автономный уровень Глава 23. ADO.NET Часть III: Entity Framework Глава 24. Введение в LINQ to XML Глава 25. Введение в Windows Communication Foundation Глава 26. Введение в Windows Workflow Foundation 4.0
ГЛАВА 19 Многопоточность и параллельное программирование Вряд ли многим нравится работать с приложением, которое притормаживает во время выполнения. Более того, никому не понравится, когда запуск некоторой задачи в приложении (возможно, по щелчку на элементе панели инструментов) снижает "отзывчивость" других частей приложения. До появления нынешней версии .NET построение приложении, способных выполнять несколько задач, требовало написания очень сложного кода и применения API-интерфейсов многопоточности Windows. К счастью, платформа .NET предоставила множество способов построения программного обеспечения, которое может решать очень сложные задачи по уникальным путям выполнения, с минимальными усилиями со стороны разработчика. Эта глава начинается с обращения к уже известному типу делегата .NET, чтобы исследовать его внутреннюю поддержку асинхронных вызовов методов. Как вы увидите, эта техника позволяет вызывать метод во вторичном потоке выполнения, без необходимости самостоятельно создавать и конфигурировать поток. Затем будет представлено пространство имен System.Threading. Вы ознакомитесь с многочисленными типами (Thread, ThreadStart и т.п.), которые позволяют легко создавать дополнительные потоки выполнения. Кроме того, вы узнаете о различных примитивах синхронизации, предоставляемых .NET Framework, которые обеспечивают разделение общих данных несколькими потоками в неизменчивой манере. В конце главы будет представлена совершенно новая библиотека .NET Task Parallel Library (TPL) и технология PLINQ (Parallel LINQ). Эти API-интерфейсы предоставляют для программирования набор высокоуровневых типов, которые отлично справляются с деталями управления потоками. Отношения между процессом, доменом приложения, контекстом и потоком В главе 16 поток (thread) был определен как путь выполнения внутри исполняемого приложения. Хотя многие приложения .NET могут успешно и продуктивно работать, будучи однопоточными, первичный поток сборки (запускаемый CLR при выполнении MainO) может создавать вторичные потоки для выполнения дополнительных единиц работы. За счет реализации дополнительных потоков можно строить более отзывчивые (но не обязательно быстрее выполняемые на одноядерных машинах) приложения.
Глава 19. Многопоточность и параллельное программирование 671 На заметку! В наши дни очень многие компьютеры имеют более одного процессора (или, по крайней мере, гиперпотоковые одноядерные процессоры). Не запуская несколько потоков, использовать на полную мощность многоядерные машины не удастся. Пространство имен System.Threading содержит различные типы, позволяющие создавать многопоточные приложения. Пожалуй, главным среди них является класс Thread, поскольку он представляет отдельный поток. Чтобы программно получить ссылку на поток, выполняемый конкретным его экземпляром, просто вызовите статическое свойство Thread.CurrentThread: static void ExtractExecutingThread() { // Получить поток, выполняющий данный метод. Thread currThread = Thread.CurrentThread; } На платформе .NET не существует прямого соответствия "один к одному" между доменами приложений (AppDomain) и потоками. Фактически определенный AppDomain может иметь несколько потоков, выполняющихся в каждый конкретный момент времени. Более того, конкретный поток не привязан к одному домену приложений на протяжении своего времени существования. Потоки могут пересекать границы доменов приложений, когда это вздумается планировщику Windows и CLR. Хотя активные потоки могут пересекать границы AppDomain, каждый поток в каждый конкретный момент времени может выполняться только внутри одного домена приложений (другими словами, невозможно, чтобы один поток работал в более чем одном домене приложений сразу). Чтобы программно получить доступ к AppDomain, в котором работает текущий поток, вызовите статический метод Thread.GetDomain(): static void ExtractAppDomainHostingThread() { // Получить AppDomain, в котором работает текущий поток. AppDomain ad = Thread.GetDomain(); } Единственный поток также в любой момент может быть перемещен в определенный контекст, и он может перемещаться в пределах нового контекста по прихоти CLR. Для получения текущего контекста, в котором выполняется поток, используйте статическое свойство Thread.CurrentContext (которое возвращает объект System.Runtime. Remoting.Contexts .Context): static void ExtractCurrentThreadContext () { // Получить контекст, в котором работает текущий поток. Context ctx = Thread.CurrentContext; } Еще раз: за перемещение потоков между доменами приложений и контекстами отвечает CLR. Как разработчик .NET, вы всегда остаетесь в счастливом неведении относительно того, когда завершается каждый конкретный поток (или куда именно он будет помещен после перемещения). Тем не менее, полезно знать о различных способах получения лежащих в основе примитивов. Проблема параллелизма Один из многих болезненных аспектов многопоточного программирования связан с ограниченным контролем над использованием потоков операционной системой или CLR. Например, написав блок кода, который создает новый поток выполнения, нельзя
672 Часть V. Введение в библиотеки базовых классов .NET гарантировать, что этот поток запустится немедленно. Вместо этого данный код лишь просит операционную систему запустить поток, как только это будет возможно (обычно, когда планировщик потоков доберется до него). Более того, учитывая, что потоки могут перемещаться между границами приложений и контекстов, когда это нужно CLR, вы должны представлять, какие аспекты приложения являются изменчивыми в потоках (т.е. речь идет о многопоточном доступе) и какие операции — атомарными (изменчивые в потоках операции опасны). Чтобы проиллюстрировать проблему, предположим, что поток вызывает метод специфического объекта. Теперь представьте, что этот поток приостановлен планировщиком потока, чтобы позволить другому потоку обратиться к тому же методу того же объекта. Если исходный поток еще не завершил свою операцию, второй входящий поток может увидеть объект в частично модифицированном состоянии. И здесь второй поток, по сути, читает фиктивные данные, что определенно может привести к очень странным (и трудно обнаруживаемым) ошибкам, которые еще более трудно воспроизвести и отладить. С другой стороны, атомарные операции всегда безопасны в многопоточной среде. К сожалению, очень мало операций в библиотеках базовых классов .NET являются гарантированно атомарными. Даже операция присваивания переменной-члену не является атомарной! Если в документации .NET Framework 4.0 SDK специально не сказано, что операция является атомарной, ее следует считать изменчивой в потоках и предпринимать соответствующие меры предосторожности. Роль синхронизации потоков Сейчас уже должно быть ясно, что домены многопоточных приложений сами по себе довольно изменчивы, поскольку множество потоков могут оперировать общими разделенными ресурсами (более или менее) одновременно. Чтобы защитить ресурсы приложений от возможного повреждения, разработчики .NET должны использовать поточные примитивы (такие как блокировки, мониторы и атрибут [Synchronization]) для контроля доступа среди выполняющихся потоков. Хотя платформа .NET не может полностью скрыть сложности, связанные с построением надежных многопоточных приложений, этот процесс все же значительно упрощен. Используя типы, определенные внутри пространства имен System.Threading, и библиотеку .NET 4.0 Tksk Parallel Library (TPL), можно порождать дополнительные потоки с минимальными усилиями. Аналогично, когда наступает момент для блокировки разделяемых элементов данных, вы найдете для этого подходящие типы, которые предлагают ту же функциональность, что и примитивы многопоточности Windows API (на основе намного более ясной объектной модели). Прежде чем углубиться в пространство имен System.Threading и библиотеку TPL, важно отметить один удобный способ включения потоков в приложение. При рассмотрении делегата .NET (см. главу 11) упоминалось, что все делегаты обладают способностью вызывать члены асинхронно. Это главное преимущество платформы .NET, учитывая, что одна из наиболее частых причин создания разработчиками потоков — вызов методов в неблокирующей (т.е. асинхронной) манере. Краткий обзор делегатов .NET Вспомните, что тип делегата .NET — это по существу безопасный в отношении типов, объектно-ориентированный указатель на функцию.
Глава 19. Многопоточность и параллельное программирование 673 На объявление делегата .NET компилятор С# отвечает построением запечатанного класса, который наследуется от System.MulticastDelegate (который, в свою очередь, унаследован от System.Delegate). Эти базовые классы предоставляют каждому делегату возможность поддерживать список адресов методов, которые могут быть вызваны позднее. Рассмотрим делегат BinaryOp, впервые показанный в главе 11: // Тип делегата С#. public delegate int BinaryOp(int x, int y) ; Исходя из определения, BinaryOp может указывать на любой метод, принимающий два целых числа (по значению) в качестве аргументов и возвращающий целое число. После компиляции сборка с определением делегата будет содержать полноценное определение класса, сгенерированного динамически при построении проекта, на основе объявления делегата. В случае BinaryOp этот класс более или менее похож на следующий (записан в псевдокоде): public sealed class BinaryOp : System.MulticastDelegate { public BinaryOp(object target, uint functionAddress); public void Invoke(int x, int y) ; public IAsyncResult Beginlnvoke(int x, int y, AsyncCallback cb, object state); public int Endlnvoke(IAsyncResult result); } Сгенерированный метод Invoke () используется для вызова метода, поддерживаемого объектом делегата в синхронном режиме. Поэтому вызывающий поток (такой как первичный поток приложения) должен будет ждать, пока не завершится вызов делегата. Также вспомните, что в С# метод Invoke () не нужно вызывать в коде напрямую — он может быть инициирован неявно, "за кулисами", при применении "нормального" синтаксиса вызова метода. Рассмотрим следующий пример консольного приложения (SyncDelegateReview), которое вызывает статический метод Add() в синхронном (т.е. блокирующем) режиме (не забудьте импортировать пространство имен System.Threading, поскольку будет вызываться метод Thread.Sleep()). namespace SyncDelegateReview { public delegate int BinaryOp (int x, int y) ; class Program { static void Main(string[] args) { Console. WriteLine ("***** Synch Delegate Review *****••); // Вывести идентификатор выполняющегося потока. Console.WriteLine("Main () invoked on thread {0}.", Thread.CurrentThread.ManagedThreadld) ; // Вызвать Add() в синхронном режиме. BinaryOp b = new BinaryOp(Add); // Можно было бы также написать b.InvokeA0, 10) ; int answer = bA0, 10) ; // Эти строки не будут выполняться, пока не завершится метод Add(). Console.WriteLine("Doing more work in Main()!"); Console.WriteLine(0 + 10 is {0}.", answer); Console.ReadLine(); }
674- Часть V. Введение в библиотеки базовых классов .NET static int Add(int x, int y) { // Вывести идентификатор выполняющегося потока. Console.WriteLine("Add() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadld); // Пауза для моделирования длительной операции. Thread.SleepE000) ; return x + у; } } } Внутри метода Add() вызывается статический метод Thread.Sleep() для приостановки вызывающего потока примерно на пять секунд, чтобы имитировать длительную задачу. Учитывая, что метод Add() вызывается в синхронном режиме, метод Main() не выведет результат операции до тех пор, пока не завершится Add(). Обратите внимание, что метод Man() получает доступ к текущему потоку (через Thread.CurrentThread) и печатает идентификатор потока, взяв его из свойства ManagedThreadld. Та же логика повторяется в статическом методе Add(). Как и можно было ожидать, учитывая, что вся работа в этом приложении выполняется исключительно в первичном потоке, обнаруживается, что на консоль выводится одно и то же значение идентификатора: ***** Synch Delegate Review ***** Main () invoked on thread 1. Add () invoked on thread 1. Doing more work in Main() ! 10 + 10 is 20. Press any key to continue . . . Запустив эту программу, вы должны заметить пятисекундную задержку перед тем, как выполнится последний вызов Console.WriteLine() в Main(). Хотя многие (если не большинство) методов могут вызываться синхронно без болезненных последствий, на самом деле при необходимости делегаты .NET позволяют выполнять назначенные им методы асинхронно. Исходный код. Проект SyncDelegateReview доступен в подкаталоге Chapter 19. Асинхронная природа делегатов Если для вас тема многопоточности в новинку, может возникнуть вопрос, что собой представляет асинхронный вызов метода? Как известно, некоторые программные операции требуют времени для своего завершения. Хотя приведенный выше метод Add () иллюстративен по природе, предположим, что строится однопоточное приложение, вызывающее метод на удаленном объекте, который выполняет длительный запрос к базе данных, загружает большой документ либо выводит 500 строк текста во внешний файл. На протяжении выполнения этих операций приложение "замирает" на некоторый период времени. И пока эта задача не будет завершена, все поведение программы (вроде активизации пунктов меню, щелчков в панели инструментов или вывода на консоль) будет заморожено. Отсюда вопрос: как заставить делегат выполнить назначенный ему метод в отдельном потоке выполнения, чтобы имитировать "одновременное" выполнение многочисленных задач? К счастью, каждый тип делегата .NET автоматически оснащен такой возможностью. Вдобавок, вы не обязаны углубляться в детали типов пространства имен System. Threading, чтобы делать это (хотя эти сущности могут успешно работать "рука об руку").
Глава 19. Многопоточность и параллельное программирование 675 Методы Be gin Invoke () и EndInvoke() Когда компилятор С# обрабатывает ключевое слово delegate, динамически сгенерированный класс определяет два метода с именами BeginlnvokeO и EndInvoke(). Учитывая определение делегата BinaryOp, эти методы прототипированы следующим образом: public sealed class BinaryOp : System.MulticastDelegate { // Используется для асинхронного вызова метода, public IAsyncResult Beginlnvoke(int x, int y, AsyncCallback cb, object state); // Используется для получения возвращаемого значения вызванного метода, public int Endlnvoke(IAsyncResult result); } Первый стек параметров, переданный BeginlnvokeO, будет основан на формате делегата С# (в случае BinaryOp — два целых). Последними двумя аргументами всегда будут System.AsyncCallback и System.Object. Чуть позже вы узнаете о назначении этих параметров, а пока передадим null в каждом из них. Также обратите внимание, что возвращаемое значение Endlnvoke() является целым, как тип возврата BinaryOp, хотя параметр этого метода имеет тип IAsyncResult. Интерфейс System.IAsyncResult Метод BeginlnvokeO всегда возвращает объект, реализующий интерфейс IAsyncResult, в то время как Endlnvoke() ожидает единственный параметр совместимого с IAsyncResult типа. Совместимый с IAsyncResult объект, возвращаемый из BeginlnvokeO — это в основном связывающий механизм, который позволяет вызывающему потоку получить позже результат вызова асинхронного метода через Endlnvoke (). Интерфейс IAsyncResult (находящийся в пространстве имен System) определен следующим образом: public interface IAsyncResult { object AsyncState { get; } WaitHandle AsyncWaitHandle { get; } bool CompletedSynchronously { get; } bool IsCompleted { get; } } В простейшем случае можно избежать непосредственного вызова этих членов. Все, что потребуется сделать — это кэшировать las у ncResu It-совместимый объект, возвращенный BeginlnvokeO, и передать его Endlnvoke0, имея готовность к получению результата вызова метода. Как будет показано, члены IAsyncResuIt-совместимого объекта можно вызывать, когда требуется "большая вовлеченность" в процесс получения возвращаемого методом значения. На заметку! Метод, возвращающий void, можно просто вызвать асинхронно и забыть. В таких случаях нет необходимости кэшировать IAsyncResuIt-совместимый объект или вызывать Endlnvoke(), поскольку нет возвращаемого значения, которое нужно получить.
676 Часть V. Введение в библиотеки базовых классов .NET Асинхронный вызов метода Чтобы заставить делегат BinaryOp вызывать Add() асинхронно, модифицируем логику предыдущего проекта (можно добавить код к имеющемуся проекту, однако в коде примеров для данной главы имеется новое консольное приложение по имени AsyncDelegate). Обновите предыдущий метод Main() следующим образом: static void Main(string[] args) { Console.WriteLine ("***** Async Delegate Invocation *****"); // Вывести идентификатор выполняющегося потока. Console.WriteLine("Main() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadld); // Вызвать Add() во вторичном потоке. BinaryOp b = new BinaryOp(Add) ; IAsyncResult lftAR = b.BeginlnvokeA0, 10, null, null); // Выполнить другую работу в первичном потоке... Console.WriteLine("Doing more work in Main()!"); // Получить результат метода Add() по готовности, int answer = b.Endlnvoke(iftAR); Console.WriteLine (0 + 10 is {0}.", answer); Console.ReadLine(); } После запуска этого приложения на консоль выводятся два уникальных идентификатора потоков, поскольку в текущем домене приложения работает несколько потоков: ***** Async Delegate Invocation ***** Main () invoked on thread 1. Doing more work in Main() ! Add() invoked on thread 3. 10 + 10 is 20. В дополнение к уникальным идентификаторам при выполнении этого приложения вы заметите, что сообщение "Doing more work in Main()!" выводится практически мгновенно, в то время как вторичный поток продолжит свое дело. Синхронизация вызывающего потока Хорошо поразмыслив о текущей реализации Main(), можно понять, что время, прошедшее между вызовами BeginlnvokeO и Endlnvoke(), очевидно меньше пяти секунд. Поэтому, как только сообщение "Doing more work in Main()!" будет выведено на консоль, вызывающий поток блокируется и ожидает, пока вторичный поток завершится, чтобы получить результат вызова метода Add(). Поэтому на самом деле производится еще один синхронный вызов: static void Main(string[] args) { BinaryOp b = new BinaryOp(Add); // После следующего оператора вызывающий поток блокируется, // пока не будет завершен BeginlnvokeO . IAsyncResult iftAR = b.BeginlnvokeA0, 10, null, null); // Этот вызов займет намного меньше 5 секунд! Console.WriteLine("Doing more work in Main()!"); int answer = b.Endlnvoke(iftAR); }
Глава 19. Многопоточность и параллельное программирование 677 Очевидно, что асинхронные делегаты утратили бы свою привлекательность, если бы вызывающий поток при определенных обстоятельствах мог блокироваться. Чтобы позволить вызывающему потоку определять, когда асинхронно вызванный метод завершит свою работу, в интерфейсе IAsyncResult предусмотрено свойство IsCompleted. С его помощью вызывающий поток может определять, действительно ли асинхронный вызов был завершен, перед вызовом EndInvoke(). Если метод еще не завершился, то IsCompleted вернет false, и вызывающий поток может продолжать заниматься своей работой. Если Incompleted вернет true, то вызывающий поток может получить результат в наименее блокирующей манере. Рассмотрим следующую модификацию метода Main(): static void Main(string [ ] args) { BinaryOp b = new BinaryOp(Add); IAsyncResult iftAR = b.BeginlnvokeA0, 10, null, null); // Это сообщение продолжит печататься, пока не завершится метод Add(). while('lftAR.IsCompleted) { Console.WriteLine("Doing more work in Main()' "); Thread.Sleep A000); } // Теперь известно, что метод Add() завершен, int answer = b.Endlnvoke(lftAR); } Здесь запускается цикл, который продолжает выполнять оператор Console. WriteLine(), пока не завершится вторичный поток. Когда это произойдет, можно получить результат метода Add(), уже зная точно, что он закончил работать. Вызов Thread.SleepA000) не обязателен для корректной работы этого конкретного приложения; однако, заставляя первичный поток ожидать примерно секунду на каждой итерации, мы предотвращаем вывод слишком большого количества одного и того же сообщения на консоль. Ниже показан вывод (он может слегка отличаться, в зависимости от скорости машины и времени запуска потока): * ***** Async Delegate Invocation ***** Main () invoked on thread 1. Doing more work in Main() ' Add() invoked on thread 3. Doing more work in Main() ! Doing more work in Main() ' Doing more work in Main() ' Doing more work in Main() ' Doing more work in Main() ! 10 + 10 is 20. В дополнение к свойству IsCompleted интерфейс IAsyncResuklt предлагает свойство As у nc Wait Handle для реализации более гибкой логики ожидания. Это свойство возвращает экземпляр типа WaitHandle, который предоставляет метод по имени WaitOne(). Преимущество WaitHandle.WaitOneO в том, что можно задавать максимальное время ожидания. Если это время истекает, то WaitOneO возвращает false. Взгляните на следующее изменение цикла while, в котором теперь не используется вызов Thread.Sleep(): while (!lftAR.AsyncWaitHandle.WaitOne A000, true)) { Console.WriteLine("Doing more work in Main()!"); }
678 Часть V. Введение в библиотеки базовых классов .NET Хотя эти свойства IAsyncResult предоставляют способ синхронизации вызывающего потока, все же это не самый эффективный подход. Во многих отношениях свойство IsCompleted подобно надоедливому менеджеру, который постоянно спрашивает: "Вы еще это не сделали?". К счастью, делегаты предлагают множество дополнительных (и более элегантных) приемов получения результата из метода, который был вызван асинхронно. Исходный код. Проект AsyncDelegate доступен в подкаталоге Chapter 19. Роль делегата AsyncCallback Вместо опроса делегата о том, завершился ли асинхронно вызванный метод, было бы более эффективно заставить вторичный поток информировать вызывающий поток о завершении выполнения задания. Чтобы включить такое поведение, необходимо передать экземпляр делегата System.AsyncCallback в качестве параметра методу BeginlnvokeOinp сих пор этот параметр был равен null. Если передается объект AsyncCallback, делегат автоматически вызовет указанный метод по завершении асинхронного вызова. На заметку! Метод обратного вызова будет вызван во вторичном потоке, а не в первичном. Это имеет важное последствие для потоков с графическим интерфейсом пользователя (WPF или Windows Forms), поскольку элементы управления привязаны к потоку, который их создал, и могут управляться только им. Далее в этой главе, при рассмотрении библиотеки TPL, будут показаны некоторые примеры работы потоков из графического интерфейса. Как и любой делегат, AsyncCallback может вызывать только методы, соответствующие определенному шаблону, который в данном случае требует единственного параметра IAsyncResult и ничего не возвращает: // Целевые методы AsyncCallback должны иметь следующую сигнатуру, void MyAsyncCallbackMethod(IAsyncResult ltfAR) Предположим, что имеется другое консольное приложение (AsyncCallbackDelegate), использующее делегат BinaryOp. Однако на этот раз мы не будем опрашивать делегат, чтобы выяснить, когда завершится метод Add (). Вместо этого определим статический метод по имени AddCompleteO для получения уведомления о том, что асинхронный вызов завершен. Также в этом примере используется булевское статическое поле уровня класса, которое служит для удержания в активном состоянии первичного потока Main(), пока не завершится вторичный поток. На заметку! Использование булевской переменной в данном примере, строго говоря, является небезопасным для потоков, поскольку к его значению имеют доступ два разных потока. В данном примере это допустимо; тем не менее, запомните в качестве хорошего эмпирического правила: вы должны обеспечивать блокировку данных, разделяемых между несколькими потоками. Далее в главе будет показано, как это делается. namespace AsyncCallbackDelegate { public delegate int BinaryOp(int x, int y) ; class Program { private static bool isDone = false; static void Main(string [ ] args) {
Глава 19. Многопоточность и параллельное программирование 679 Console.WriteLine("***** AsyncCallbackDelegate Example *****"); Console.WriteLine ("Main () invoked on thread {0}.", Thread.CurrentThread.ManagedThreadld); BinaryOp b = new BinaryOp(Add); IAsyncResult iftAR = b.BeginlnvokeA0, 10, new AsyncCallback(AddComplete) , null); // Предположим, здесь выполняется какая-то другая работа... while (lisDone) { Thread.SleepA000); Console.WriteLine("Working...."); } Console.ReadLine(); } static int Add(int x, int y) { Console.WriteLine ("Add() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadld); Thread.SleepE000); return x + y; } static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadld); Console.WriteLine("Your addition is complete"); isDone = true; } } } Статический метод AddCompleteO будет вызван делегатом AsyncCallback по завершении метода Add (). Запустив эту программу, можно убедиться, что именно вторичный поток вызывает AddCompleteO: ***** AsyncCallbackDelegate Example ***** Main () invoked on thread 1. Add() invoked on thread 3. Working.... Working... . Working... . Working.... Working.... AddCompleteO invoked on thread 3. Your addition is complete Как и в других примерах настоящей главы, вывод может несколько отличаться. Фактически, может появиться только одно финальное сообщение "Working..." после завершения AddCompleteO. Это просто следствие односекундной задержки в Main0. Роль класса AsyncResult Сейчас метод AddCompleteO не выводит действительный результат операции (сложения двух чисел). Причина с том, что цель делегата AsycnCallback (в данном примере — AsyncResult0) не имеет доступа к исходному делегату BinaryOp, созданному в контексте Main 0, и потому вызывать EndInvoke() из AdCompleteO нельзя!
680 Часть V. Введение в библиотеки базовых классов .NET Хотя можно было бы просто объявить переменную BinaryOp, как статический член класса, чтобы позволить обоим методам обращаться к одному и тому же объекту, более элегантное решение предусматривает применение входного параметра IAsyncResult. Входной параметр IAsyncResult, передаваемый цели делегата AsyncCallback — это на самом деле экземпляр класса AsyncResult (обратите внимание на отсутствие префикса I), определенного в пространстве имен System.Runtime.Remoting. Messaging. Статическое свойство AsyncDelegate возвращает ссылку на первоначальный асинхронный делегат, который был создан где-то в другом месте. Поэтому для получения ссылки на объект делегата BinaryOp, размещенный в Main(), нужно привести экземпляр System.Object, возвращенный свойством AsyncDelegate, к BinaryOp. И здесь можно запустить EndlnvokeO, как и ожидалось: // Не забудьте импортировать System.Runtime.Remoting.Messaging! static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadld); Console.WriteLine("Your addition is complete"); // Теперь получить результат. AsyncResult ar = (AsyncResult)itfAR; BinaryOp b = (BinaryOp)ar.AsyncDelegate; Console.WriteLine(0 + 10 is {0}.", b.Endlnvoke(itfAR)); lsDone = true; } Передача и прием специальных данных состояния финальный аспект асинхронных делегатов, который должен быть учтен — это последний аргумент метода BeginlnvokeO (который до сих пор был равен null). Этот параметр позволяет передавать дополнительную информацию о состоянии методу обратного вызова от первичного потока. Поскольку этот аргумент прототипирован как System.Object, его можно передавать в любом типе данных, если только метод обратного вызова знает, чего в нем ожидать. Для демонстрации предположим, что первичный поток желает передать специальное текстовое сообщение методу AddComplete (): static void Main(string[] args) { IAsyncResult lftAR = b.BeginlnvokeA0, 10, new AsyncCallback(AddComplete), "Main() thanks you for adding these numbers."); } Для получения этих данных в контексте AddComplete () используется свойство AsyncState входящего параметра IAsyncResult. Обратите внимание, что здесь понадобится явное приведение, поэтому первичный и вторичный потоки должны согласовать тип, возвращаемый AsyncState. static void AddComplete(IAsyncResult itfAR) { // Получить информационный объект и привести его к string, string msg = (string)itfAR.AsyncState; Console.WriteLine(msg); lsDone = true; }
Глава 19. Многопоточность и параллельное программирование 681 Ниже показан вывод последней итерации примера: ***** AsyncCallbackDelegate Example ***** Main () invoked on thread 1. Add() invoked on thread 3. Working... Working. Working. Working... Working.. , AddComplete() invoked on thread 3. Your addition is complete 10 + 10 is 20. Main() thanks you for adding these numbers. Working.... Теперь, когда известно, как использовать делегат .NET для автоматического завершения вторичного потока, обрабатывающего асинхронный вызов метода, давайте обратимся непосредственно к взаимодействию с потоками, используя пространство имен System.Threading. Исходный код. Проект AsyncCallbackDelegate доступен в подкаталоге Chapter 19. Пространство имен System.Threading Пространство имен System.Threading в .NET предлагает множество типов, которые позволяют непосредственно конструировать многопоточные приложения. В дополнение к типам, позволяющим взаимодействовать с определенным потоком CLR, в этом пространстве имен определены типы, которые открывают доступ к пулу потоков, обслуживаемому CLR, простому (не связанному с графическим интерфейсом) классу Timer и многочисленным типам, используемым для предоставления синхронизированного доступа к разделенным ресурсам. В табл. 19.1 перечислены некоторые основные члены этого пространства имен (подробные сведения ищите в документации .NET Framework 4.0 SDK). Таблица 19.1. Некоторые члены пространства имен System.Threading Тип Назначение Interlocked Monitor Mutex Parameter!zedThreadStart Semaphore Thread Этот тип предоставляет атомарные операции для переменных, разделяемых между несколькими потоками Этот тип обеспечивает синхронизацию потоковых объектов, используя блокировки и ожидания/сигналы. Ключевое слово С# lock использует "за кулисами" объект Monitor Примитив синхронизации, который может быть использован для синхронизации между границами доменов приложений Этот делегат позволяет потоку вызывать методы, принимающие произвольное количество аргументов Этот тип позволяет ограничить количество потоков, которые могут иметь доступ к ресурсу или к определенному типу ресурсов одновременно Этот тип представляет поток, выполняемый в CLR. Используя этот тип, можно порождать дополнительные потоки в исходном домене приложений
682 Часть V. Введение в библиотеки базовых классов .NET Окончание табл. 19.1 Тип Назначение ThreadPool ThreadPriority ThreadStart ThreadState Timer TimerCallback Этот тип позволяет взаимодействовать с поддерживаемым CLR пулом потоков внутри заданного процесса Это перечисление представляет уровень приоритета потока (Highest, Normal и т.п.) Этот делегат позволяет указать метод для вызова в заданном потоке. В отличие от делегата ParametrizedThreadStart, цель ThreadStart всегда должна иметь одинаковый прототип Это перечисление специфицирует допустимые состояния потока (Running, Aborted и т.п.) Этот тип предоставляет механизм выполнения метода через указанные интервалы Этот тип делегата используется в сочетании с типами Timer Класс System.Threading.Thread Класс Thread является самым элементарным из всех типов пространства имен System.Threading. Этот класс представляет объектно-ориентированную оболочку вокруг заданного пути выполнения внутри определенного AppDomaln. Этот тип также определяет набор методов (как статических, так и уровня экземпляра), которые позволяют создавать новые потоки внутри текущего AppDomain, а также приостанавливать, останавливать и уничтожать определенный поток. Список основных статических членов приведен в табл. 19.2. Таблица 19.2. Основные статические члены типа Thread Статический член Назначение CurrentContext CurrentThread GetDomainO GetDomainlDO SleepO Это свойство только для чтения возвращает контекст, в котором в данный момент выполняется поток Это свойство только для чтения возвращает ссылку на текущий выполняемый поток Этот метод возвращает ссылку на текущий AppDomain или идентификатор этого домена, в котором выполняется текущий поток Этот метод приостанавливает текущий поток на заданное время Класс Thread также поддерживает несколько методов уровня экземпляра, часть из которых описана в табл. 19.3. На заметку! Отмена или приостановка активного потока обычно считается плохой идеей. Когда вы делаете это, есть шанс (хотя и небольшой), что поток может допустить "утечку" своей рабочей нагрузки, когда его беспокоят или прерывают.
Глава 19. Многопоточность и параллельное программирование 683 Таблица 19.3. Некоторые члены экземпляра типа Thread Член уровня экземпляра Назначение Is Alive Возвращает булевское значение, указывающее на то, запущен ли поток (и еще не прерван и не отменен) IsBackground Получает или устанавливает значение, указывающее, является ли данный поток "фоновым" (подробнее объясняется далее) Name Позволяет вам установить дружественное текстовое имя потока Priority Получает или устанавливает приоритет потока, который может принимать значение из перечисления ThreadPriority ThreadState Получает состояние данного потока, которому может быть присвоено значение из перечисления ThreadState Abort () Инструктирует CLR прервать поток, как только это будет возможно Interrupt () Прерывает (т.е. приостанавливает) текущий поток на заданный период ожидания Join () Блокирует вызывающий поток до тех пор, пока указанный поток (тот, в котором вызван JoinO) не завершится Resume () Возобновляет ранее приостановленный поток Start () Инструктирует CLR запустить поток как можно скорее Suspend () Приостанавливает поток. Если поток уже приостановлен, вызов Suspend() не дает эффекта Получение статистики о текущем потоке Вспомните, что точка входа исполняемой сборки (т.е. метод Main(J) запускается в первичном потоке выполнения. Чтобы проиллюстрировать базовое применение типа Thread, предположим, что имеется новое консольное приложение по имени ThreadStats. Как известно, статическое свойство Thread.CurrentThread извлекает объект Thread, представляющий текущий выполняющийся поток. После получения текущего потока можно вывести разнообразную статистику о нем. // Не забудьте импортировать пространство имен System.Threading. static void Main(string [ ] args) { Console.WriteLine("***** Primary Thread stats *****\n"); // Получить имя текущего потока. Thread primaryThread = Thread.CurrentThread; primaryThread.Name = "ThePnmaryThread"; // Показать детали включающего домена приложений и контекста. Console.WriteLine("Name of current AppDomain: {0}", Thread.GetDomain().FriendlyName); Console.WriteLine("ID of current Context: {0}", Thread.CurrentContext.ContextID); // Вывести некоторую статистику о текущем потоке. Console.WriteLine("Thread Name: {0}", primaryThread.Name); // имя потока Console.WriteLine("Has thread started?: {0}", primaryThread.IsAlive); // запущен ли поток Console.WriteLine("Priority Level: {0}", primaryThread.Priority); // приоритет потока Console.WriteLine("Thread State: {0}", primaryThread.ThreadState); // состояние потока Console.ReadLine();
684 Часть V. Введение в библиотеки базовых классов .NET Ниже показан вывод этого кода: ***** Primary Thread stats ***** Name of current AppDomain: ThreadStats.exe ID of current Context: 0 Thread Name: ThePrimaryThread Has thread started?: True Priority Level: Normal Thread State: Running Свойство Name Хотя этот код более-менее очевиден, обратите внимание, что класс Thread поддерживает свойство по имени Name. Если не установить его значение явно, то Name вернет пустую строку. Присваивание дружественного имени конкретному объекту Thread может значительно упростить отладку. Во время сеанса отладки в Visual Studio 2010 можно открыть окно Threads (Потоки), выбрав пункт меню Debug^Windows^Threads (Отладка1^ Окна1^ Потоки). Как показано на рис. 19.1, это окно позволяет быстро идентифицировать поток, который нужно диагностировать. Рис. 19.1. Отладка потока в Visual Studio 2010 Свойство Priority Обратите внимание, что в типе Thread определено свойство по имени Priority. По умолчанию все потоки имеют уровень приоритета Normal. Однако это можно изменить в любой момент жизненного цикла потока, используя свойство Thread.Priority и связанное с ним перечисление System.Threading.ThreadPriority: public enum ThreadPriority { Lowest, BelowNormal, Normal, // Значение по умолчанию. AboveNormal, Highest } При установке уровня приоритета потока равным значению, отличному от принятого по умолчанию (ThreadPriority.Normal), следует иметь в виду, что это не предоставляет прямого контроля над тем, как планировщик потоков будет переключать потоки между собой. На самом деле уровень приоритета потока предоставляет CLR подсказку относительно важности действий потока. Таким образом, поток с уровнем приоритета ThreadPriority. Highest не обязательно гарантированно получит наивысший приоритет.
Глава 19. Многопоточность и параллельное программирование 685 Опять-таки, если планировщик потоков занят решением определенной задачи (например, синхронизацией объекта, переключением потоков или их перемещением), то уровень приоритета, скорее всего, будет соответствующим образом изменен. Однако, как бы то ни было, среда CLR прочитает эти значения и проинструктирует планировщик потоков о том, как наилучшим образом выделять порции времени. Потоки с идентичным уровнем приоритета должны получать одинаковый объем времени на выполнение своей работы. В большинстве случаев редко требуется (если вообще требуется) напрямую менять уровень приоритета потока. Теоретически можно повысить уровень приоритета для множества потоков, тем самым предотвращая выполнение низкоприоритетных потоков на их запрошенных уровнях (поэтому будьте осторожны). Исходный код. Проект ThreadStats доступен в подкаталоге Chapter 19. Программное создание вторичных потоков При программном создании дополнительных потоков для выполнения некоторой единицы работы необходимо следовать строго регламентированному процессу. 1. Создать метод, который будет точкой входа для нового потока. 2. Создать новый делегат ParametrizedThreadStart (или ThreadStart), передав конструктору адрес метода, определенного на шаге 1. 3. Создать объект Thread, передав в качестве аргумента конструктора ParametrizedThreadStart/ThreadStart. 4. Установить начальные характеристики потока (имя, приоритет и т.п.). - 5. Вызвать метод Thread.Start(). Это запустит поток на методе, который указан делегатом, созданным на шаге 2, как только это будет возможно. Согласно шагу 2, можно использовать два разных типа делегатов для "указания" метода, который выполнит вторичный поток. Делегат ThreadStart относится к пространству имен System.Threading, начиная с .NET 1.0, и он может указывать на любой метод, не принимающий аргументов и ничего не возвращающий. Этот делегат пригодится, когда метод предназначен просто для запуска в фоновом режиме, без какого-либо дальнейшего взаимодействия. Очевидное ограничение ThreadStart связано с невозможность передавать ему параметры для обработки. Тем не менее, тип делегата ParametrizedThreadStart позволяет передать единственный параметр типа System.Object. Учитывая, что с помощью System.Object представляется все, что угодно, можно передать любое количество параметров через специальный класс или структуру. Однако имейте в виду, что делегат ParametrizedThreadStart может указывать только на методы, возвращающие void. Работа с делегатом ThreadStart Чтобы проиллюстрировать процесс построения многопоточного приложения (а также его пользу), предположим, что есть консольное приложение (SimpleMultiThreadApp), которое позволяет конечному пользователю выбирать, будет приложение выполнять свою работу в единственном первичном потоке либо распределит рабочую нагрузку на два отдельных потока выполнения. После импортирования пространства имен System.Threading следующий шаг заключается в определении метода для выполнения работы (возможного) вторичного потока. Чтобы сосредоточиться на механизме построения многопоточных программ, этот
686 Часть V. Введение в библиотеки базовых классов .NET метод будет просто выводить на консоль последовательность чисел, приостанавливаясь примерно на 2 секунды на каждом шаге. Ниже показано полное определение класса Printer: public class Printer { public void PrintNumbers() { // Вывести информацию о потоке. Console.WriteLine("-> {0} is executing PrintNumbers()", Thread.CurrentThread.Name) ; // Вывести числа. Console.Write("Your numbers: "); for(int i = 0; i < 10; i++) { Console.Write ("{0}, ", i) ; Thread.SleepB000); } Console.WriteLine (); } } Внутри Main() сначала пользователю предлагается решить, сколько потоков применять для выполнения работы приложения: один или два. Если пользователь запрашивает один поток, нужно просто вызвать метод PrintNumbers () внутри первичного потока. Если же пользователь отдает предпочтение двум потокам, необходимо создать делегат Threads tart, указывающий на PrintNumbers (), передать объект делегата конструктору нового объекта Thread и вызвать метод Start (), информируя среду CLR, что этот поток готов к обработке. Для начала установим ссылку на сборку Systern.Windows .Forms .dll (и импортируем пространство имен System.Windows.Forms) и отобразим сообщение в Main(), используя MessageBox.Show() (причина станет понятной после запуска программы). Ниже показана полная реализация Main(): static void Main(string[] args) { Console.WriteLine ("***** The Amazing Thread App *****\n"); Console.Write ("Do you want [1] or [2] threads? "); string threadCount = Console.ReadLine(); // Назначить имя текущему потоку. Thread primaryThread = Thread.CurrentThread; primaryThread.Name = "Primary"; // Вывести информацию о потоке. Console.WriteLine("-> {0} is executing Main()", Thread.CurrentThread.Name); // Создать рабочий класс. Printer p = new Printer(); switch(threadCount) { case ": // Создать поток. Thread backgroundThread = new Thread(new ThreadStart(p.PrintNumbers)); backgroundThread.Name = "Secondary"; backgroundThread.Start(); break;
Глава 19. Многопоточность и параллельное программирование 687 case ": р.PrintNumbers (); breaks- default: Console.WriteLine("I don't know what you want. goto case "; , .you get 1 thread.11); // Выполнить некоторую дополнительную обработку. MessageBox.Show("I'm busy'", "Work on main thread..."); Console.ReadLine(); } Если теперь запустить эту программу в одном потоке, обнаружится, что финальное окно сообщения не отображает сообщения, пока вся последовательность чисел не будет выведена на консоль. Поскольку после вывода каждого числа установлена пауза примерно в 2 секунды, это создаст не слишком приятное впечатление у пользователя. Однако в случае выбора двух потоков окно сообщения отображается немедленно, поскольку для вывода чисел на консоль выделен отдельный уникальный объект Thread (рис. 19.2). - C:\Windom\sy5tem32\cmd.e **** The Amazing Thread App ***** |do you want [1] or [2] threads? 2 > Primary is executing Main() > Secondary is executing PrintNumbers() [Your numbers: 0, 1. 2. 3. 4. П I'm busy! J Рис. 19.2. Многопоточные приложения позволяют создавать более отзывчивые пользовательские интерфейсы Исходный код. Проект SimpleMultiThreadApp доступен в подкаталоге Chapter 19. Работа с делегатом ParametrizedThreadStart Вспомните, что делегат ThreadStart может указывать только на методы, возвращающие void и не имеющие аргументов. Во многих случаях это подходит, но если нужно передать данные методу, выполняющемуся во вторичном потоке, то придется использовать тип делегата ParametrizedThreadStart. Для начала создадим новое консольное приложение по имени AddWithThreads и импортируем пространство имен System.Threading. Теперь, учитывая, что Parametrized ThreadStart может указывать на любой метод, принимающий параметр System.Object, создадим специальный тип, содержащий число, которое должно быть прибавлено: class AddParams { public int a, b; public AddParams(int numbl, int numb2) { a = numbl; b = numb2; }
688 Часть V. Введение в библиотеки базовых классов .NET Затем создадим в классе Program статический метод, который принимает параметр AddParams и выводит на консоль сумму двух чисел: static void Add(object data) { if (data is AddParams) { Console.WriteLine("ID of thread in Add(): {0}", Thread.CurrentThread.ManagedThreadld); AddParams ap = (AddParams)data; Console.WriteLine ("{0} + {1} is {2}", ap.a, ap.b, ap.a + ap.b); } } Код внутри Main() достаточно очевиден. Просто вместо ThreadStart должен применяться ParametrizedThreadStart: static void Main(string [ ] args) { Console.WriteLine ("***** Adding with Thread objects *****"); Console.WriteLine("ID of thread in Main(): {0}", Thread.CurrentThread.ManagedThreadld); // Создать объект AddParams для передачи вторичному потоку. AddParams ap = new AddParamsA0, 10); Thread t = new Thread(new ParametenzedThreadStart (Add) ) ; t.Start (ap); // Подождать, пока другие потоки завершатся. Thread.SleepE); Console.ReadLine(); } Класс AutoResetEvent В этих первых примерах для того, чтобы заставить первичный поток подождать, пока вторичный поток завершится, применялось несколько грубых способов. Во время рассмотрения асинхронных делегатов в качестве переключателя использовалась простая переменная булевского типа. Однако это решение не может быть рекомендуемым, поскольку оба потока обращаются к одному и тому же элементу данных, что может привести к его повреждению. Более безопасной, хотя также нежелательной альтернативой может быть вызов Thread.Sleep() на определенный период времени. Проблема в том, что нет желания ждать больше, чем необходимо. Простой и безопасный к потокам способ заставить один поток ожидать завершения другого потока, предусматривает использование класса AutoResetEvent. В потоке, который должен ждать (таком как поток метода MainO), создадим экземпляр этого класса и передадим конструктору false, указав, что уведомления пока не было. В точке, где требуется ожидать, вызовем метод WaitOne(). Ниже приведен измененный класс Program, который делает все это, используя статическую переменную-член AutoResetEvent: class Program { private static AutoResetEvent waitHandle = new AutoResetEvent(false); static void Main(string [ ] args) { Console.WriteLine ("***** Adding with Thread objects *****"); Console.WriteLine("ID of thread in Main(): {0}", Thread.CurrentThread.ManagedThreadld); AddParams ap = new AddParamsA0, 10);
Глава 19. Многопоточность и параллельное программирование 689 Thread t = new Thread(new ParameterizedThreadStart(Add)); t.Start(ap); // Подождать здесь уведомления! waitHandle.WaitOne() ; Console. WriteLine ("Other thread is done!11); Console.ReadLine (); } Когда другой поток завершит свою работу, он вызовет метод Set() на том же экземпляре типа AutoResetEvent: static void Add(object data) { if (data is AddParams) { Console.WriteLine("ID of thread in Add(): {0}", Thread.CurrentThread.ManagedThreadld); AddParams ap = (AddParams)data; Console.WriteLine("{0} + {1} is {2}", ap.a, ap.b, ap.a + ap.b); // Сообщить другому потоку о завершении работы. waitHandle.Set() ; } Исходный код. Проект AddWithThreads доступен в подкаталоге Chapter 19. Потоки переднего плана и фоновые потоки Теперь, когда известно, как создавать новые потоки выполнения программно с помощью типов из пространства имен System.Threading, давайте проясним разницу между потоками переднего плана и фоновыми потоками. • Потоки переднего плана (foreground threads) обеспечивают предохранение текущего приложения от завершения. Среда CLR не остановит приложение (что означает выгрузку текущего домена приложения) до тех пор, пока не будут завершены все фоновые потоки. • Фоновые потоки (background threads), иногда называемые потоками-демонами, воспринимаются средой CLR как расширяемые пути выполнения, которые в любой момент времени могут игнорироваться (даже если они в текущее время заняты выполнением некоторой части работы). Таким образом, если все потоки переднего плана прекращаются, то все фоновые потоки автоматически уничтожаются при выгрузке домена приложения. Важно отметить, что потоки переднего плана и фоновые потоки — это не синонимы первичных и рабочих потоков. По умолчанию каждый поток, создаваемый через метод Thread.Start(), автоматически становится потоком переднего плана. Это означает, что домен приложения не выгрузится до тех пор, пока все потоки выполнения не завершат свою часть работы. В большинстве случаев именно такое поведение и нужно. Чтобы подтвердить сказанное, предположим, что необходимо вызвать Printer. PrintNumbers () на вторичном потоке, который должен вести себя как фоновый. Это означает, что метод, указанный типом Thread (через делегат ThreadStart или ParametrizedThreadStart), должен быть готов безопасно прерваться, как только потоки переднего плана закончат свою работу.
690 Часть V. Введение в библиотеки базовых классов .NET Конфигурирование такого потока предусматривает всего лишь установку свойства IsBackground в true: static void Main(string [ ] args) { Console.WriteLine ("***** Background Threads *****\n"); Printer p = new Printer (); Thread bgroundThread = new Thread(new ThreadStart(p.PrintNumbers)); // Теперь это фоновый поток. bgroundThread.IsBackground = true; bgroundThread.Start() ; } Обратите внимание, что в этом методе Main() не производится вызов Console. ReadLineO, чтобы оставить консоль видимой до нажатия клавиши <Enter>. Таким образом, после запуска приложение завершится немедленно, потому что объект Thread сконфигурирован как фоновый поток. Учитывая, что метод Main() инициирует создание первичного потока переднего плана, как только логика метода Main() завершится, домен приложения будет выгружен, прежде чем вторичный поток сможет завершить свою работу. Однако если закомментировать строку, которая устанавливает в true свойство IsBackground, обнаружится, что каждое число выводится на консоль, поскольку все потоки переднего плана должны завершить свою работу перед тем, как домен приложения будет выгружен из размещающего процесса. По большей части конфигурирование потока для выполнения в фоновом режиме может пригодиться, когда рабочий поток выполняет некритичную работу, потребность в которой отпадает после завершения главной задачи программы. Например, можно построить приложение, которое проверяет сервер электронной почты каждые несколько минут на предмет поступления новых писем, обновляет текущий прогноз погоды или выполняет какие-то другие некритичные задачи. Пример проблемы, связанной с параллелизмом При построении многопоточного приложения необходимо гарантировать, что любая часть разделяемых данных защищена от возможности изменения их значений множеством потоков. Учитывая, что все потоки в AppDomain имеют параллельный доступ к разделяемым данным приложения, представьте, что может случиться, если несколько потоков одновременно обратятся к одному и тому же элементу данных. Поскольку планировщик потоков случайным образом будет приостанавливать их работу, что если поток А будет прерван до того, как завершит свою работу? В вот что: поток В после этого прочтет нестабильные данные. Чтобы проиллюстрировать проблему, связанную с параллелизмом, давайте создадим еще один проект консольного приложения под названием MultiThreadedPrinting. В этом приложении опять используется созданный ранее класс Printer, но на этот раз метод PrintNumbers () приостановит текущий поток на случайно сгенерированный период времени. public class Printer { public void PrintNumbers () { for (int 1 = 0; l < 10; i++) { // Отправить поток спать на случайный период времени.
Глава 19. Многопоточность и параллельное программирование 691 Random r = new Random () ; Thread.SleepdOOO * r.NextE)); Console.Write("{0}, ", i); } Console.WriteLine (); Метод Main() отвечает за создание массива из десяти (уникально именованных) объектов Thread, каждый из которых производит вызов одного и того же экземпляра объекта Printer: class Program { static void Main(string[] агдз) { Console.WriteLine("*****Synchronizing Threads *****\nM); Printer p = new Printer(); // Создать 10 потоков, которые указывают на один //и тот же метод одного и того же объекта. Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(new ThreadStart(p.PrintNumbers)); threads[i] .Name = string.Format("Worker thread #{0}", i) ; } // Теперь запустить их все. foreach (Thread t in threads) t. Start () ; Console.ReadLine (); Прежде чем посмотреть на тестовые запуски, давайте еще раз проясним проблему. Первичный поток внутри этого домена приложений начинает свое существование, порождая десять вторичных рабочих потоков. Каждый рабочий поток должен вызвать метод PrintNumbers() на одном и том же экземпляре Printer. Учитывая, что никаких мер для блокировки разделяемых ресурсов этого объекта (консоли) не предпринималось, есть хороший шанс, что текущий поток будет отключен, прежде чем метод PrintNumbers () сможет напечатать полные результаты. Поскольку в точности не известно, когда это может случиться (и может ли вообще), будут получаться непредвиденные результаты. Например, может появиться следующий вывод: •••••Synchronizing Threads ***** -> Worker thread #1 is executing PrintNumbers () Your numbers: -> Worker thread #0 is executing PrintNumbers() -> Worker thread #2 is executing PrintNumbers() Your numbers: -> Worker thread #3 is executing PrintNumbers() Your numbers: -> Worker thread #4 is executing PrintNumbers() Your numbers: -> Worker thread #6 is executing PrintNumbers() Your numbers: -> Worker thread #7 is executing PrintNumbers() Your numbers: -> Worker thread #8 is executing PrintNumbers() Your numbers: -> Worker thread #9 is executing PrintNumbers() Your numbers: Your numbers: -> Worker thread #5 is executing PrintNumbers() Your numbers: 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 2, 1, 0, 0, 4, 3, 4, 1, 2, 4, 5, 5, 5, 6, 6, 6, 2, 7, 7, 7, 3, 4, 0, 8, 4, 5, 1, 5, 8, 8, 9, 2, 6, 1, 0, 9, 1, 6, 2, 1, 9,
692 Часть V. Введение в библиотеки базовых классов .NET 2, 1, 7, В, 3, 2, 3, 3, 9, 8, 4, 4, 5, 9, 4, 3, 5, 5, 6, 3, 6, 7, 4, 7, 6, В, 7, 4, 8, 5, 5, 6, 6, В, 7, 7, 9, 8, 9, В, 9, 9, 9, Запустите приложение еще несколько раз. Вот еще один вариант вывода: *****Synchronizing Threads ***** -> Worker thread #0 is executing PrintNumbers() -> Worker thread #1 is executing PrintNumbers() -> Worker thread #2 is executing PrintNumbers () Your numbers: -> Worker thread #4 is executing PrintNumbers () Your numbers: -> Worker thread #5 is executing PrintNumbers() Your numbers: Your numbers: -> Worker thread #6 is executing PrintNumbers() Your numbers: -> Worker thread #7 is executing PrintNumbers () Your numbers: Your numbers: -> Worker thread #8 is executing PrintNumbers () Your numbers: -> Worker thread #9 is executing PrintNumbers () Your numbers: -> Worker thread #3 is executing PrintNumbers () Your numbers: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7 i 7, 7, 1, 1, В, В, В, В, В, В, В, В, В, В, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, На заметку! Если не удается получить непредсказуемый вывод, увеличьте количество потоков с 10 до 100 (например) или добавьте в код еще один вызов Thread. Sleep (). В конце концов, "вы получите проблему, связаннуючс параллелизмом. Ясно, что здесь присутствует определенная проблема. Как только каждый поток требует от Printer печати числовых данные, планировщик потоков меняет их местами в фоновом режиме. В результате получается несогласованный вывод. Необходим способ обеспечения в коде синхронизированного доступа к разделяемым ресурсам. Как и можно было предположить, пространство имен System.Threading предлагает множество типов, ориентированных на синхронизацию. В языке С# также предусмотрено специальное ключевое слово для синхронизации разделяемых данных в многопоточном приложении. Синхронизация с использованием ключевого слова С# lock Первая техника, которую вы можете использовать для синхронизованного доступа к разделяемым ресурсам, состоит в применении ключевого слова С# lock. Это ключевое слово позволяет определять контекст операторов, которые должны быть синхронизованными между потоками. В результате входящие потоки не могут прервать текущий поток, мешая ему завершить свою работу. Ключевое слово lock требует указания маркера (ссылки на объект), которые должен быть получен потоком для входа в контекст блокировки.
Глава 19. Многопоточность и параллельное программирование 693 Чтобы предпринять попытку заблокировать приватный метод уровня экземпляра, нужно просто передать ссылку на текущий тип: private void SomePrivateMethod() { // Использовать текущий объект как маркер потока. lock(this) { // Весь код внутри этого контекста безопасен к потокам. } } Однако если вы заблокируете область кода внутри общедоступного члена, безопаснее (и вообще лучше) объявить приватный член object для использования в качестве маркера блокировки: public class Printer { // Маркер блокировки. private object threadLock = new object (); public void PrintNumbers () { // Использование маркера блокировки, lock (threadLock) { } } } В любом случае, если посмотреть на метод PrintNumbers (), то можно заметить, что разделяемый ресурс, за доступ к которому соперничают потоки — это окно консоли. В результате помещения всего взаимодействия с типом Console в контекст lock, как показано ниже, создается метод, который позволит конкурирующему потоку завершить свою работу: public void PrintNumbers () { // Использовать маркер блокировки приватного объекта, lock (threadLock) { // Вывести информацию о потоке. Console.WriteLine ("-> {0} is executing PrintNumbers()", Thread.CurrentThread.Name); // Вывести числа. Console.Write("Your numbers: "); for (int i = 0; l < 10; i++) { Random r = new Random () ; Thread.SleepA000 * r.Next E)); Console.Write ("{0}, ", i) ; } Console.WriteLine(); } } Как только поток войдет в контекст lock, маркер блокировки (в данном случае — текущий объект) станет недоступным другим потокам до тех пор, пока блокировка не будет снята по выходе из контекста lock. Таким образом, если поток А захватит маркер блокировки, другие потоки не смогут войти ни в один из контекстов, использующих тот же маркер, до тех пор, пока поток А не освободит его.
694 Часть V. Введение в библиотеки базовых классов .NET На заметку! Чтобы блокировать код в статическом методе, нужно объявить приватную статическую переменную-член, которая будет служить в качестве маркера блокировки. Если теперь запустить приложение, можно увидеть, что каждый поток получил возможность выполнить свою работу до конца: *****Synchronizi -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, -> Worker thread Your numbers: 0, ng #0 1, #1 1, #3 1, #2 1, #4 1, #5 1, #7 1, #6 1, #8 1, #9 1, Threads ***** is 2, is 2, is 2, is 2, is 2, is 2, is 2, is 2, is 2, is 2, executing PrintNumbers( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers ( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers ( 3, 4, 5, 6, 7, 8, 9, executing PrintNumbers ( 3, 4, 5, 6, 7, 8, 9, Исходный код. Проект MultiThreadedPrinting доступен в подкаталоге Chapter 19. Синхронизация с использованием типа Sy s tem. Threading. Monitor Оператор C# lock — это на самом деле сокращенная нотация для работы с классом System.Threading.Monitor. При обработке компилятором С# контекст lock на самом деле преобразуется в следующую конструкцию (в чем легко убедиться с помощью утилиты ldasm.exe или reflector.exe): public void PrintNumbers () { Monitor.Enter(threadLock); try { // Вывести информацию о потоке. Console.WriteLine("-> {0} is executing PrintNumbers()", Thread.CurrentThread.Name); // Вывести числа. Console.Write("Your numbers: ") ; for (int i = 0; i < 10; i++) { Random r = new Random () ; Thread.SleepA000 * r.NextE)); Console.Write ("{0}, ", i) ; } Console.WriteLine (); }
Глава 19. Многопоточность и параллельное программирование 695 finally { Monitor.Exit(threadLock); } } Для начала обратите внимание, что метод Monitor.Enter () является конечным получателем маркера потока, который указывается как аргумент ключевого слова lock. Весь код внутри контекста lock помещен в блок try. Соответствующая конструкция finally гарантирует освобождение маркера блокировки (через метод Monitor.Exit()) независимо от каких бы то ни было исключений времени выполнения. Модифицировав программу MultiThreadShareData для прямого использования типа Monitor (как только что было показано), вы обнаружите, что вывод идентичен. С учетом того, что ключевое слово lock, похоже, требует меньше кода, чем явное использование типа System.Threading.Monitor, может возникнуть вопрос о преимуществах его прямого использования. Короткий ответ: преимущество в большем контроле. Применяя тип Monitor, можно заставить активный поток ожидать в течение некоторого периода времени (с помощью статического метода Monitor.Wait()), информировать ожидающие потоки, когда текущий поток завершится (статическими методами Monitor.Pulse() и Monitor.PulseAllO), и т.д. Как и можно было ожидать, в значительном числе случаев ключевого слова С# lock вполне достаточно. Если вас интересуют дополнительные члены класса Monitor, обращайтесь в документацию .NET Framework 4.0 SDK. Синхронизация с использованием типа System.Threading. Interlocked Не заглядывая в CIL-код, очень трудно поверить, что присваивание и простые арифметические операции не являются атомарными. По этой причине в пространстве имен System.Threading предоставляется тип, позволяющий оперировать одним элементом данных автоматически, с меньшими накладными расходами, чем Monitor. В классе Interlocked определены статические члены, перечисленные в табл. 19.4. Таблица 19.4. Некоторые члены типа System.Threading.Interlocked Член Назначение CompareExchange () Безопасно проверяет два значения на эквивалентность. Если они эквивалентны, изменяет одно из значений на третье Decrement () Безопасно уменьшает значение на 1 Exchange () Безопасно меняет два значения местами Increment () Безопасно уменьшает значение на 1 Хотя это не видно сразу, процесс атомарного изменения отдельного значения довольно часто применяется в многопоточной среде. Предположим, что имеется метод по имени AddOneO, который увеличивает целочисленную переменную-член по имени intVal. Вместо написания кода синхронизации вроде следующего: public void AddOneO { lock(myLockToken) { intVal++; } }
696 Часть V. Введение в библиотеки базовых классов .NET можно воспользоваться статическим методом Interlocked.Increment () и в результате упростить код. Этому методу нужно передать по ссылке переменную для увеличения. Обратите внимание, что метод Increment () не только изменяет значение входного параметра, но также возвращает полученное новое значение: public void AddOne() { int newVal = Interlocked.Increment(ref intVal); } В дополнение к Increment и Decrement тип Interlocked позволяет автоматически присваивать числовые и объектные данные. Например, чтобы присвоить значение 83 переменной-члену, можно обойтись без явного оператора lock (или явной логики Monitor) и применить вместо этого метод Interlock.Exchange(): public void SafeAssignment() { Interlocked.Exchange(ref mylnt, 83); } Наконец, если необходимо проверить эквивалентность двух значений и изменить элемент сравнения в безопасной к потокам манере, то можно воспользоваться методом Interlocked.CompareExchange(), как показано ниже: public void CompareAndExchange() { // Если значение i равно 83, изменить его на 99. Interlocked.CompareExchange(ref i, 99, 83); } Синхронизация с использованием атрибута [Synchronization] Последний из примитивов синхронизации, которые здесь рассматриваются — это атрибут [Synchronization], который является членом пространства имен System. Runtime.Remoting.Contexts. Этот атрибут уровня класса эффективно блокирует весь код членов экземпляра объекта, обеспечивая безопасность в отношении потоков. Когда среда CLR размещает объекты, снабженные атрибутами [Synchronization], она помещает объект в контекст синхронизации. Как было показано в главе 17, объекты, которые не должны выходить за границы контекста, должны наследоваться от ContextBoundObject. Поэтому, чтобы сделать класс Printer безопасным к потокам (без явного написания кода внутри членов класса), необходимо модифицировать его следующим образом: using System.Runtime.Remoting.Contexts; // Все методы Printer теперь безопасны к потокам! [Synchronization] public class Printer : ContextBoundObject { public void PrintNumbers() { } } В некоторых отношениях этот подход выглядит как "ленивый" способ написания безопасного к потокам кода, учитывая, что не приходится углубляться в детали относительно того, какие именно аспекты типа действительно манипулируют чувствительными к потокам данными. Однако главный недостаток этого подхода состоит в том,
Глава 19. Многопоточность и параллельное программирование 697 что даже если определенный метод не использует чувствительные к потокам данные, CLR будет по-прежнему блокировать вызовы этого метода. Очевидно, что это приведет к деградации общей функциональности типа, поэтому используйте такую технику с осторожностью. Программирование с использованием обратных вызовов Timer Многие приложения нуждаются в вызове специфического метода через регулярные периоды времени. Например, в приложении может понадобиться отображать текущее время в панели состояния с помощью определенной вспомогательной функции. Другой пример: нужно, чтобы приложение вызывало вспомогательную функцию периодически, выполняя некоторые некритичные фоновые задачи, такие как проверка поступления новых сообщений электронной почты. Для ситуаций вроде этой можно использовать тип System.Threading.Timer в сочетании с делегатом по имени TimerCallback. Для целей иллюстрации предположим, что имеется консольное приложение (TimerApp), которое выводит текущее время каждую секунду до тех пор, пока пользователь не нажмет клавишу для прекращения приложения. Первый очевидный шаг — написать метод, который будет вызываться типом Timer (не забудьте импортировать System.Threading в файл кода): class Program { static void PrintTime(object state) { Console.WriteLine("Time is: {0}", DateTime.Now.ToLongTimeString()); } static void Main(string[] args) { } } Обратите внимание, что данный метод имеет единственный параметр типа System. Object и возвращает void. Это обязательно, поскольку делегат TimerCallback может вызывать только методы, соответствующие такой сигнатуре. Значение, переданное цели делегата TimerCallback, может представлять какую угодно информацию (в случае примера с электронной почтой этот параметр может представлять имя сервера Microsoft Exchange для взаимодействия в процессе). Также обратите внимание, что поскольку этот параметр — экземпляр System.Object, ему можно передавать несколько аргументов, используя System. Array или специальный класс/структуру. Следующий шаг связан с конфигурированием экземпляра делегата TimerCallback и передачей его объекту Timer. В дополнение к настройке делегата TimerCallback, конструктор Timer позволяет указать необязательный информационный параметр для передачи цели делегата (определенному как System.Object), интервал вызова метода и период времени ожидания (в миллисекундах), которое должно истечь перед первым вызовом. Ниже показан пример. static void Main(string [ ] args) { Console.WriteLine("***** Working with Timer type *****\n"); // Создать делегат для типа Timer. TimerCallback timeCB = new TimerCallback(PrintTime);
698 Часть V. Введение в библиотеки базовых классов .NET // Установить настройки таймера. Timer t = new Timer ( timeCB, // Объект-делегат TimerCallback. null, // Информация для передачи в вызванный метод (null — информация отсутствует) О, // Период времени ожидания перед запуском (в миллисекундах). 1000) ; // Интервал времени между вызовами (в миллисекундах) . Console.WriteLine ("Hit key to terminate..."); Console.ReadLine(); } В этом случае метод PrintTime () будет вызываться приблизительно каждую секунду и получит дополнительную информацию. Вот вывод примера: ***** Working with Timer type ***** Hit key to terminate. . . Time is: 6:51:48 PM Time is: 6:51:49 PM Time is: 6:51:50 PM Time is: 6:51:51 PM Time is: 6:51:52 PM Press any key to continue . . . Чтобы передать цели делегата какую-то информацию, замените значение null второго параметра конструктора соответствующей информацией: // Установить настройки таймера. Timer t = new Timer (timeCB, "Hello From Main", 0, 1000); Получить входные данные можно следующим образом: static void PrintTime(object state) { Console.WriteLine("Time is: {0}, Param is: {1}", DateTime.Now.ToLongTimeStringO, state.ToString ()); } Исходный код. Проект TimerApp доступен в подкаталоге Chapter 19. Пул потоков CLR Следующей темой, связанной с потоками, которую мы рассмотрим в этой главе, является роль пула потоков CLR. При асинхронном вызове метода посредством типов делегатов (через метод BeginlnvokeO) среда CLR на самом деле не создает новый поток. Для эффективности метод делегата Beginlnvoke () полагается на пул рабочих потоков, которые поддерживаются исполняющей средой. Чтобы позволить взаимодействовать с этим пулом ожидающих потоков, в пространстве имен System.Threading предлагается класс ThreadPool. Чтобы запросить поток из пула для обработки вызова метода, можно использовать метод ThreadPool.QueueUserWorkltem(). Этот метод перегружен, чтобы в дополнение к экземпляру делегата WaitCallback позволить указывать необязательный параметр System.Object для специальных данных состояния: public static class ThreadPool { public static bool QueueUserWorkltem(WaitCallback callBack); public static bool QueueUserWorkltem(WaitCallback callBack, object state); }
Глава 19. Многопоточность и параллельное программирование 699 Делегат WaitCallback может указывать на любой метод, принимающий System. Object в качестве единственного параметра (представляющего необязательные данные состояния) и ничего не возвращающий. Обратите внимание, что если не указать System.Object при вызове QueueUserWorkltemO, среда CLR автоматически передаст значение null. Чтобы проиллюстрировать работу методов очередей, работающих с пулом потоков CLR, рассмотрим еще раз программу, использующую тип Printer. В этом случае, однако, массив объектов Thread вручную создаваться не будет; вместо этого методу PrintNumbers () будут присваиваться члены пула: class Program { static void Main(string [ ] args) { Console.WriteLine("***** Fun with the CLR Thread Pool *****\n"); Console.WriteLine ("Main thread started. ThreadID = {0}", Thread.CurrentThread.ManagedThreadld); Printer p = new Printer(); WaitCallback workltem = new WaitCallback(PrintTheNumbers); // Поставить в очередь метод десять раз. for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkltem(workltem, p) ; } Console.WriteLine("All tasks queued"); Console.ReadLine(); } static void PrintTheNumbers(object state) { Printer task = (Printer)state; task.PrintNumbers() ; } } Здесь может возникнуть вопрос: в чем же преимущество использования поддерживаемого CLR пула потоков по сравнению с явным созданием объектов Thread? Ниже перечислены эти преимущества. 1. Пул потоков управляет потоками эффективно, уменьшая количество создаваемых, запускаемых и останавливаемых потоков. 2. Используя пул потоков, можно сосредоточиться на решении задачи, а не на инфраструктуре потоков приложения. Тем не менее, в некоторых случаях предпочтительно ручное управление потоками. • Если нужны потоки переднего плана, или должен быть установлен приоритет потока. Потоки из пула всегда являются фоновыми с приоритетом по умолчанию (ThreadPriority. Normal). • Если требуется поток с фиксированной идентичностью, чтобы можно было прерывать его или находить по имени. Исходный код. Проект TimerPoolApp доступен в подкаталоге Chapter 19. На этом исследование пространства имен System.Threading завершается. Остальная часть этой главы посвящена новому пополнению библиотек базовых классов .NET 4.0, а именно — Task Parallel Library.
700 Часть V. Введение в библиотеки базовых классов .NET Параллельное программирование на платформе .NET В последнее время повсеместно распространены компьютеры, оснащенные двумя или более центральными процессорами (ядрами). Причем они не только широко распространены, но уже и достаточно дешевы. Когда машина поддерживает несколько процессоров, она в состоянии выполнять потоки в параллельном режиме, что значительно увеличивает производительность выполнения приложений. Так сложилось, что для построения приложения .NET, которое может распределять рабочую нагрузку между несколькими ядрами, нужно обладать достаточной квалификацией в многопоточном программировании. Хотя это определенно реально, все же эта область довольно утомительна и подвержена ошибкам, учитывая неизбежную сложность построения многопоточных приложений. С выходом версии .NET 4.0 вы получили в свое распоряжение замечательную новую библиотеку для параллельного программирования. Используя типы System.Threading. Tasks, можно строить тонко гранулированный масштабируемый параллельный код без необходимости непосредственной работы с потоками или пулом потоков. Более того, при этом для распределения рабочей нагрузки можно использовать строго типизированные запросы LINQ (с помощью параллельного LINQ, или PLINQ). Интерфейс Task Parallel Library API Говоря в общем, типы из пространства System.Threading.Tasks (а также некоторые связанные с ними типы в System.Threading) объединены общим названием Task Parallel Libranj (Библиотека параллельных задач), или TPL. Библиотека TPL позволяет автоматически распределять нагрузку приложений между доступными процессорами в динамическом режиме, используя пул потоков CLR. Библиотека TPL занимается распределением работы, планированием потоков, управлением состоянием и прочими низкоуровневыми деталями. В результате появляется возможность максимизировать производительность приложений .NET, не имея дела со сложностями прямой работы с потоками. На рис. 19.3 показаны члены нового пространства имен .NET 4.O. Н Object Browser X Hj Browse*. All Components ШИЯШяшшвашяшшшшшшшшшшшшшшишшш <Search> л () Bt 0 4} Parallel b p ParallelLoopResult l> *4$ ParallelLoopState t> % ParallelOptions > ^t Task fr 4$ Task<TResurt> • Ц$ TaskCanceledException > *!$ TaskCompletionSource<TResurt> TaskContinuationOpttons aiP TaskCreattonOpttons t> •»■{$ TaskFactory ;> 4$ TaskFactory<TResutt> t> ^ TaskScheduler Г> ^ TaskSchedulerException i> .# TaskStatus "J| UnobservedTaskExceptionEventArgs j - 1 В namespace System.Threading.Tasks Member of mscorlib Z3EH - Рис. 19.3. Члены пространства имен System.Threading
Глава 19. Многопоточность и параллельное программирование 701 Начиная с .NET 4.0, использование TPL является рекомендуемым способом построения многопоточных приложений. Это вовсе не значит, что теперь понимание традиционных многопоточных технологий с использованием асинхронных делегатов или классов из пространства имен System.Threading излишне. На самом деле, чтобы эффективно использовать TPL, необходимо понимать такие примитивы, как потоки, блокировки и параллелизм. Более того, многие ситуации, требующие нескольких потоков (такие как асинхронные вызовы удаленных объектов), могут быть обработаны без использования TPL. Тем не менее, время, которое понадобится для непосредственной работы с классом Thread, уменьшается, и существенно. Наконец, имейте в виду, что возможность что-то делать вовсе не означает необходимость это делать. В некоторых случаях создание нескольких потоков может замедлить выполнение программ .NET, а порождение множества излишних параллельных задач может нанести ущерб производительности. Используйте функциональность TPL, только когда есть рабочая нагрузка, которая действительно создает узкое место в программах, например, итерация по сотням объектов, обработка данных из нескольких файлов и т.п. На заметку! Инфраструктура TPL довольно интеллектуальна. Если TPL определяет, что набор задач будет иметь лишь минимальный выигрыш (или вообще никакого) от выполнения в параллельном режиме, то выполняет такие задачи последовательно. Роль класса Parallel Главным классом в TPL является System.Threading.Tasks.Parallel. Этот класс поддерживает набор методов, которые позволяют выполнять итерации по коллекции данных (точнее, по объектам, реализующим IEnumerable<T>) в параллельном режиме. Заглянув в документацию .NET Framework 4.0 SDK, вы увидите, что этот класс поддерживает два статических метода— Parallel.For () и Parallel.ForEach(), для каждого из которых определены многочисленные перегруженные версии. Эти методы позволяют создавать тело операторов кода, которое может выполняться в параллельном режиме. Концептуально эти операторы представляют собой логику того же рода, которую была бы написана в нормальной циклической конструкции (с использованием ключевых слов С# for и foreach). Однако их преимущество состоит в том, что класс Parallel самостоятельно берет потоки из пула потоков (и управляет конкуренцией). Оба эти метода требуют указания совместимого с IEnumerable или IEnumerable<T> контейнера, хранящего данные, которые нужно обработать в параллельном режиме. Контейнер может быть простым массивом, необобщенной коллекцией (вроде ArrayList), обобщенной коллекцией (наподобие List<T>) или результатом запроса LINQ. Вдобавок нужно будет использовать делегаты System.Func<T> и System. Action<T> для указания целевого метода, который будет вызываться для обработки данных. Вы уже встречали делегат Func<T> в главе 13, когда рассматривалась технология LINQ to Objects. Вспомните, что Func<T> представляет метод, который возвращает значение и принимает различное количество аргументов. Делегат Action<T> очень похож на Func<T> в том, что позволяет указывать метод, принимающий несколько параметров. Однако Action<T> указывает метод, который может возвращать только void. Хотя можно было бы вызывать методы Parallel.For() и Parallel.ForEachO и передавать строго типизированные объекты делегатов Func<T> или Action<T>, задача программирования упрощается с использованием подходящих анонимных методов С# и лямбда-выражений.
702 Часть V. Введение в библиотеки базовых классов .NET Понятие параллелизма данных DiaBHoe применение TPL связано с обеспечением параллелизма данных. Этим термином обозначается задача итерации по массиву или коллекции в параллельном режиме с использованием методов Parallel.ForO или Parallel.ForEach(). Предположим, что нужно выполнять некоторые трудоемкие операции, связанные с файловым вводом- выводом. Например, требуется загрузить в память огромное количество файлов *.jpg, повернуть содержащиеся в них изображения и сохранить модифицированные данные в новом местоположении. В документации .NET Framework 4.0 SDK представлен пример консольного приложения, решающего эту задачу, но мы повторим его, оснастив графическим интерфейсом пользователя. Давайте создадим приложение Windows Forms по имени DataParallelismWithForEach и переименуем Forml.cs в MainForm.cs. После этого импортируем в главный файл кода следующие пространства имен: // Эти пространства имен нужны! using System.Threading.Tasks; using System.Threading; using System.10; Графический интерфейс этого приложения содержит многострочную текстовую область TextBox и одну кнопку Button (по имени btnProcessImages). Текстовая область предназначена для ввода данных во время выполнения работы в фоновом режиме, что иллюстрирует неблокирующую природу параллельной задачи. В обработчике события Click этого элемента Button в конечном итоге будет использоваться TPL, а пока напишем следующий блокирующий код: public partial class MainForm : Form { public MainForm () InitializeComponent(); private void btnProcessImages_Click(object sender, EventArgs e) ProcessFiles(); private void ProcessFiles () // Загрузить все файлы *.jpg и создать новую папку для модифицированных данных, string[] files = Directory.GetFiles(@"C:\Users\AndrewTroelsen\Pictures\My Family", "*.jpg", SearchOption.AllDirectories) ; string newDir = @"C:\ModifiedPictures"; Directory.CreateDirectory(newDir); // Обработка графических данных в блокирующей манере. foreach (string currentFile in files) { string filename = Path.GetFileName(currentFile); using (Bitmap bitmap = new Bitmap(currentFile)) { bitmap.RotateFlip(RotateFlipType.Rotatel80FlipNone); bitmap.Save(Path.Combine(newDir, filename)); this.Text = string.Format("Processing {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadld) ; } } this.Text = "All done!"; } }
Глава 19. Многопоточность и параллельное программирование 703 ' Processing P'. Feel free to type here while the images are processed. b 0»ck to Hip Your Images! Обратите внимание, что метод Process Files () поворачивает изображение в каждом файле *.jpg из папки Pictures\My Family, которая в данный момент содержит всего 37 файлов (при необходимости укажите другой путь в вызове Directory.GetFiles ()). В настоящее время вся работа происходит в первичном потоке исполняемой программы. Поэтому после щелчка пользователем на кнопке программа выглядит зависшей. Более того, заголовок окна также сообщит о том, что тот же первичный поток обрабатывает файл (рис. 19.4). Чтобы обеспечить обработку файлов на как можно большем числе процессоров (или ядер), следует переписать цикл foreach, воспользовавшись Parallel.ForEach(). Вспомните, что этот метод имеет множество перегрузок. В его простейшей форме методу можно передать совместимый с IEnumerable<T> объект, который содержит элементы, подлежащие обработке (например, массив строк с именами файлов), и делегат Action<T>, который указывает на метод, выполняющий работу. Ниже показано важнейшее изменение с использованием лямбда-операции С# вместо литерального объекта делегата Action<T>: // Обработка графических данных в параллельном режиме! Parallel.ForEach(files, currentFile => Рис. 19.4. Сейчас вся работа происходит в первичном потоке { string filename = Path.GetFileName(currentFile); using (Bitmap bitmap = new Bitmap(currentFile)) { bitmap.RotateFlip(RotateFlipType.Rotatel80FlipNone); bitmap.Save(Path.Combine(newDir, filename)); this.Text = string.Format("Processing {0} on thread {1}' Thread.CurrentThread.ManagedThreadld); filename, } ), Если теперь запустить программу, библиотека TPL распределит рабочую нагрузку по множеству потоков, взятых из пула, используя столько процессоров, сколько возможно. Однако в заголовке окна не будут отображаться имена уникальных потоков, а при вводе в текстовой области ничего не будет видно, пока не обработаются все файлы изображений. Причина в том, что первичный поток пользовательского интерфейса все равно остается блокированным, ожидая, пока все прочие потоки завершат свою работу. Класс Task Чтобы сохранить пользовательский интерфейс отзывчивым, можно напрямую использовать асинхронные делегаты или члены пространства System.Threading, но пространство имен System.Threading.Tasks предлагает более простую альтернативу — класс Task. Этот класс позволяет легко вызывать метод во вторичном потоке и может применяться вместо работы с асинхронными делегатами. Изменим обработчик события Click элемента Button следующим образом: private void btnProcessImages_Click(object sender, EventArgs e) { // Запустить новую "задачу" для обработки файлов.
704 Часть V. Введение в библиотеки базовых классов .NET Task.Factory.StartNew( () => { ProcessFiles (); }); } Свойство Factory класса Task возвращает объект TaskFactory. При вызове методу StartNow() передается делегат Action<T> (здесь это скрыто подходящим лямбда-выражением), который указывает на метод для вызова в асинхронной манере. После этой небольшой модификации вы обнаружите, что заголовок окна отображает, какой поток из пула обрабатывает конкретный файл, а текстовое поле может принимать ввод, поскольку пользовательский интерфейс больше не блокируется. Обработка запроса на отмену В текущий пример можно добавить еще одно усовершенствование — позволить пользователю останавливать обработку графических данных за счет щелчка на второй кнопке Cancel (Отмена). К счастью оба метода, Parallel.For() и Parallel.ForEachO, поддерживают отмену через использование маркеров отмены (cancellation tokens). При вызове методов на Parallel можно передавать объект ParallelOptions, который, в свою очередь, содержит объект CancellationTokenSource. Прежде всего, определим в классе-наследнике Form приватную переменную-член cancelToken типа CancellationTokenSource: public partial class MainForm : Form { // Новая переменная уровня Form. private CancellationTokenSource cancelToken = new CancellationTokenSource(); } Предполагая, что был добавлен новый элемент Button (по имени btnCancel), реализуем его обработчик события Click следующим образом: private void btnCancelTask_Click(object sender, EventArgs e) { // Это будет использовано для передачи всем рабочим потокам команды останова! cancelToken.Cancel (); } Теперь можно внести необходимые модификации в метод ProcessFiles(). Ниже показана окончательная реализация этого метода. private void ProcessFiles () { // Использовать экземпляр ParallelOptions для сохранения CancellationToken. ParallelOptions parOpts = new ParallelOptions (); parOpts.CancellationToken = cancelToken.Token; parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount; // Загрузить все файлы *.jpg и создать новую папку для модифицированных данных. string[] files = Directory.GetFiles (@"C:\Users\AndrewTroelsen\Pictures\My Family", "*.]pg", SearchOption.AllDirectories); string newDir = @"C:\ModifledPictures"; Directory.CreateDirectory(newDir); try { // Обработать данные изображения в параллельном режиме!
Глава 19. Многопоточность и параллельное программирование 705 Parallel.ForEach(files, parOpts, currentFile => { parOpts.CancellationToken.ThrowIfCancellationRequested(); string filename = Path.GetFileName(currentFile) ; using (Bitmap bitmap = new Bitmap(currentFile)) { bitmap.RotateFlip(RotateFlipType.Rotatel80FlipNone); bitmap.Save(Path.Combine(newDir, filename)); this.Text = string.Format("Processing {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadld); } } ); this.Text = "All done1"; } catch (OperationCanceledException ex) { this.Text = ex.Message; } } Обратите внимание, что метод начинается с конфигурирования объекта Parallel Options путем установки его свойства CancellationToken в cancelToken.Token. Кроме того, при вызове методу Parallel.ForEach() во втором параметре передается объект ParallelOptions. В контексте логики цикла осуществляется вызов ThrowIfCancellationRequestedO на маркере. Это гарантирует, что если пользователь щелкнет на кнопке Cancel, то все потоки будут остановлены, и будет сгенерировано исключение времени выполнения. Перехватив исключение OperationCanceledException, можно включить сообщение об ошибке в текст главного окна. Исходный код. Проект DataParallelismWithForEach доступен в подкаталоге Chapter 19. Понятие параллелизма задач В дополнение к обеспечению параллелизма данных, библиотека TPL также может применяться для простого запуска любого количества асинхронных задач с помощью метода Parallel.Invoke(). Этот подход немного проще, чем использование делегатов или типов из пространства имен System.Threading. Тем не менее, если нужна более высокая степень контроля над выполняемыми задачами, следует отказаться от Parallel.Invoke() и напрямую работать с классом Task, как это делалось в предыдущем примере. Чтобы проиллюстрировать параллелизм задач в действии, создадим новое приложение Windows Forms по имени MyEBookReader и импортируем в нем пространства имен System.Threading.Tasks и System.Net. Это приложение представляет собой модификацию примера из документации .NET Framework 4.0 SDK, в котором извлекается электронная книга из сайта проекта Гуттенберга (http://www.gutenberg.org) и затем параллельно выполняется набор длительных заданий. Графический интерфейс состоит из многострочной текстовой области Text Box (no имени txtBook) и двух кнопок Button (btnDownload и btnGetStats). Для каждой кнопки понадобится обработать событие Click, а в файле кода формы объявить на уровне класса переменную string по имени theEBook. Ниже показана реализация обработчика события Click для кнопки btnDownload:
706 Часть V. Введение в библиотеки базовых классов .NET private void btnDownload_Click (object sender, EventArgs e) { WebClient wc = new WebClientO ; wc.DownloadStringCompleted += (s, eArgs) => { theEBook = eArgs.Result; txtBook.Text = theEBook; }; // Загрузка электронной книги "A Tale of Two Cities". wc.DownloadStringAsync(new Uri("http://www.gutenberg.org/files/98/98-8.txt")) ; } Класс WebClient — это член пространства имен System.Net. Он предоставляет ряд методов для отправки данных и получения данных от ресурса, идентифицированного URL. Многие из этих методов имеют асинхронные версии, такие как DownloadStringAsynO- Этот метод автоматически запускает новый поток, взятый из пула потоков CLR. Когда WebClient завершает получение данных, он инициирует событие DownloadStringCompleted, которое обрабатывается с использованием лямбда- выражения С#. Если вызвать синхронную версию этого метода (DownloadStringO), то форма на некоторое время перестанет реагировать на действия пользователя. Обработчик события Click кнопки btnGetStats реализован так, чтобы извлекать индивидуальные слова, содержащиеся в переменной theEBook, и передавать строковый массив на обработку новой вспомогательной функции: private void btnGetStats_Click(object sender, EventArgs e) { // Получить слова из электронной книги. string[] words = theEBook.Split (new char[] { ' ', '\u000A\ ',', '.', ';', ':', '-', '?', '/' }, StringSplitOptions.RemoveEmptyEntries); // Найти 10 наиболее часто встречающихся слов. string[] tenMostCommon = FindTenMostCommon(words); // Получить самое длинное слово. string longestWord = FindLongestWord(words); // Когда все задачи завершены, построить строку, // показывающую всю статистику в окне сообщений. StringBuilder bookStats = new StringBuilder("Ten Most Common Words are:\n"); foreach (string s in tenMostCommon) { bookStats.AppendLine(s); } bookStats.AppendFormat("Longest word is : {0}", longestWord); bookStats.AppendLine() ; MessageBox.Show(bookStats.ToString() , "Book info"); } Метод FindTenMostCommonO использует запрос LINQ для получения списка объектов string, которые наиболее часто встречаются в массиве string, а метод FindLongestWord() находит самое длинное слово: private string[] FindTenMostCommon(string [ ] words) { var frequencyOrder = from word in words where word.Length > 6 group word by word into g orderby g.Count () descending select g.Key; string[] commonWords = (frequencyOrder.Take A0)) .ToArray(); return commonWords;
Глава 19. Многопоточность и параллельное программирование 707 private string FindLongestWord(string[] words) { return (from w in words orderby w.Length descending select w) .First (); } После запуске этого проекта на выполнение всех задач может потребоваться некоторое время, в зависимости от количества процессоров машины и их тактовой частоты. В конце концов, должен появиться следующий вывод (рис. 19.5). «fa* My EBook Reader The Prefect Gutenberg EBook of A Tate of Two Gbes by Chafes Щ This eBook is for the use of anyone anywhere at no cost and with almost no restrictions whatsoever You may copy t give 1 away or rente 1 under the tern» of the Project Gutenberg License nduded wth this eBook or online at www gutenberg.org Tile A Tale of Two Obes A Story of the French Revolution Author: Charles Dickens Release Date: January. 1994 {EBook S98] Posting Date: November 28. 2009 Language Engtsh Character set encodng: ISO-8859-1 ~ START OF THIS PROJECT GUTENBERG EBOOK A TALE 01 Рис. 19.5. Статистика загруженной электронной книги Можете убедиться, что приложение использует все доступные процессоры машины, вызвав параллельно методы FindTenMostCommonO и FindLongestWordO. Для этого необходимо модифицировать метод btnGetStats_Click() следующим образом: private void btnGetStats_Click(object sender, EventArgs e) // Получить слова из электронной книги. string[] words = theEBook.Split ( new char[] { ' ', '\u000A\ \\ '.', ';', ':', ' StringSplitOptions.RemoveEmptyEntries); string[] tenMostCommon = null; string longestWord = string.Empty; Parallel.Invoke( 0 => { // Найти 10 наиболее часто встречающихся слов. tenMostCommon = FindTenMostCommon(words); '/' }, О => { // Найти самое длинное слово. longestWord = FindLongestWord(words); }); // Когда все задачи завершены, построить строку, // показывающую всю статистику в окне сообщений. Метод Parallel.InvokeO ожидает параметра-массива делегатов Act ion о, которые передаются неявно с помощью лямбда-выражения. Хотя вывод идентичен, преиму-
708 Часть V. Введение в библиотеки базовых классов .NET щество библиотеки TPL состоит в использовании всех доступных процессоров машины для вызова каждого метода параллельно, если это возможно. Исходный код. Проект MyEBookReader доступен в подкаталоге Chapter 19. Запросы параллельного LINQ (PLINQ) В завершение знакомства с TPL следует отметить, что есть и другой способ встраивания параллельных задач в ваши приложения .NET. При желании можно использовать новый набор расширяющих методов, которые позволяют конструировать запрос LINQ, распределяющий свою нагрузку по параллельным потокам (если это возможно). Неудивительно, что запросы LINQ, которые спроектированы для параллельного выполнения, называются запросами Parallel LINQ (PLINQ). Подобно параллельному коду, написанному с использованием класса Parallel, в PLINQ имеется опция игнорирования запроса на обработку коллекции в параллельном режиме. Библиотека PLINQ оптимизирована во многих отношениях, включая определение того, не будет ли запрос на самом деле эффективнее выполняться в синхронном режиме. Во время выполнения PLINQ анализирует общую структуру запроса, и если есть вероятность, что запрос выиграет от параллелизма, он будет выполнен параллельно. Однако если это ухудшит производительность, PLINQ выполнит запрос последовательно. Если возникает выбор между потенциально дорогостоящим параллельным алгоритмом и недорогим последовательным, предпочтение по умолчанию отдается последовательному алгоритму. Необходимые расширяющие методы находятся в классе ParallelEnumerable пространства имен System.Linq. В табл. 19.5 описаны некоторые полезные расширения PLINQ. Таблица 19.5. Некоторые члены класса ParallelEnumarable Член Назначение AsParallelO Указывает, что остаток запроса должен быть выполнен параллельно, если это возможно WithCancellationO Указывает, что PLINQ должен периодически следить за состоянием представленного маркера отмены и, если понадобится, отменять выполнение WithDegreeOf ParallelismO Указывает максимальное количество процессоров, которое PLINQ должен использовать для распараллеливания запроса For All () Позволяет обрабатывать результаты параллельно, без предварительного соединения с потоком-потребителем, как это происходит при перечислении результата LINQ посредством ключевого слова foreach Чтобы посмотреть на PLINQ в действии, создадим приложение Windows Forms по имени PLINQDataProcessingWithCancellation и импортируем в него пространства имен System.Threading и System.Threading.Tasks. Эта простая форма потребует всего двух кнопок с именами btnExecute и btnCancel. Щелчок на кнопке Execute (Выполнить) приводит к запуску новой задачи (Task), которая выполнит запрос LINQ, просматривающий очень большой массив целых чисел в поисках элементов, для которых остаток от деления на 3 равен 0. Ниже показана непараллельная версия этого запроса:
Глава 19. Многопоточность и параллельное программирование 709 public partial class MainForm : Form { private void btnExecute_Click(object sender, EventArgs e) { // Запустить новую "задачу" для обработки целых. Task.Factory.StartNew ( () => { ProcessIntData (); }); } private void ProcessIntData () { // Получить очень большой массив целых чисел. int[] source = Enumerable.Range A, 10000000).ToArray(); // Найти числа, для которых истинно num и 3 == 0, //и возвратить их в порядке убывания. int [ ] modThreelsZero = (from num in source where num % 3 == 0 orderby num descending select num) .ToArray (); MessageBox.Show (string.Format("Found {0} numbers that match query!", modThreelsZero.Count())); } } Выполнение запроса PLINQ Чтобы заставить TPL выполнить этот запрос в параллельном режиме (по возможности), для этого понадобится расширяющий метод AsParallel(): int [ ] modThreelsZero = (from num in source .AsParallel () where num n6 3 == 0 orderby num descending select num).ToArray(); Обратите внимание, что общий формат запроса LINQ идентичен тому, что было показано в предыдущих главах. Однако, при включенном вызове AsParallel(), библиотека TPL попытается распределить нагрузку по всем доступным процессорам. Отмена запроса PLINQ С помощью объекта CancellationTokenSource можно заставить запрос PLINQ прекращать обработку при определенных условиях (обычно из-за вмешательства пользователя). Для этого потребуется объявить на уровне формы объект CancellationTokenSource по имени cancelToken и реализовать обработчик события Click кнопки btnCancel. Ниже показаны соответствующие изменения в коде: public partial class MainForm : Form { private CancellationTokenSource cancelToken = new CancellationTokenSource (); private void btnCancel_Click(object sender, EventArgs e) { cancelToken.Cancel (); } } Теперь необходимо информировать запрос PLINQ о том, что он должен ожидать запроса на отмену выполнения, добавив в цепочку расширяющий метод WithCancellation () и передав маркер. Вдобавок нужно поместить этот запрос PLINQ в контекст try/catch и обработать возможные исключения. Финальная версия метода ProcessInDataO выглядит следующим образом:
710 Часть V. Введение в библиотеки базовых классов .NET private void ProcessIntData () { // Получить очень большой массив целых чисел. int[] source = Enumerable.Range A, 10000000).ToArray(); // Найти числа, для которых истинно num % 3 == 0, и возвратить их в порядке убывания int[] modThreelsZero = null; try { modThreelsZero = (from num in source.AsParallel().WithCancellation(cancelToken.Token) where num nu 3 == 0 orderby num descending select num).ToArray(); } catch (OperationCanceledException ex) { this.Text = ex.Message; } MessageBox.Show(string.Format("Found {0} numbers that match query!", modThreelsZero.Count ())); } На этом первоначальное знакомство с библиотекой Tksk Parallel Library и PLINQ завершено. Как было сказано, эти новые API-интерфейсы .NET 4.0 быстро завоевывают популярность при работе с многопоточными приложениями. Тем не менее, эффективное использование TPL и PLINQ требует основательного понимания концепций и примитивов многопоточности, представленных в этой главе. Дополнительные сведения можно найти в разделе "Parallel Programming in the .NET Framework" ("Параллельное программирование на платформе .NET Framework") в документации .NET Framework 4.0 SDK. В нем представлен богатый набор примеров проектов, которые расширят то, что было описано ранее в главе. Исходный код. Проект PLINQDataProcessingWithCancellation доступен в подкаталоге Chapter 19. Резюме Эта глава началась с описания того, как сконфигурировать типы делегатов .NET для выполнения метода в асинхронном режиме. Как вы видели, методы BeginlnvokeO и EndlnvokeO позволяют неявно манипулировать вторичным потоком с минимальными усилиями с вашей стороны. Далее были представлены интерфейс IAsyncResult и тип класса AsyncResult. Было показано, что эти типы предлагают различные способы синхронизации вызывающего потока и получения возможных возвращаемых значений методов. Следующая часть главы была посвящена рассмотрению роли пространства имен System.Threading. Как было сказано, когда приложение создает дополнительные потоки выполнения, в результате оно может выполнять несколько задач (как кажется) одновременно. Также были продемонстрированы различные способы зашиты чувствительных к потокам блоков кода для предотвращения повреждения разделяемых ресурсов. Глава завершилась рассмотрением совершенно новой модели многопоточной разработки на платформе .NET 4.0 — библиотеки Tksk Parallel Library и PLINQ. Можно смело утверждать, что эти API-интерфейсы станут предпочтительным инструментом построения многозадачных систем, поскольку они позволяют программировать с использованием набора высокоуровневых типов (многие из которых находятся в пространстве имен System.Threading.Tasks), которые скрывают от разработчика значительную часть сложности.
ГЛАВА 20 Файловый ввод-вывод и сериализация объектов При создании настольных приложений способность сохранять информацию между пользовательскими сеансами является обязательной. В этой главе рассматривается множество тем, связанных с вводом-выводом, с точки зрения платформы .NET Framework. Первая задача связана с исследованием основных типов, определенных в пространстве имен System. 10, которые позволяют программно модифицировать структуру каталогов и файлов. Вторая задача состоит в изучении различных способов чтения и записи символьных, двоичных и располагаемых в памяти структур данных. Изучив способы манипулирования файлами и каталогами с использованием базовых типов ввода-вывода, вы ознакомитесь с родственной темой — сериализацией объектов. Сериализация объектов служит для сохранения и восстановления состояния объекта в любом типе, унаследованном от System. 10. Stream. Возможность сериализации объектов критична, когда необходимо копировать объект на удаленную машину с помощью технологий удаленного взаимодействия, таких как Windows Communication Foundation. Однако сериализация удобна и сама по себе, и наверняка пригодится во многих разрабатываемых приложениях .NET (как распределенных, так и нет). Исследование пространства имен System. 10 Пространство имен System. 10 в .NET — это область библиотек базовых классов, посвященная службам файлового ввода-вывода, а также ввода-вывода из памяти. Подобно любому пространству имен, в System. 10 определен набор классов, интерфейсов, перечислений, структур и делегатов, большинство из которых находятся в mscorlib.dll. В дополнение к типам, содержащимся внутри mscorlib.dll, в сборке System.dll определены дополнительные члены пространства имен System. 10 . Обратите внимание, что все проекты Visual Studio 2010 автоматически устанавливают ссылки на обе сборки. Многие типы из пространства имен System. 10 сосредоточены на программных манипуляциях физическими каталогами и файлами. Дополнительные типы предоставляют поддержку чтения и записи данных в строковые буферы, а также области памяти. В табл. 20.1 кратко описаны основные (неабстрактные) классы, которые дают понятие о функциональности System. 10.
712 Часть V. Введение в библиотеки базовых классов .NET Таблица 20.1. Ключевые члены пространства имен System.10 Неабстрактные классы ввода-вывода Назначение BinaryReader BinaryWnter BufferedStream Directory Directorylnfo Drivelnfo File Filelnfo FileStream FileSystemWatcher MemoryStream Path StreamWriter StreamReader StringWriter StringReader Эти классы позволяют сохранять и извлекать элементарные типы данных (целочисленные, булевские, строковые и т.п.) в двоичном виде Этот класс предоставляет временное хранилище для потока байтов, которые могут затем быть перенесены в постоянные хранилища Эти классы используются для манипуляций структурой каталогов машины. Тип Directory представляет функциональность, используя статические члены. Тип Directorylnfo обеспечивает аналогичную функциональность через действительную объектную ссылку Этот класс предоставляет детальную информацию относительно дисковых устройств, используемых данной машиной Эти классы служат для манипуляций множеством файлов данной машины. Тип File представляет функциональность через статические члены. Тип Filelnfo обеспечивает аналогичную функциональность через действительную объектную ссылку Этот класс обеспечивает произвольный доступ к файлу (т.е. возможности поиска) с данными, представленными в виде потока байт Этот класс позволяет отслеживать модификации внешних файлов в определенном каталоге Этот класс обеспечивает произвольный доступ к данным, хранящимся в памяти, а не в физическом файле Этот класс выполняет операции над типами System.String, содержащими информацию о пути к файлу или каталогу в независимой от платформы манере Эти классы используются для хранении я (и извлечения) текстовой информации из файла. Эти классы не поддерживают произвольного доступа к файлу Подобно классам StreamWriter/StreamReader, эти классы также работают с текстовой информацией Однако лежащим в основе хранилищем является строковый буфер, а не физический файл В дополнение к этим конкретным типам классов в System. 10 определено несколько перечислений, а также набор абстрактных классов (т.е. Stream, TextReader и TextWriter), которые определяют разделяемый полиморфный интерфейс для всех наследников. В этой главе вы узнаете о многих таких типах. Классы Directory (Directorylnfo) и File (Filelnfo) В System. 10 предоставляются четыре класса, которые позволяют манипулировать индивидуальными файлами, а также взаимодействовать со структурой каталогов машины. Первые два класса — Directory и File — предлагают операции создания, удаления, копирования и перемещения с использованием различных статических членов. Родственные им классы Filelnfo и Directorylnfo предлагают подобную функциональность в виде методов уровня экземпляра (и потому должны размещаться в памяти с помощью ключевого слова new). Обратите внимание на рис. 20.1, что классы Directory
Глава 20. Файловый ввод-вывод и сериализация объектов 713 и File непосредственно расширяют System.Object, в то время как Directorylnfo и Filelnfo наследуются от абстрактного класса FileSystemlnfo. г~ FileSystemlnfo Abstract Class Object Class He Class (P £ Directory Class ®N Filelnfo Class ■+ FileSystemlnfo Directorylnfo QE Class •* FileSystemlnfo Рис. 20.1. Классы для работы с файлами и каталогами Вообще говоря, Filelnfo и Directorylnfo представляют собой лучший выбор для получения полных подробностей о файле или каталоге (например, время создания, возможности чтения/записи и т.п.), поскольку их члены возвращают строго типизированные объекты. В отличие от этого, члены классов Directory и File обычно возвращают простые строковые значения, а не строго типизированные объекты. Абстрактный базовый класс FileSystemlnfo Классы Directorylnfo и Filelnfo унаследовали значительную часть своего поведения от абстрактного базового класса FileSystemlnfo. По большей части члены класса FileSystemlnfo используются для получения общих характеристик (таких как время создания, различные атрибуты и т.д.) определенного файла или каталога. В табл. 20.2 перечислены некоторые основные свойства, представляющие интерес. Таблица 20.2. Свойства класса FileSystemlnfo Свойство Назначение Attributes CreationTime Exists Extension FullName LastAccessTime LastWriteTime Получает или устанавливает ассоциированные с текущим файлом атрибуты, которые представлены перечислением FileAttributes (доступный только для чтения, зашифрованный, скрытый или сжатый) Получает или устанавливает время создания текущего файла или каталога Может использоваться для определения, существует ли данный файл или каталог Извлекает расширение файла Получает полный путь к файлу или каталогу Получает или устанавливает время последнего доступа к текущему файлу или каталогу Получает или устанавливает время последней записи в текущий файл или каталог Name Получает имя текущего файла или каталога
714 Часть V. Введение в библиотеки базовых классов .NET В классе FileSystemlnfo также определен метод Delete(). Этот метод реализуется производными типами для удаления файла или каталога с жесткого диска. Кроме того, метод Refresh() может быть вызван перед получением информации об атрибутах, чтобы обеспечить актуальность состояния статистики о текущем файле или каталоге. Работа с типом Directorylnf о Первый неабстрактный тип, связанный с вводом-выводом, который мы рассмотрим здесь — Directorylnfo. Этот класс содержит набор членов, используемых для создания, перемещения, удаления и перечисления каталогов и подкаталогов. В дополнение к функциональности, предоставленной базовым классом (FileSystemlnfo), Directorylnfo предлагает ключевые члены, перечисленные в табл. 20.3. Таблица 20.3. Ключевые члены типа Directorylnfo Член Назначение Create () Создает каталог (или набор подкаталогов) по заданному путево- CreateSubdirectoryO муимени Delete () Удаляет каталог и все его содержимое GetDirectories () Возвращает массив объектов Directorylnfo, представляющих все подкаталоги в текущем каталоге Get Files () Извлекает массив объектов Filelnfo, представляющий множество файлов в заданном каталоге MoveTo () Перемещает каталог со всем содержимым по новому пути Parent Извлекает родительский каталог данного каталога Root Получает корневую часть пути Работа с типом Directorylnfo начинается с указания определенного пути в качестве параметра конструктора. Если требуется получить доступ к текущему рабочему каталогу (т.е. каталогу выполняющегося приложения), применяйте нотацию ".". Вот некоторые примеры: // Привязаться к текущему рабочему каталогу. Directorylnfo dirl = new Directorylnfo("."); // Привязаться к C:\Windows, используя литеральную строку. Directorylnfo dir2 = new Directorylnfo(@"C:\Windows"); Во втором примере делается предположение, что переданный в конструктор путь (C:\Windows) физически существует на машине. При попытке взаимодействовать с несуществующим каталогом будет сгенерировано исключение System. 10.DirectoryNotFoundException. Таким образом, чтобы указать каталог, который пока еще не создан, сначала придется вызвать метод Create (): // Привязаться к несуществующему каталогу, затем создать его. Directorylnfo dir3 = new Directorylnfo (@"C:\MyCode\Testing"); dir3.Create (); После создания объекта Directorylnfo можно исследовать его содержимое, используя любое свойство, унаследованное от FileSystemlnfo. Для иллюстрации создайте новое консольное приложение по имени DirectoryApp и добавьте в файл кода С# импорт пространства имен System. 10. Дополните класс Program новым статическим методом, который создаст новый объект Directorylnfo, отображенный на C:\Windows (при необходимости подкорректируйте путь), и выведет ряд статистических показателей:
Глава 20. Файловый ввод-вывод и сериализация объектов 715 class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with Directory (Info) *****\nM); ShowWindowsDirectorylnfo(); Console.ReadLine(); } static void ShowWindowsDirectorylnfo() { // Вывести информацию о каталоге. Directorylnfo dir = new Directorylnfo(@"C:\Windows"); Console.WriteLine("***** Directory Info *****"); Console.WriteLine ("FullName: {0}", dir.FullName); // полное имя Console.WriteLine("Name: {0}", dir.Name); // имя каталога Console.WriteLine ("Parent: {0}", dir.Parent); // родительский каталог Console.WriteLine ("Creation: {0}", dir.CreationTime); // время создания Console.WriteLine("Attributes: {0}", dir.Attributes); // атрибуты Console.WriteLine ("Root: {0}", dir.Root); // корневой каталог Console.WriteLine("************************** \n" ); } } Вывод будет выглядеть примерно так, как показано ниже: ***** pun wj_th Directory(Info) ***** ***** Directory Info ***** FullName: C:\Windows Name: Windows Parent: Creation: 7/13/2009 10:20:08 PM Attributes: Directory Root: C:\ •••••••••••••••••••••••••• Перечисление файлов с помощью типа Directorylnfo В дополнение к получению базовых деталей о существующем каталоге можно расширить текущий пример использованием некоторых методов типа Directorylnfo. Для начала применим метод GetFiles () для получения информации обо всех файлах *.jpg, расположенных в каталоге C:\Windows\Web\Wallpaper. На заметку! Если на вашей машине нет каталога C:\Windows\Web\Wallpaper, измените код для чтения файлов из какого-то существующего каталога (например, прочитайте все файлы *.bmp из каталога C:\Windows). Метод GetFiles () возвращает массив объектов типа Filelnfo, каждый из которых представляет детальную информацию о конкретном файле (все подробности о типе Filelnfo будут описаны далее в этой главе). Предположим, что следующий статический метод класса Program вызывается в методе Main(): static void DisplaylmageFiles () { Directorylnfo dir = new Directorylnfo(@"C:\Windows\Web\Wallpaper"); // Получить все файлы с расширением *.jpg. FileInfo[] imageFiles = dir.GetFiles("*.jpg", SearchOption.AllDirectones) ; // Сколько файлов найдено? Console.WriteLine("Found {0} *.jpg files\n", imageFiles.Length);
716 Часть V. Введение в библиотеки базовых классов .NET // Вывести информацию о каждом файле. foreach (Filelnfo f in imageFiles) Console.WriteLine( Console.WriteLine( Console.WriteLine( Console.WriteLine( Console.WriteLine( Console.WriteLine( lr ***** • ) ; File name: {0}", f.Name); // имя File size: {0}", f.Length); // размер Creation: {0}", f.CreationTime); // время создания Attributes: {0}", f.Attributes); // атрибуты r ****** t! lr****************** \n") j } Обратите внимание на указание в вызове GetFiles () опции поиска; SearchOption. AllDirectories обеспечивает просмотр всех подкаталогов корня. После запуска этого приложения получается список файлов, отвечающих критерию поиска. Создание подкаталогов с помощью типа Directorylnf о Для программного расширения структуры каталогов служит метод Directorylnf о. CreateSubdirectory (). С его помощью можно создавать как одиночный подкаталог, так и множество вложенных подкаталогов за один вызов. Для иллюстрации ниже приведен метод, который расширяет структуру диска С: дополнительными подкаталогами: static void ModifyAppDirectory () { Directorylnfo dir = new Directorylnfo (@"C:\"); // Создать \MyFolder в каталоге приложения. dir.CreateSubdirectory("MyFolder"); // Создать \MyFolder2\Data в каталоге приложения. dir .CreateSubdirectory (@"MyFolder2\Data") ; } Вызвав этот метод в Main() и выполнив программу, в проводнике Windows можно будет увидеть новые подкаталоги (рис. 20.2). ПИ^Я Ь > l ninputef » Mongo Drive (G) ► Myfdder2 ► Organize *■ Include in library » Share with » Burn И ШЯЯШША ', Ь MyFolder - ii MyFolder2 jk Data PerfLogs -• , Program Files Program Files (x86) ProgramDataTechSmith > Ji; Users > * Windows > JJ CD Drive (R) >uMy Passport (bt) Name Data Date modified | 2/1/2010 4:06 p[| 1 Рис. 20.2. Результат создания подкаталогов Хотя получать возвращаемое значение метода CreateSubdirectoryO не обязательно, имейте в виду, что в случае его успешного выполнения возвращается объект Directorylnfo, представляющий вновь созданный элемент. Рассмотрим следующую модификацию предыдущего метода:
Глава 20. Файловый ввод-вывод и сериализация объектов 717 static void ModifyAppDirectory() { Directorylnfo dir = new Directorylnfo(".") ; // Создать \MyFolder в начальном каталоге. dir.CreateSubdirectory("MyFolder"); // Получить возвращенный объект Directorylnfo. Directorylnfo myDataFolder = dir.CreateSubdirectory(@MMyFolder2\DataM); // Напечатать путь ..\MyFolder2\Data. Console.WriteLine("New Folder is: {0}", myDataFolder); } Работа с типом Directory После опробования типа Directorylnfo в действии можно приступать к изучению типа Directory. По большей части статические члены Directory повторяют функциональность, предоставленную членами уровня экземпляра, которые определены в Directorylnfo. Вспомните, однако, что члены Directory обычно возвращают строковые данные вместо строго типизированных объектов Filelnfo/Directorylnfo. Теперь рассмотрим некоторую функциональность типа Directory, приведенная ниже последняя вспомогательная функция отображает имена всех устройств, представленных на текущем компьютере (через метод Directory.GetLogicalDrivesO), и использует статический метод Directory.Delete () для удаления созданных ранее подкаталогов \MyFolder и \MyFolder2\Data: static void FunWithDirectoryType () { // Перечислить все дисковые устройства данного компьютера. string [] drives = Directory.GetLogicalDrivesO; Console.WriteLine ("Here are your drives:"); foreach (string s in drives) Console.WriteLine (" — > {0} ", s) ; // Удалить то, что было ранее создано. Console.WriteLine ("Press Enter to delete directories"); Console.ReadLine(); try { Directory.Delete(@"C:\MyFolder"); // Второй параметр указывает, нужно ли удалять все подкаталоги. Directory.Delete(@"C:\MyFolder2", true); } catch (IOException e) { Console.WriteLine(e.Message); } } Исходный код. Проект DirectoryApp доступен в подкаталоге Chapter 20. Работа с типом Drivelnf о Пространство имен System. 10 включает класс Drivelnf о. Подобно Directory. GetLogicalDrivesO, статический метод Drivelnfo.GetDrivesO позволяет получить имена дисковых приводов машины.
718 Часть V. Введение в библиотеки базовых классов .NET Однако, в отличие от Directory.GetLogicalDrives (), Drivelnfo предоставляет множество дополнительных деталей (таких как тип привода, доступное свободное пространство и метка тома). Рассмотрим следующий класс Program, определенный в новом консольном приложении по имени DrivelnfoApp (не забудьте импортировать пространство имен System. 10): class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with Drivelnfo *****\nM); // Получить информацию обо всех приводах. Drivelnfo[] myDrives = Drivelnfo.GetDrives(); // Напечатать состояние приводов. foreach(Drivelnfo d in myDrives) { Console.WriteLine("Name: {0}", d.Name); // Имя Console.WriteLine("Type: {0}", d.DriveType); //Тип // Проверить, смонтирован ли диск. if (d.IsReady) { Console.WriteLine("Free space: {0}", d.TotalFreeSpace); // Свободное // пространство Console.WriteLine ("Format: {0}", d.DriveFormat); // Формат Console.WriteLine("Label: {0}", d.VolumeLabel); //Метка Console.WriteLine(); Console.ReadLine(); } } Ниже показан возможный вывод: ***** pun with Drivelnfo ***** Name: C:\ Type: Fixed Free space: 587376394240 Format: NTFS Label: Mongo Drive Name: D:\ Type: CDRom Name: E:\ Type: CDRom Name: F:\ Type: CDRom Name: H:\ Type: Fixed Free space: 477467508736 Format: FAT32 Label: My Passport Пока что были исследованы только некоторые основы поведения классов Directory, Directorylnfo и Drivelnfo. Далее будет показано, как создавать, открывать, закрывать и удалять файлы, наполняющие данный каталог. Исходный код. Проект DrivelnfoApp доступен в подкаталоге Chapter 20.
Глава 20. Файловый ввод-вывод и сериализация объектов 719 Работа с классом Filelnf о Как было показано в предыдущем примере DirectoryApp, класс Filelnf о позволяет получать подробности относительно существующих файлов на жестком диске (т.е. время создания, размер и атрибуты) и предназначен для создания, копирования, перемещения и удаления файлов. Вдобавок к набору функциональности, унаследованной от FileSystemlnfo, есть некоторые члены, уникальные для класса Filelnfo, которые описаны в табл. 20.4. Таблица 20.4. Основные члены Filelnfo Член Назначение AppendText () Создает объект StreamWriter (описанный ниже) и добавляет текст в файл СоруТо () Копирует существующий файл в новый файл Create () Создает новый файл и возвращает объект FileStream (описанный ниже) для взаимодействия с вновь созданным файлом CreateTextO Создает объект StreamWriter, записывающий новый текстовый файл Delete () Удаляет файл, к которому привязан экземпляр Filelnfo Directory Получает экземпляр родительского каталога DirectoryName Получает полный путь к родительскому каталогу Length Получает размер текущего файла или каталога MoveTo () Перемещает указанный файл в новое местоположение, предоставляя возможность указать новое имя файла Name Получает имя файла Open () Открывает файл с различными привилегиями чтения/записи и совместного доступа OpenReadO Создает доступный только для чтения объект FileStream OpenTextO Создает объект StreamReader (описанный ниже) и читает из существующего текстового файла OpenWrite () Создает доступный только для записи объект FileStream Обратите внимание, что большинство методов класса Filelnfo возвращают специфический объект ввода-вывода (т.е. FileStream и StreamWriter), который позволяет начать чтение и запись данных в ассоциированный файл в разнообразных форматах. Скоро мы рассмотрим эти типы; однако прежде чем увидеть работающий пример, давайте изучим различные способы получения дескриптора файла с использованием класса File In ft). Метод Filelnf о. Create () Один из способов создания дескриптора файла предусматривает использование метода Filelnfo. Create(): static void Main(string [ ] args) { // Создать новый файл на диске С: . Filelnfo f = new Filelnfo(@"C:\Test.dat"); FileStream fs = f.Create();
720 Часть V. Введение в библиотеки базовых классов .NET // Использовать объект FileStream... // Закрыть файловый поток. fs.CloseO ; } Метод Filelnfo.Create() возвращает тип FileStream, который предоставляет синхронную и асинхронную операции записи/чтения лежащего в его основе файла. Имейте в виду, что объект FileStream, возвращенный Filelnfo.CreateO, открывает полный доступ по чтению и записи всем пользователям. Также имейте в виду, что после окончания работы с текущим объектом FileStream следует закрыть его дескриптор, чтобы освободить лежащие в основе потока неуправляемые ресурсы. Учитывая, что FileStream реализует интерфейс IDisposable, можно применить контекст С# using и позволить компилятору сгенерировать логику завершения (подробности ищите в главе 8). static void Main(string [ ] args) { // Определение контекста using для файлового ввода-вывода. Filelnfo f = new Filelnfo(@"C:\Test.dat") ; using (FileStream fs = f.CreateO) { // Использовать объект FileStream. .. } } Метод Filelnfo.Open() С помощью метода Filelnfo.OpenO можно открывать существующие файлы, а также создавать новые файлы с гораздо более высокой точностью, чем Filelnfo.CreateO, учитывая, что Ореп() обычно принимает несколько параметров для описания общей структуры файла, с которым будет производиться работа. В результате вызова Ореп() получается возвращенный им объект FileStream. Взгляните на следующую логику: static void Main(string [ ] args) { // Создать новый файл через Filelnfo.OpenO • Filelnfo f2 = new Filelnfo(@"C:\Test2.dat") ; using(FileStream fs2 = f2.Open(FileMode.OpenOrCreate, FileAccess .ReadWnte, FileShare .None) ) { // Использовать объект FileStream... } } Эта версия перегруженного метода Ореп() требует трех параметров. Первый параметр указывает общий тип запроса ввода-вывода (т.е. создать новый файл, открыть существующий файл и дописать в файл), указываемый в виде перечисления FileMode (описание членов дано в табл. 20.5): public enum FileMode { CreateNew, Create, Open, OpenOrCreate, Truncate, Append }
Глава 20. Файловый ввод-вывод и сериализация объектов 721 Таблица 20.5. Члены перечисления FileMode Член Назначение CreateNew Информирует операционную систему о создании нового файла. Если файл уже существует, генерируется исключение IOException Create Информирует операционную систему о создании нового файла. Если файл уже существует, он будет перезаписан Open Открывает существующий файл. Если файл не существует, генерируется исключение FileNotFoundException OpenOrCreate Открывает файл, если он существует; в противном случае создает новый Truncate Открывает файл и усекает его до нулевой длины Append Открывает файл, переходит в его конец и начинает операции чтения (этот флаг может быть использован только с потоками, доступными лишь для чтения). Если файл не существует, то создается новый Второй параметр метода Ореп() — значение перечисления FileAccess — используется для определения поведения чтения/записи лежащего в основе потока: public enum FileAccess { Read, Write, ReadWrite } И, наконец, третий параметр метода Open() — FileShare — указывает, как файл может быть разделен с другими файловыми дескрипторами. Ниже перечислены его возможные значения: public enum FileShare { Delete, Inheritable, None, Read, ReadWrite, Write } Методы FileOpen.OpenReadQ и Filelnfo.OpenWriteQ Хотя метод FileOpen.Open() позволяет получить дескриптор файла довольно гибким способом, в классе Filelnfo также предусмотрены для этого члены OpenReadO и OpenWrite(). Как и можно было ожидать, эти методы возвращают объект FileStream, соответствующим образом сконфигурированный только для чтения или только для записи, без необходимости применять различные значения перечислений. Подобно Filelnfo.CreateO и Filelnfo.Open(), методы OpenReadO HOpenWrite() возвращают объект FileStream (обратите внимание, что в следующем коде предполагается наличие файлов по имени Test3.dat и Test4.dat на диске С:). static void Main(string[] args) { // Получить объект FileStream с правами только для чтения. Filelnfo f3 = new Filelnfo (@"C:\Test3.dat");
722 Часть V. Введение в библиотеки базовых классов .NET using(FileStream readOnlyStream = f3.OpenRead()) { // Использовать объект FileStream... } // Теперь получить объект FileStream с правами только для записи. Filelnfo f4 = new Filelnfo(@"C:\Test4.dat"); using(FileStream writeOnlyStream = f4.OpenWrite ()) { // Использовать объект FileStream... } } Метод Filelnfo.OpenText() Еще один член типа Filelnfo, связанный с открытием файлов — OpenText(). В отличие от Create (), Open (), OpenRead () и OpenWrite (), метод OpenText () возвращает экземпляр типа StreamReader, а не FileStream. Исходя из того, что на диске С: уже есть файл по имени boot.ini, получить доступ к его содержимому можно следующим образом: static void Main(string[] args) { // Получить объект StreamReader. Filelnfo f5 = new Filelnfo(@"C:\boot.ini"); using(StreamReader sreader = f5.OpenText ()) { // Использовать объект StreamReader... } } Как вскоре будет показано, тип StreamReader предоставляет способ чтения символьных данных из лежащего в основе файла. Методы Filelnfo.CreateTextQ и Filelnfo.AppendTextQ Последними двумя методами, представляющими интерес, являются CreateTextO и AppendText(). Оба возвращают объект StreamWriter, как показано ниже: static void Main(string [ ] args) { Filelnfo f6 = new Filelnfo(@"C:\Test6.txt"); using(StreamWriter swriter = f6.CreateText()) { // Использовать объект StreamWriter... } Filelnfo f7 = new Filelnfo(@"C:\FinalTest.txt"); using(StreamWriter swriterAppend = f7.AppendText()) { // Использовать объект StreamWriter... } } Как и можно было ожидать, тип StreamWriter предлагает способ записи данных в лежащий в основе файл. Работа с типом File Тип File предоставляет функциональность, почти идентичную типу Filelnfo, с помощью нескольких статических методов. Подобно Filelnfo, тип File поддерживает методы AppendText(), CreateO, CreateTextO, Open(), OpenReadO, OpenWriteO HOpenText().
Глава 20. Файловый ввод-вывод и сериализация объектов 723 Фактически во многих случаях типы File и Filelnf о могут использоваться взаимозаменяемым образом. Для иллюстрации каждый из предшествующих примеров применения FileStream можно упростить, используя вместо него тип File. static void Main(string[] arqb) { II Получить объект FileStream через File.Create () . using(FileStream fs = File.Create(@"C:\Test.dat")) {} // Получить объект FileStream через File.Open(). using(FileStream fs2 = File.Open(@MC:\Test2.dat", FileMode.OpenOrCreate, FileAccess .ReadWrite, FileShare .None) ) {} // Получить объект FileStream с правами только для чтения. using(FileStream readOnlyStream = File.OpenRead(@"Test3.dat")) U // Получить объект FileStream с правами только для записи. using(FileStream writeOnlyStream = File.OpenWrite(@"Test4.dat")) {} // Получить объект StreamReader. using (StreamPeacler sreader = File .OpenText (@"C : \boot. mi" ) ) {} // Получить несколько объектов StreamWriter. using (StreamWriter swriter = File.CreateText(@"C:\Test6.txt")) {} using (StreamWriter swriterAppend = File.AppendText(@"C:\FinalTest.txt")) {} } Дополнительные члены File Тип File также поддерживает несколько уникальных членов, перечисленных в табл. 20.6, которые могут значительно упростить процесс чтения и записи текстовых данных. Таблица 20.6. Методы типа File Метод Назначение ReadAllBytes () Открывает указанный файл, возвращает двоичные данные в виде массива байт и затем закрывает файл ReadAllLines () Открывает указанный файл, возвращает символьные данные в виде массива строк, затем закрывает файл ReadAllText () Открывает указанный файл, возвращает символьные данные в виде System.StringO, затем закрывает файл WriteAllBytesO Открывает указанный файл, записывает в него байтовый массив и закрывает файл WriteAllLines () Открывает указанный файл, записывает в него массив строк и закрывает файл WriteAllTextO Открывает указанный файл, записывает в него данные из указанной строки и закрывает файл
724 Часть V. Введение в библиотеки базовых классов .NET Используя эти новые методы типа File, можно осуществлять чтение и запись пакетов данных с помощью всего нескольких строк кода. Еще лучше то, что эти новые методы автоматически закрывают лежащий в основе файловый дескриптор. Например, следующая консольная программа (по имени SimpleFilelO) сохраняет строковые данные в новом файле на диске С: (и читает их в память) с минимальными усилиями (предполагается, что был произведен импорт System. 10): class Program { static void Main(string[] args) { Console.WriteLine ("***** Simple 10 with the File Type *****\nM); stnng[] myTasks = { "Fix bathroom sink", "Call Dave", "Call Mom and Dad", "Play Xbox 360"}; // Записать все данные в файл на диске С. File.WriteAllLines(@"C:\tasks.txt", myTasks); // Прочитать все обратно и распечатать. foreach (string task in File.ReadAllLines(@"C:\tasks.txt") ) { Console.WriteLine("TODO: {0}", task); } Console.ReadLine(); } } Отсюда вывод: когда необходимо быстро получить файловый дескриптор, тип File сэкономит некоторый объем ввода. Однако преимущество предварительного создания объекта Filelnf о связано с возможностью исследования файла с использованием членов абстрактного базового класса FileSystemlnfo. Исходный код. Проект SimpleFilelO доступен в подкаталоге Chapter 20. Абстрактный класс Stream К данному моменту вы уже ознакомились с несколькими способами получения объектов FileStream, StreamReader и StreamWriter, но еще нужно читать данные и записывать их в файл, использующий эти типы. Чтобы понять, как это делается, необходимо изучить концепцию потока. В мире манипуляций вводом-выводом поток (stream) представляет порцию данных, протекающую от источника к цели. Потоки предоставляют общий способ взаимодействия с последовательностью байгги независимо от того, какого рода устройство (файл, сеть, соединение, принтер и т.п.) хранит или отображает эти байты. В абстрактном классе System. 10.Stream определен набор членов, которые обеспечивают поддержку синхронного и асинхронного взаимодействия с хранилищем (например, файлом или областью памяти). На заметку! Концепция потока не ограничена файловым вводом-выводом. Точности ради следует отметить, что библиотеки .NET предоставляют потоковый доступ к сетям, областям памяти и прочим абстракциям, связанным с потоками. Опять-таки, потомки класса Stream представляют данные, как низкоуровневые потоки байт, а непосредственная работа с низкоуровневыми потоками может оказаться довольно загадочной. Некоторые типы, унаследованные от Stream, поддерживают по-
Глава 20. Файловый ввод-вывод и сериализация объектов 725 иск (seeking), что означает возможность получения и изменения текущей позиции в потоке. Чтобы приблизиться к пониманию функциональности класса Stream, рассмотрим список основных его членов, приведенный в табл. 20.7. Таблица 20.7. Члены абстрактного класса Stream Член Назначение CanRead Определяют, поддерживает ли текущий поток чтение, поиск и/или запись CanWrite CanSeek Close () Закрывает текущий поток и освобождает все ресурсы (такие как сокеты и файловые дескрипторы), ассоциированные с текущим потоком. Внутренне этот метод является псевдонимом Dispose (). Поэтому закрытие потока функционально эквивалентно освобождению потока Flush () Обновляет лежащий в основе источник данных или репозиторий текущим состоянием буфера с последующей очисткой буфера. Если поток не реализует буфер, метод не делает ничего Length Возвращает длину потока в байтах Position Определяет текущую позицию в потоке Read () Читает последовательность байт (или одиночный байт) из текущего потока и ReadBy te () перемещает текущую позицию потока на количество прочитанных байтов Seek() Устанавливает позицию в текущем потоке SetLength () Устанавливает длину текущего потока Write () Пишет последовательность байт (или одиночный байт) в текущий поток и пе- WriteBy te () ремещает текущую позицию на количество записанных байтов Работа с классом FileStream Класс FileStream предоставляет реализацию абстрактного члена Stream в манере, подходящей для потоковой работы с файлами. Это элементарный поток, и он может записывать или читать только один байт или массив байтов. Однако взаимодействовать с членами типа FileStream придется нечасто. Вместо этого, скорее всего, будут использоваться оболочки потоков, которые облегчают работу с текстовыми данными или типами .NET. Тем не менее, в целях иллюстрации полезно поэкспериментировать с возможностями синхронного чтения/записи типа FileStream. Предположим, что имеется консольное приложение под названием FileStreamApp, и в файле кода С# выполнен импорт пространств имен System. 10 и System.Text. Цель состоит в записи простого текстового сообщения в новый файл по имени myMessage .dat. Однако, учитывая, что FileStream может работать только с низкоуровневыми байтами, придется закодировать тип System.String в соответствующий байтовый массив. К счастью, в пространстве имен System.Text определен тип по имени Encoding, который содержит члены, способные кодировать и декодировать строки в массивы байт (тип Encoding подробно описан в документации .NET Framework 4.0 SDK). Закодированный массив байт сохраняется в файле с помощью метода FileStream. Write(). Чтобы прочитать байты обратно в память, необходимо сбросить внутреннюю позицию потока (через свойство Position) и вызвать метод ReadByte(). И, наконец, на консоль выводится низкоуровневый байтовый массив и декодированная строка. Ниже приведен полный текст метода Main().
726 Часть V. Введение в библиотеки базовых классов .NET //Не забудьте импортировать пространства имен System.Text и System.10! static void Main(string [ ] args) { Console.WriteLine ("***** Fun with FileStreams *****\n"); // Получить объект FileStream. using(FileStream fStream = File.Open(@"C:\myMessage.dat", FileMode.Create)) { // Закодировать строку в виде массива байт. string msg = "Hello!"; byte[] msgAsByteArray = Encoding.Default.GetBytes(msg); // Записать byte[] в файл. fStream.Write(msgAsByteArray, 0, msgAsByteArray.Length); // Сбросить внутреннюю позицию потока. fStream.Position = 0; // Прочитать типы из файла и вывести на консоль. Console.Write("Your message as an array of bytes: "); byte[] bytesFromFile = new byte[msgAsByteArray.Length]; for (int i = 0; i < msgAsByteArray.Length; i++) { bytesFromFile[l] = (byte)fStream.ReadByte(); Console.Write(bytesFromFile[i]); } // Вывести декодированные сообщения. Console.Write("\nDecoded Message: " ) ; Console.WriteLine(Encoding.Default.GetString(bytesFromFile)); } Console.ReadLine(); } В этом примере производится не только наполнение файла данными, но также демонстрируется основной недостаток прямой работы с типом FileStream: приходится оперировать низкоуровневыми байтами. Другие унаследованные от Stream типы работают аналогично. Например, чтобы записать последовательность байт в область памяти, необходимо создать объект MemoryStream. Аналогично, для передачи массива байт по сетевому соединению используется класс Net works t ream (из пространства имен System. Net. Sockets). Как уже упоминалось, в пространстве имен System. 10 доступно множество типов для чтения и записи, которые инкапсулируют детали работы с типами-наследниками Stream. Исходный код. Проект FileStreamApp доступен в подкаталоге Chapter 20. Работа с классами StreamWriter и StreamReader Классы StreamWriter и StreamReader удобны во всех случаях, когда нужно читать или записывать символьные данные (например, строки). Оба типа работают по умолчанию с символами Unicode; однако это можно изменить предоставлением правильно сконфигурированной ссылки на объект System.Text .Encoding. Чтобы не усложнять пример, предположим, что кодировка по умолчанию Unicode вполне устраивает. Класс StreamReader, как и StringReader (о котором речь пойдет далее в этой главе), унаследован от абстрактного класса по имени Text Reader. Базовый класс предла-
Глава 20. Файловый ввод-вывод и сериализация объектов 727 гает очень ограниченный набор функциональности каждому из его наследников, в частности — возможность читать и "заглядывать" (peek) в символьный поток. Класс StreamWriter (как и StringWriter, о котором речь пойдет ниже) наследуется от абстрактного базового класса по имени Тех tW rite г. В этом классе определены члены, позволяющие производным типам записывать текстовые данные в заданный символьный поток. Чтобы приблизить вас к пониманию основных возможностей записи классов StreamWriter и StringWriter, в табл. 20.8 представлены описания основных членов абстрактного базового класса ТехtWriter. Таблица 20.8. Основные члены TextWriter Член Назначение Close () Этот метод закрывает объект-писатель и освобождает все связанные с ним ресурсы. В процессе автоматически сбрасывается буфер (опять-таки, этот член функционально эквивалентен методу Dispose()) Flush () Этот метод очищает все буферы текущего объекта-писателя и записывает все буферизованные данные на лежащее в основе устройство, однако, не закрывает его NewLine Это свойство задает константу перевода строки для унаследованного класса писателя. По умолчанию ограничителем строки в Windows является возврат каретки, за которым следует перевод строки (\г\п) Write () Этот перегруженный метод записывает данные в текстовый поток без добавления константы новой строки WriteLine () Этот перегруженный метод записывает данные в текстовый поток с добавлением константы новой строки На заметку! Последние два члена класса TextWriter, скорее всего, покажутся знакомыми. Если помните, тип System.Console имеет члены Write() и WriteLine(), которые выталкивают текстовые данные на стандартное устройство вывода. Фактически свойство Console.In хранит TextWriter, a Console.Out — TextWriter. Унаследованный класс StreamWriter предоставляет соответствующую реализацию методов Write(), Close () и Flush(), а также определяет дополнительное свойство AutoFlush. Когда это свойство установлено в true, оно заставляет StreamWriter выталкивать данные при каждой операции записи. Имейте в виду, что можно обеспечить более высокую производительность, установив AutoFlush в false, но при этом всегда вызывать Close () по завершении работы с StreamWriter. Запись в текстовый файл Чтобы увидеть класс StreamWriter в действии, создадим новое консольное приложение по имени StreamWriterReaderApp и импортируем пространство имен System. 10. В показанном ниже методе Main() создается новый файл по имени reminders.txt с помощью метода File.CreateText(). Используя полученный объект StreamWriter, в новый файл будут добавлены некоторые текстовые данные. static void Main(string [ ] args) { Console.WriteLine ("***** Fun with StreamWriter / StreamReader *****\n");
728 Часть V. Введение в библиотеки базовых классов .NET // Получить StreamWriter и записать строковые данные. using (StreamWnter writer = File .CreateText ( "reminders . txt") ) { writer.WriteLine("Don't forget Mother's Day this year..."); writer.WriteLine("Don't forget Father's Day this year..."); writer.WriteLine("Don't forget these numbers:"); for(int i = 0; l < 10; i++) writer.Write (l + " "); // Вставить новую строку. writer.Write (writer.NewLine); } Console.WriteLine("Created file and wrote some thoughts..."); Console.ReadLine(); } После выполнения этой программы можно просмотреть содержимое созданного файла (рис. 20.3). Этот файл находится в папке bin\Debug текущего приложения, поскольку при вызове CreateText () абсолютный путь не указывался. | reminders.txt Don't forget Mother's Day this year... Don't forget Father's Day this year... Don't forget these nimbers: 0123456789 100% Рис. 20.3. Содержимое созданного текстового файла Чтение из текстового файла Теперь давайте посмотрим, как программно прочитать данные из файла, используя соответствующий тип StreamReader. Как вы помните, этот класс унаследован от абстрактного класса TextReader, который обеспечивает функциональность, описанную в табл. 20.9. Таблица 20.9. Основные члены TextReader Член Назначение Peek() Возвращает следующий доступный символ, не изменяя текущей позиции читателя. Значение -1 указывает на достижение конца потока Read () Читает данные из входного потока ReadBlockO Читает указанное максимальное количество символов из текущего потока и записывает данные в буфер, начиная с заданного индекса ReadLine () Читает строку символов из текущего потока и возвращает данные в виде строки (null-строка указывает на признак конца файла (EOF)) ReadToEnd () Читает все символы от текущей позиции до конца потока и возвращает их в виде одной строки Если теперь расширить пример приложения StreamWriterReaderApp, чтобы в нем применялся класс StreamReader, то можно будет прочитать текстовые данные из файла reminders.txt:
Глава 20. Файловый ввод-вывод и сериализация объектов 729 static void Main(string [ ] args) { Console.WriteLine ("***** Fun with StreamWriter / StreamReader *****\n"); // Прочитать данные из файла. Console.WriteLine ("Here are your thoughts:\n"); using(StreamReader sr = File.OpenText("reminders.txt")) { string input = null; while ((input = sr.ReadLine ()) != null) { Console.WriteLine (input); } } Console.ReadLine(); } После запуска этой программы на консоли отображаются символьные данные из файла reminders.txt. Прямое создание экземпляров классов StreamWriter/StreamReader Одним из сбивающих с толку аспектов работы с типами, которые входят в пространство имен System. 10, является то, что одного и того же результата часто можно достичь с использованием разных подходов. Например, ранее уже было показано, что с помощью метода CreateText() можно получить объект StreamWriter с типом File или Filelnfo. В действительности есть и еще один способ работы с объектами StreamWriter и StreamReader: создавая их напрямую. Например, текущее приложение можно было бы переделать следующим образом: static void Main(string [ ] args) { Console.WriteLine (••***** Fun with StreamWriter / StreamReader *****\n"); // Получить StreamWriter и записать строковые данные. using(StreamWriter writer = new StreamWriter("reminders.txt")) // Прочитать данные из файла. using (StreamReader sr = new StreamReader("reminders.txt")) { } } Несмотря на то что существованием множества на первый взгляд идентичных подходов к файловому вводу-выводу может обескуражить, следует понимать, что в результате достигается более высокая гибкость. Теперь, когда известно, как перемещать символьные данные в файл и из файла с использованием классов StreamWriter и StreamReader, рассмотрим роль классов StringWriter и StringReader. Исходный код. Проект StreamWriterReaderApp доступен в подкаталоге Chapter 20.
730 Часть V. Введение в библиотеки базовых классов .NET Работа с классами StringWriter и StringReader Классы StringWriter и StringReader можно использоваться для трактовки текстовой информации как потока символов, находящихся в памяти. Это может помочь, когда требуется добавить символьную информацию к лежащему в основе буферу. Для иллюстрации в следующем консольном приложении (по имени StringReaderWriterApp) блок строковых данных записывается в объект StringWriter вместо файла на локальном жестком диске: static void Main(string [ ] args) { Console.WriteLine (••***** Fun with StringWriter / StringReader *****\n"); // Создать StringWriter и записать символьные данные в память. using(StringWriter strWriter = new StringWriter()) { strWriter.WriteLine("Don't forget Mother's Day this year..."); // Получить копию содержимого (хранящегося в строке) и вывести на консоль. Console.WriteLine("Contents of StringWriter:\n{0}", strWriter); } Console.ReadLine(); } Поскольку и StringWriter, и StreamWriter унаследованы от одного и того же базового класса (TextWriter), логика записи в какой-то мере похожа. Однако, учитывая природу StringWriter, имейте в виду, что этот класс позволяет использовать метод GetStringBuilder() для извлечения объекта System.Text.StringBuilder: using (StringWriter strWriter = new StringWriter ()) { strWriter.WriteLine("Don't forget Mother's Day this year..."); Console.WriteLine("Contents of StringWriter:\n{0}", strWriter); // Получить внутренний StringBuilder. StringBuilder sb = strWriter.GetStringBuilder (); sb.Insert @, "Hey1 ! ") ; Console.WriteLine ("-> {0}", sb.ToString()); sb.Remove@, "Hey!! ".Length); Console.WriteLine ("-> {0}", sb.ToString ()) ; } Когда необходимо выполнить чтение из потока строковых данных, используйте соответствующий тип StringReader, который (как и можно было ожидать) функционирует идентично классу StreamReader. Фактически класс StringReader не делает ничего помимо переопределения унаследованных членов для чтения блока символьных данных вместо чтения файла: using (StringWriter strWriter = new StringWriter()) { strWriter.WriteLine("Don't forget Mother's Day this year..."); Console.WriteLine("Contents of StringWriter:\n{0}", strWriter); // Читать данные из StringWriter. using (StringReader strReader = new StringReader(strWriter.ToString()) ) { string input = null; while ((input = strReader.ReadLine()) != null) { Console.WriteLine(input); } } }
Глава 20. Файловый ввод-вывод и сериализация объектов 731 Исходный код. Проект StringReaderWriterApp доступен в подкаталоге Chapter 20. Работа с классами BinaryWriter и BinaryReader И последний набор классов читателей и писателей, которые рассматриваются в настоящем разделе — это BinaryWriter и BinaryReader; оба они являются прямыми наследниками System.Object. Эти типы позволяют читать и записывать дискретные типы данных в потоки в компактном двоичном формате. В классе BinaryWriter определен многократно перегруженный метод Write () для помещения типов данных в лежащий в основе поток. В дополнение к Write (), класс BinaryWriter предоставляет дополнительные члены, позволяющие получать или устанавливать объекты унаследованных от Stream типов, а также поддерживает произвольный доступ к данным (табл. 20.10). Таблица 20.10. Основные члены BinaryWriter Член Назначение BaseStream Это свойство, предназначенное только для чтения, обеспечивает доступ к лежащему в основе потоку, используемому объектом BinaryWriter Close () Этот метод закрывает двоичный поток Flush () Этот метод выталкивает буфер двоичного потока Seek() Этот метод устанавливает позицию в текущем потоке Write () Этот метод пишет значение в текущий поток Основные члены класса BinaryReader перечислены в табл. 20.11. Таблица 20.11. Основные члены BinaryReader Член Назначение BaseStream Это свойство, предназначенное только для чтения, обеспечивает доступ к лежащему в основе потоку, используемому объектом BinaryReader Close () Этот метод закрывает двоичный поток PeekChar () Этот метод возвращает следующий доступный символ без перемещения текущей позиции потока Read () Этот метод читает заданный набор байт или символов и сохраняет их в переданном ему массиве ReadXXXXO В классе BinaryReader определено множество методов чтения, которые извлекают из потока объекты различных типов (ReadBooleanO, ReadByteO, Readlnt32() и т.д.) В следующем примере (консольное приложение по имени Binary Write r Reader) объекты данных разных типов записываются в файл *.dat: static void Main(string[] args) { Console.WriteLine ("***** Fun with Binary Writers / Readers *****\n"); // Открыть двоичную запись в файл. Filelnfo f = new Filelnfo("BinFile.dat") ;
732 Часть V. Введение в библиотеки базовых классов .NET using(BinaryWriter bw = new BinaryWriter(f.OpenWrite())) { // Вывести на консоль тип BaseStream (System.10.FileStream в данном случае). Console.WriteLine("Base stream is: {0}", bw.BaseStream); // Создать некоторые данные для сохранения в файле. double aDouble = 1234.67; int anlnt = 34567; string aString = "А, В, C"; // Записать данные. bw.Write(aDouble); bw.Write(anlnt); bw.Write(aString); } Console . ReadLme () ; } Обратите внимание, что объект FileStream, возвращенный методом Filelnfo. OpenWrite(), передается конструктору типа BinaryWriter. Используя эту технику, очень просто организовать по уровням поток перед записью данных. Имейте в виду, что конструктор BinaryWriter принимает любой тип, унаследованный от Stream (т.е. FileStream, MemoryStream или BufferedStream). Таким образом, если необходимо записать двоичные данные в память, просто используйте объект MemoryStream. Для чтения данных из файла BinFile.dat в классе BinaryReader предлагается ряд опций. Например, ниже будут вызываться различные члены, выполняющие чтение, для извлечения каждого фрагмента данных из файлового потока: static void Main(string [ ] args) { Filelnfo f = new Filelnfo("BinFile.dat"); // Читать двоичные данные из потока. using(BinaryReader br = new BinaryReader(f.OpenRead()) ) { Console.WriteLine(br.ReadDouble()); Console.WriteLine(br.Readlnt32()); Console.WriteLine(br.ReadString()); } Console.ReadLine() ; } Исходный код. Проект BinaryWriterReader доступен в подкаталоге Chapter 20. Программное отслеживание файлов Теперь, когда вы научились использовать различные классы для чтения и записи, следующая задача — изучить роль класса FileSystemWatcher. Этот тип довольно полезен, когда требуется программно отслеживать состояние файлов в системе. В частности, с помощью FileSystemWatcher можно организовать слежение за файлами на предмет выполнения с ними действий, указанных в перечислении System. 10.NotifyFilters (подробно описано в документации .NET Framework 4.0 SDK): public enum NotifyFilters { Attributes, CreationTime, DirectoryName, FileName, LastAccess, LastWnte, Security, Size, }
Глава 20. Файловый ввод-вывод и сериализация объектов 733 Первый шаг, который необходимо предпринять при работе с типом FileSystemWatcher — это установить свойство Path, чтобы оно указывало имя (и местоположение) каталога, содержащего файлы, которые нужно отслеживать, а также свойство Filter, определяющее расширения упомянутых файлов. Сейчас можно выбрать обработку событий Changed, Created и Deleted — все они работают в сочетании с делегатом FileSystemE vent Handler. Этот делегат может вызывать любой метод, соответствующий следующей сигнатуре: // Делегат FileSystemEventHandler должен указывать //на метод, соответствующий следующей сигнатуре. void MyNotificationHandler(object source, FileSystemEventArgs e) Событие Renamed также может быть обработано делегатом типа RenamedEventHandler, который может вызывать методы, отвечающие следующей сигнатуре: // Делегат RenamedEventHandler должен указывать //на метод, соответствующий следующей сигнатуре. void MyNotificationHandler(object source, RenamedEventArgs e) Чтобы проиллюстрировать процесс наблюдения за файлом, предположим, что на диске С: создан новый каталог по имени MyFolder, содержащий различные файлы *.txt (с произвольными именами). Следующее консольное приложение (под названием MyDirectoryWatcher) будет выполнять мониторинг файлов *.txt внутри каталога MyFolder и выводить сообщения при создании, удалении, модификации или переименовании файлов: static void Main(string[] args) { Console.WriteLine ("***** The Amazing File Watcher App *****\n"); // Установить путь к каталогу, за которым нужно наблюдать. FileSystemWatcher watcher = new FileSystemWatcher(); try { watcher.Path = @"C:\MyFolder"; } catch(ArgumentException ex) { Console.WriteLine(ex.Message); return; } // Указать предметы наблюдения. watcher.NotifyFilter = NotifyFilters.LastAccess I NotifyFilters.LastWrite I NotifyFilters.FileName I NotifyFilters.DirectoryName; // Следить только за текстовыми файлами. watcher.Filter = "*.txt"; // Добавить обработчики событий. watcher.Changed += new FileSystemEventHandler(OnChanged); watcher.Created += new FileSystemEventHandler(OnChanged); watcher.Deleted += new FileSystemEventHandler(OnChanged); watcher.Renamed += new RenamedEventHandler(OnRenamed); // Начать наблюдение за каталогом. watcher.EnableRaisingEvents = true; // Ожидать команды пользователя на завершение программы. Console. WriteLine (@"Press 'q' to quit app.11); while(Console.Read()!='q'); }
734 Часть V. Введение в библиотеки базовых классов .NET Два обработчика событий просто сообщают о модификациях файлов: static void OnChanged(object source, FileSystemEventArgs e) { // Показать, что сделано, если файл изменен, создан или удален. Console.WriteLine ("File: {0} {1},п, e.FullPath, е.ChangeType); } static void OnRenamed(object source, RenamedEventArgs e) { // Показать, что файл был переименован. Console.WriteLine("File: {0} renamed to\n{1}", e.OldFullPath, e.FullPath); } Чтобы протестировать эту программу, запустите ее и откройте проводник Windows. Попробуйте переименовать файлы, создать файл *.txt, удалить файл *.txt и т.д. При этом вы увидите информацию относительно состояния текстовых файлов внутри MyFolder, как показано ниже: ***** The Amazing File Watcher App ***** Press 'q' to quit app. File: C:\MyFolder\New Text Document.txt Created! File: C:\MyFolder\New Text Document.txt renamed to С:\MyFolder\Hello.txt File: C:\MyFolder\Hello.txt Changed! File: C:\MyFolder\Hello.txt Changed! File: C:\MyFolder\Hello.txt Deleted1 Исходный код. Проект MyDirectoryWatcher доступен в подкаталоге Chapter 20. На этом знакомство с фундаментальными операциями ввода-вывода, предлагаемыми платформой .NET, завершено. Эти приемы наверняка будут использоваться во многих приложениях. Кроме того, значительно упростить задачу сохранения больших объемов данных, могут службы сериализации объектов. Понятие сериализации объектов Термин сериализация описывает процесс сохранения (и, возможно, передачи) состояния объекта в потоке (т.е. файловом потоке и потоке в памяти). Последовательность сохраняемых данных содержит всю необходимую информацию, необходимую для реконструкции (или десериализации) состояния объекта с целью последующего использования. Применяя эту технологию, очень просто сохранять большие объемы данных (в различных форматах) с минимальными усилиями. Во многих случаях сохранение данных приложения с использованием служб сериализации выливается в код меньшего объема, чем применение классов для чтения/записи из пространства имен System. 10. Например, предположим, что создано настольное приложение с графическим интерфейсом, в котором необходимо предоставить конечным пользователям возможность сохранения их предпочтений (цвета окон, размер шрифта и т.п.). Для этого можно определить класс по имени UserPref s и инкапсулировать в нем примерно два десятка полей данных. В случае применения типа System. 10.BinaryWriter придется вручную сохранять каждое поле объекта UserPrefs. Аналогично, когда вы понадобится загрузить данные из файла обратно в память, придется использовать System.IO.BinaryReader и, опять-таки, вручную читать каждое значение, чтобы реконструировать новый объект UserPrefs. Сэкономить значительное время можно, снабдив класс UserPrefs атрибутом [Serializable]:
Глава 20. Файловый ввод-вывод и серйализация объектов 735 [Serializable] public class UserPrefs { public string WindowColor; public int FontSize; } После этого полное состояние объекта может быть сохранено с помощью всего нескольких строк кода. Пока не погружаясь в детали, взгляните на следующий метод Main(): static void Main(string[] args) { UserPrefs userData = new UserPrefs (); userData.WindowColor = "Yellow"; userData.FontSize = 0"; // BinaryFormatter сохраняет данные в двоичном формате. // Чтобы получить доступ к BinaryFormatter, понадобится // импортировать System.Runtiтле.Serialization.Formatters.Binary. BinaryFormatter binFormat = new BinaryFormatter(); // Сохранить объект в локальном файле. using(Stream fStream = new FileStream("user.dat", FileMode.Create, FileAccess.Write, FileShare.None)) { binFormat.Serialize(fStream, userData); } Console.ReadLine(); } Хотя сохранять объекты с помощью механизма сериализации объектов .NET довольно просто, процесс, происходящий при этом "за кулисами", достаточно сложен. Например, когда объект сохраняется в потоке, все ассоциированные с ним данные (т.е. данные базового класса и содержащиеся в нем объекты) также автоматически сериали- зуются. Поэтому, при попытке сериализовать производный класс в игру вступают также все данные по цепочке наследования. И как будет показано, набор взаимосвязанных объектов, участвующих в этом, представляется графом объектов. Службы сериализации .NET также позволяют сохранять граф объектов в различных форматах. В предыдущем примере кода применялся тип BinaryFormatter, поэтому состояние объекта UserPrefs сохраняется в компактном двоичном формате. Г)эаф объектов можно также сохранить в формате SOAP или XML, используя другие типы форматеров. Эти форматы полезны, когда необходимо гарантировать возможность передачи хранимых объектов между разными операционными системами, языками и архитектурами. На заметку! В WCF предлагается слегка отличающийся механизм для сериализации объектов в и из операций службы WCR в нем используются атрибуты [DataContract] и [DataMember]. Подробнее об этом речь пойдет в главе 25. И, наконец, имейте в виду, что граф объектов может быть сохранен в любом типе, унаследованном от System.10.Stream. В предыдущем примере объект UserPrefs был сохранен в локальном файле через тип FileStream. Однако если вместо этого понадобится сохранить объект в определенной области памяти, можно применить тип MemoryStream. Главное, чтобы последовательность данных корректно представляла состояние объектов в графе.
736 Часть V. Введение в библиотеки базовых классов .NET Роль графов объектов Как упоминалось ранее, когда объект сериализуется, среда CLR учитывает все связанные объекты, чтобы гарантировать корректное сохранение данных. Этот набор связанных объектов называется графом объектов. Г^афы объектов представляют простой способ документирования набора отношений между объектами, и эти отношения не обязательно отображаются на классические отношения ООП (вроде отношений "является" и "имеет"), хотя достаточно хорошо моделируют эту парадигму. Каждый объект в графе получает уникальное числовое значение. Имейте в виду, что числа, назначенные объектам в графе, являются произвольными и не имеют никакого значения для внешнего мира. Как только каждому объекту присвоено числовое значение, граф объектов может записать все наборы зависимостей каждого объекта. В качестве простого примера предположим, что создан набор классов, моделирующих автомобили. Существует базовый класс по имени Саг, который "имеет" класс Radio. Другой класс по имени JamesBondCar расширяет базовый тип Саг. На рис. 20.4 показан возможный граф объектов, который моделирует эти отношения. Car Radio Рис. 20.4. Простой граф объектов При чтении графов объектов для описания соединяющих стрелок можно использовать выражение "зависит от" или "ссылается на". Таким образом, на рис. 20.1 видно, что класс Саг ссылается на класс Radio (учитывая отношение "имеет"), JamesBondCar ссылается на Саг (учитывая отношение "имеет"), как и на Radio (поскольку наследует эту защищенную переменную-член). Конечно, среда CLR не рисует картинок в памяти для представления графа взаимосвязанных объектов. Вместо этого отношение, документированное в предыдущей диаграмме, представлено математической формулой, которая выглядит примерно так: [Саг 3, ref 2], [Radio 2], [JamesBondCar 1, ref 3, ref 2] Если вы проанализируете эту формулу, то опять увидите, что объект 3 (Саг) имеет зависимость от объекта 2 (Radio). Объект 2 (Radio) — это "одинокий волк", которому не нужен никто. И, наконец, объект 1 (JamesBondCar) имеет зависимость как от объекта 3, так и от объекта 2. В любом случае, при сериализации или десериализации JamesBondCar граф объектов гарантирует, что типы Radio и Саг также будут участвовать в процессе. Изящество процесса сериализации состоит в том, что граф, представляющий отношения между объектами, устанавливается автоматически, "за кулисами". Как будет показано далее в этой главе, если необходимо вмешаться в конструирование графа объектов, это можно сделать посредством настройки процесса сериализации через атрибуты и интерфейсы.
Глава 20. Файловый ввод-вывод и сериализация объектов 737 На заметку! Строго говоря, тип XmlSerializer (описанный далее в главе) не сохраняет состояния, используя граф объектов; однако он сериализует и десериализует связанные объекты в предсказуемой манере. Конфигурирование объектов для сериализации Чтобы сделать объект доступным для служб сериализации .NET, понадобится только декорировать каждый связанный класс (или структуру) атрибутом [Serializable]. Если выясняется, что некоторый тип имеет члены-данные, которые не должны (или не могут) участвовать в схеме сериализации, можно пометить такие поля атрибутом [NonSerialized]. Это помогает сократить размер хранимых данных, при условии, что в сериализуемом классе есть переменные-члены, которые не следует "запоминать" (например, фиксированные значения, случайные значения, кратковременные данные и т.п.). Определение сериализуемых типов Для начала создадим новое консольное приложение по имени SimpleSerialize. Добавим в него новый класс по имени Radio, помеченный атрибутом [Serializable], у которого исключается одна переменная-член (radioID), помеченная атрибутом [NonSerialized] и потому не сохраняемая в специфицированном потоке данных. [Serializable] public class Radio { public bool hasTweeters; public bool hasSubWoofers; public double[] stationPresets; [IlonSerialized] public string radioID = "XF-552RR6"; } Затем добавим два дополнительных типа, представляющих базовые классы JamesBondCar и Саг (оба они также помечены атрибутом [Serializable]), и определим в них следующие поля данных: [Serializable] public class Car I public Radio theRadio = new Radio (); public bool isHatchBack; } [Serializable] public class JamesBondCar : Car { public bool canFly; public bool canSubmerge; } Имейте в виду, что атрибут [Serializable] не может наследоваться от родительского класса. Поэтому при наследовании типа, помеченного [Serializable], дочерний класс также должен быть помечен [Serializable] или же его нельзя будет сохранить в потоке. Фактически все объекты в графе объектов должны быть помечены атрибутом [Serializable]. Попытка сериализовать несериализуемый объект с использованием BinaryFormatter или SoapFormatter приводит к исключению SerializationException во время выполнения.
738 Часть V. Введение в библиотеки базовых классов .NET Общедоступные поля, приватные поля и общедоступные свойства Обратите внимание, что в каждом из этих классов поля данных определены как public, это сделано для упрощения примера. Конечно, приватные данные, представленные общедоступными свойствами, были бы более предпочтительны с точки зрения ООП. Также для простоты в этих типах не определились никакие специальные конструкторы, и потому все неинициализированные поля данных получат ожидаемые значения по умолчанию. Оставив в стороне принципы ООП, можно спросить: какого определения полей данных типа требуют различные форматеры, чтобы сериализовать их в поток? Ответ такой: в зависимости от обстоятельств. Если вы сохраняете состояние объекта, используя BinaryFormatter или SoapFormatter, то разницы никакой. Эти типы запрограммированы для сериализации всех сериализуемых полей типа, независимо от того, представлены они общедоступными полями, приватными полями или приватными полями с соответствующими общедоступными свойствами. Однако вспомните, что если есть элементы данных, которые не должны сохраняться в графе объектов, можно выборочно пометить общедоступные или приватные поля атрибутом [NonSerialized], как сделано со строковыми полями в типе Radio. Однако ситуация существенно меняется, если вы собираетесь применять тип XmlSerializer. Этот тип будет сериализовать только сериализуемые общедоступные поля данных или приватные поля, представленные общедоступными свойствами. Приватные данные, не представленные свойствами, просто игнорируются. Например, рассмотрим следующий сериализуемый тип Person: [Serializable] public class Person { // Общедоступное поле. public bool isAlive = true; // Приватное поле. private int personAge = 21; // Общедоступное свойство/приватные данные. private string fName = string.Empty; public string FirstName { get { return fName; } set { fName = value; } } } При обработке BinaryFormatter или SoapFormatter обнаружится, что поля isAlive, personAge и fName сохраняются в выбранном потоке. Однако XmlSerializer не сохранит значения personAge, поскольку эта часть приватных данных не инкапсулирована в свойство. Чтобы сохранять возраст персоны с помощью XmlSerializer, это поле понадобится определить как public или же инкапсулировать его в общедоступном свойстве. Выбор форматера сериализации Как только типы сконфигурированы для участия в схеме сериализации .NET с применением необходимых атрибутов, следующий шаг состоит в выборе формата (двоичного, SOAP или XML) для сохранения состояния объектов. Перечисленные возможности представлены следующими классами: • BinaryFormatter • SoapFormatter • XmlSerializer
Глава 20. Файловый ввод-вывод и сериализация объектов 739 Тип BinaryFormatter сериализует состояние объекта в поток, используя компактный двоичный формат. Этот тип определен в пространстве имен System.Runtime. Serialization.Formatters.Binary, которое входит в сборку mscorlib.dll. Таким образом, чтобы получить доступ к этому типу, необходимо указать следующую директиву using: // Получить доступ к BinaryFormatter в mscorlib.dll. using System.Runtime.Serialization.Formatters.Binary; Тип SoapFormatter сохраняет состояние объекта в виде сообщения SOAP (стандартный XML-формат для передачи и приема сообщений от веб-служб). Этот тип определен в пространстве имен System.Runtime.Serialization.Formatters.Soap, находящемся в отдельной сборке. Поэтому для форматирования графа объектов в сообщение SOAP необходимо сначала установить ссылку на System.Runtime.Serialization.Formatters. Soap.d 11, используя диалоговое окно Add Reference (Добавить ссылку) в Visual Studio 2010 и затем указать следующую директиву using: // Необходима ссылка на System.Riintime.Serialization.Formatters.Soap.dll! using System.Runtime.Serialization.Formatters.Soap; И, наконец, для сохранения дерева объектов в документе XML имеется тип XmlSerializer. Чтобы использовать этот тип, нужно указать директиву using для пространства имен System.Xml. Serialization и установить ссылку на сборку System.Xml.dll. К счастью, шаблоны проектов Visual Studio 2010 автоматически ссылаются на System.Xml.dll, так что достаточно просто указать соответствующее пространство имен: // Определено внутри System.Xml.dll. using System.Xml.Serialization; Интерфейсы IFormatter и IRemotingFormatter Независимо от того, какой форматер выбран, имейте в виду, каждый из них наследуется непосредственно от System.Object, так что они не разделяют общего набора членов от какого-то базового класса сериализации. Однако типы BinaryFormatter и SoapFormatter поддерживают общие члены через реализацию интерфейсов IFormatter и IRemotingFormatter (как ни странно, XmlSerializer не реализует ни одного из них). В System.Runtime.Serialization.IFormatter определены основные методы SerializeO HDeserializeO, которые выполняют черновую работу по перемещению графов объектов в определенный поток и обратно. Помимо этих членов в IFormatter определено несколько свойств, используемых "за кулисами" реализующим типом: public interface IFormatter { SerializationBinder Binder { get; set; } StreamingContext Context { get; set; } ISurrogateSelector SurrogateSelector { get; set; } object Deserialize(Stream serializationStream); void Serialize(Stream serializationStream, object graph); } Интерфейс System. Runt ime.Remoting. Mess aging. IRemotingFormatter (который внутри полагается на уровень удаленного взаимодействия .NET Remoting) перегружает члены SerializeO nDeserializeO в манере, более подходящей для распределенного сохранения. Обратите внимание, что интерфейс IRemotingFormatter унаследован от более общего интерфейса IFormatter:
740 Часть V. Введение в библиотеки базовых классов .NET public interface IRemotingFormatter : IFormatter • { object Deserialize(Stream serializationStream, HeaderHandler handler); void Serialize(Stream serializationStream, object graph, Header[] headers); } Хотя взаимодействовать с этими интерфейсами не понадобится в большинстве сценариев сериализации, вспомните, что полиморфизм на базе интерфейсов позволяет подставлять экземпляры BinaryFormatter или SoapFormatter там, где ожидается IFormatter. Таким образом, если необходимо построить метод, который может сериали- зовать граф объектов с использованием любого из этих классов, можно записать так: static void SerializeObjectGraph(IFormatter itfFormat, Stream destStream, object graph) { itfFormat.Serialize(destStream, graph); } Точность типов среди форматеров Наиболее очевидное отличие между тремя форматерами связано с тем, как граф объектов сохраняется в потоке (двоичном, SOAP или XML). Следует знать также о некоторых более тонких отличиях, в частности — каким образом форматеры добиваются точности типов (type fidelity). Когда используется тип BinaryFormatter, он сохраняет не только данные полей объектов из графа, но также полное квалифицированное имя каждого типа и полное имя определяющей его сборки (имя, версия, маркер общедоступного ключа и культура). Эти дополнительные элементы данных делают BinaryFormatter идеальным выбором, когда необходимо передавать объекты по значению (т.е. полные копии) между границами машин для использования в .NET-приложениях. Форматер SoapFormatter сохраняет трассировки сборок-источников за счет использования пространства имен XML. Например, вспомните тип Person, определенный ранее в этой главе. Если понадобится сохранить этот тип в сообщении SOAP, вы обнаружите, что открывающий элемент Person квалифицирован сгенерированным параметром xlmns. Взгляните на следующее частичное определение, обратив особое внимание на пространство имен XML под названием al: <al:Person id="ref-l" xmlns:al= "http: //schemas .microsoft. com/clr/nsassem/SimpleSerialize/MyApp%2Cnd20 Versionnu3Dl .0.0. 0%2Cnj20Culture%3Dneutral%2C^20РиЫ1сКеуТокепЪЗОпи11м> <isAlive>true</isAlive> <personAge>21</personAge> <fName id=,,ref-3"></fName> </al:Person> Однако XmlSerializer не пытается предохранить точную информацию о типе, и потому не записывает его полного квалифицированного имени или сборки, в которой он определен. Хотя на первый взгляд это может показаться ограничением, причина состоит в открытой природе представления данных XML. Ниже показано возможное XML- представление типа Person: <?xml version="l.0"?> <Регson xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <isAlive>true</isAlive> <PersonAge>21</PersonAge> <FirstName /> </Person>
Глава 20. Файловый ввод-вывод и сериализация объектов 741 Если необходимо сохранить состояние объекта так, чтобы его можно было использовать в любой операционной системе (Windows XP, Mac OS X и различных дистрибутивах Linux), на любой платформе приложений (.NET, Java Enterprise Edition, COM и т.п.) или в любом языке программирования, придерживаться полной точности типов не следует, поскольку нельзя рассчитывать, что все возможные адресаты смогут понять специфичные для .NET типы данных. Учитывая это, SoapFormatter и XmlSerializer являются идеальным выбором, когда требуется гарантировать как можно более широкое распространение объектов. Сериализация объектов с использованием BinaryFormatter Чтобы проиллюстрировать, насколько просто сохранить экземпляр JamesBondCar в физическом файле, воспользуемся типом BinaryFormatter. Двумя ключевыми методами типа BinaryFormatter, о которых следует знать, являются Serialize () и Deserialize(): 1. SerializeO сохраняет граф объектов в указанный поток в виде последовательности байтов; 2. Deserialize () преобразует сохраненную последовательность байт в граф объектов. Предположим, что после создания экземпляра JamesBondCar и модификации некоторых данных состояния требуется сохранить этот экземпляр в файле *.dat. Первая задача — создание самого файла *.dat. Для этого можно создать экземпляр типа System. IO.FileStream. Затем следует создать экземпляр BinaryFormatter и передать ему FileStream и граф объектов для сохранения. Взгляните на следующий метод Main(): //Не забудьте импортировать пространства имен // System.Runtime.Serialization.Formatters.Binary и System.10' static void Main(string[] args) { Console.WriteLine("***** Fun with Object Serialization *****\n"); // Создать JamesBondCar и установить состояние. JamesBondCar jbc = new JamesBondCar(); jbc.canFly = true; jbc.canSubmerge = false; jbc.theRadio.stationPresets = new doublet]{89.3, 105.1, 97.1}; jbc.thePadio.hasTweeters = true; // Сохранить объект в указанном файле в двоичном формате. SaveAsBinaryFormat(jbc, "CarData.dat"); Console.ReadLine(); } Метод SaveAsBinaryFormat() реализован следующим образом: static void SaveAsBinaryFormat(object objGraph, string fileName) { // Сохранить объект в файл CarData.dat в двоичном виде. BinaryFormatter binFormat = new BinaryFormatter(); using (Stream fStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None)) { binFormat.Serialize(fStream, objGraph); I Console.WriteLine("=> Saved car in binary format1");
742 Часть V. Введение в библиотеки базовых классов .NET Как видите, метод BinaryFormatter.Serialize() — это член, отвечающий за составление графа объектов и передачу последовательности байт в некоторый объект унаследованного от Stream типа. В данном случае поток представляет физический файл. Однако можно также сериализовать объекты в любой тип-наследник Stream, представляющий область памяти, сетевой поток и т.п. После выполнения программы можно просмотреть содержимое файла CarData.dat, представляющее этот экземпляр JamesBondCar, в папке bin\Debug текущего проекта. На рис. 20.5 показан этот файл, открытый в Visual Studio 2010. в CarData.so*p* X S<SQAP-ENV:Envelope xwlnsixsig-http://www.w3.org/29ei/XHLSchema-instance'* xmlns:xsd="l <SOAP-ENV:Body> <al:JamesBondCar id-'Vef-l" xmlns:al="http://scheilias.microsoft.com/cIr/nsassew/S: <canFly>true</canFly> <canSubraerge>false</canSubmerge> <theRadio href="#ref-3"/> <isHatchBack>false</isHatchBack> </al:3amesBondCar> <al:Radio id^ref-3" xnlnsial^'http^/scheiiias.iiicrosoft.coii/clr/nsassew/SinipleSei В <hasTweeters>true</hasTweeters> <hasSubwoofers>false</hasSubWoofers> <stationPresets href«"#ref-4'*/> </al:Radio> <S0AP-ENC:Array id-Vef-A*' S0AP-ENC:arrayType=*,xsd:double[3]',> <it«»>89.3</ite«> < ite»>105.1</item> <itee>97.K/iteni> </SOAP-ENC:Array> </SOAP-ENV:Body> </SOAP-EW: Envelope> Рис. 20.5. Объект JamesBondCar, сериализованный с использованием BinaryFormatter Десериализация объектов с использованием BinaryFormatter Теперь предположим, что необходимо прочитать сохраненный объект JamesBondCar из двоичного файла обратно в объектную переменную. После открытия файла CataData.dat (методом File.OpenReadO) просто вызовите метод Deserialize() класса BinaryFormatter. Имейте в виду, что Deserialize() возвращает объект общего типа System.Object, так что понадобится применить явное приведение, как показано ниже: static void LoadFromBinaryFile(string fileName) { BinaryFormatter binFormat = new BinaryFormatter(); // Прочитать JamesBondCar из двоичного файла. using(Stream fStream = File.OpenRead(fileName)) { JamesBondCar carFromDisk = (JamesBondCar)binFormat.Deserialize(fStream); Console.WriteLine("Can this car fly? : {0}", carFromDisk.canFly); } Обратите внимание, что при вызове Deserialize() ему передается тип-наследник Stream, представляющий местоположение сохраненного графа объектов. Приведя возвращенный объект к правильному типу, вы получаете объект в том состоянии, в каком он был на момент сохранения.
Глава 20. Файловый ввод-вывод и сериализация объектов 743 Сериализация объектов с использованием SoapFormatter Следующий форматер, которым мы воспользуемся, будет SoapFormatter, сериа- лизующий данные в подходящем конверте SOAP. Протокол SOAP (Simple Object Access Protocol — простой протокол доступа к объектам) определяет стандартный процесс вызова методов в независящей от платформы и операционной системы манере. Предполагая, что ссылка на сборку System.Runtime.Serialization.Formatters. Soap.dll установлена, а пространство имен System.Runtime.Serialization. Formatters.Soap импортировано, для сохранения и извлечения JamesBondCar в виде сообщения SOAP можно просто заменить в предыдущем примере все вхождения BinaryFormatter на SoapFormatter. Ниже показан новый метод класса Program, который сериализует объект в локальный файл: //Не забудьте импортировать пространства имен // System.Runtime.Serialization.Formatters.Soap //и установить ссылку на System.Runtimp.Serialization.Formatters.Soap.dll! static void SaveAsSoapFormat (object objGraph, string fileName) { // Сохранить объект в файл CarData.soap в формате SOAP. SoapFormatter soapFormat = new SoapFormatter(); using(Stream fStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None) ) { soapFormat.Serialize(fStream, objGraph); } Console. WriteLine ("=> Saved car in SOAP format!11); } Как и ранее, для перемещения графа объектов в поток и обратно применяются методы Serialize () и Deserialize (). После вызова метода SaveAsSoapFormat () в Main() и запуска приложения можно открыть файл *.soap. В нем находятся XML-элементы, которые описывают значения состояния текущего объекта JamesBondCar, а также отношения между объектами в графе через лексемы #ref (рис. 20.6). MyData-soap* X Q E<SOAP-ENV:Envelope w»lns:xsi»"http://www.w3.orj»/2eei/XMLSchema-instance" xnlnsixsd^http^jH 3 <S0AP-ENV:Body> 7; <al:StringData id="ref-l" xrolns:al^"http://schefflas.fflicrosoft.com/clr/nsassem/CustoniSerl J K<First_It€W id»"ref-3',>FIRST DATA BL0OC</First_Ite«> e| <dataIte«Two id*Vef-4">HC)RE fWTA</dataIteeTwo> I </al:StringPata> [ </50AP-ENV:Body> [</SOAP-ENV:Envelope> |iPP.%. JJ * 1 * Рис. 20.6. Объект JamesBondCar, сериализованный с использованием SoapFormatter Сериализация объектов с использованием XmlSerializer В дополнение к двоичному форматеру и форматеру SOAP сборка System.Xml.dll предлагает третий класс форматера — System.Xml.Serialization.XmlSerializer, — который может использоваться для сохранения общедоступного состояния заданного объекта в виде чистой XML-разметки, в противоположность данным XML внут-
744 Часть V. Введение в библиотеки базовых классов .NET ри сообщения SOAP. Работа с этим типом несколько отличается от работы с типами SoapFormatter или BinaryFormatter. Рассмотрим следующий код (предполагается, что было импортировано пространство имен System.Xml.Serialization): static void SaveAsXmlFormat(object objGraph, string fileName) { // Сохранить объект в файле CarData.xml в формате XML. XmlSerializer xmlFormat = new XmlSerializer(typeof(JamesBondCar) , new Type[] { typeof(Radio), typeof(Car) }); using(Stream fStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None)) { xmlFormat.Serialize(fStream, objGraph); } Console. WriteLine ("=> Saved car in XML format!11); } Ключевое отличие состоит в том, что тип XmlSerializer требует указания информации о типе, представляющей класс, который необходимо сериализовать. В сгенерированном файле XML находятся показанные ниже данные XML: <?xml version=.0"?> <JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <theRadiq> <hasTweeters>true</hasTweeters> <hasSubWoofers>false</hasSubWoofers> <stationPresets> <double>89.3</double> <double>105. K/double> <double>97 . K/double> </stationPresets> <radioID>XF-552RR6</radioID> </theRadio> <isHatchBack>false</isHatchBack> <canFly>true</canFly> <canSubmerge>false</canSubmerge> </JamesBondCar> На заметку! Класс XmlSerializer требует, чтобы все сериализованные типы в графе объектов поддерживали конструктор по умолчанию (поэтому не забудьте его добавить, если определяли специальные конструкторы). Если этого не сделать, во время выполнения сгенерируется исключение InvalidOperationException. Управление генерацией данных XML Если у вас есть опыт в технологиях XML, то вы хорошо знаете, насколько важно удостоверяться, что данные внутри документа XML отвечают набору правил, которые устанавливают действительность данных. Понятие "действительного" документа XML не имеет отношения к синтаксической правильности элементов XML (вроде того, что все открывающие элементы должны иметь соответствующие закрывающие элементы). Действительные документы — это те, что отвечают согласованным правилам форматирования (например, поле X должно быть выражено как атрибут, но не как подэлемент), которые обычно определены схемой XML или в файле определения типа документа (Document-Type Definition — DTD).
Глава 20. Файловый ввод-вывод и сериализация объектов 745 По умолчанию класс XmlSerializer сериализует все общедоступные поля/свойства как элементы XML, а не как атрибуты XML. Чтобы управлять генерацией результирующего документа XML с помощью класса XmlSerializer, необходимо декорировать типы любым количеством дополнительных атрибутов из пространства имен System. Xml.Serialization. В табл. 20.12 документированы некоторые (но не все) атрибуты, которые влияют на кодирование данных XML в потоке. Таблица 20.12. Избранные атрибуты из пространства имен System.Xml.Serialization Атрибут Назначение [XmlAttnbute] Поле или свойство будет сериализовано как атрибут XML (а не как подэлемент) [XmlElement] Поле или свойство будет сериализовано как элемент XML с указанным именем [XmlEnum] Этот атрибут предоставляет имя элемента, являющееся членом перечисления [XmlRoot] Этот атрибут управляет тем, как будет сконструирован корневой элемент (пространство имен и название элемента) [XmlText] Свойство или поле должно быть сериализовано как текст XML (т. е. содержимое, находящееся между начальным и конечным дескрипторами корневого элемента) [XmlType] Этот атрибут предоставляет имя и пространство имен типа XML В следующем простом примере показано текущее представление данных полей JamesBondCar в XML: <?xml ersion="l. 0" encoding="utf-8ll?> <JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <canFly>tme</canFly> <canSubmerge>false</canSubmerge> </JamesBondCar> Если необходимо указать специальное пространство имен XML, которое квалифицирует JamesBondCar и закодирует значения canFly и canSubmerge в виде атрибутов XML, модифицируем определение класса JamesBondCar следующим образом: [XmlRoot (Namespace = "http://www.MyCompany.com11)] public class JamesBondCar : Car { [XmlAttribute] public bool canFly; [XmlAttribute] public bool canSubmerge; } Это порождает показанный ниже XML-документ (обратите внимание на открывающий элемент <JamesBondCar>): <?xml version="l.0 ?> <JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" canFly="true11 canSubmerge=" false" xmlns="http://www.MyCompany.com"> </JamesBondCar>
746 Часть V. Введение в библиотеки базовых классов .NET Естественно, для управления генерацией результирующего XML-документа с помощью XmlSerializer может использоваться и множество других атрибутов. За подробной информацией обращайтесь к описанию пространства имен System.Xml.Serialization в документации .NET Framework 4.0 SDK. Сериализация коллекций объектов Теперь, когда известно, как сохранять единственный объект в потоке, давайте посмотрим, каким образом можно сохранить множество объектов. Как вы, возможно, заметили, метод Serialize () интерфейса IFormatter не предусматривает способа указать произвольное количество объектов в качестве ввода (допускается только один объект System.Object). Вдобавок возвращаемое значение DeserializeO также представляет собой одиночный объект System.Object (то же базовое ограничение касается и XmlSerializer): public interface IFormatter { object Deserialize(Stream serializationStream); void Serialize(Stream serializationStream, object graph); } Вспомните, что System.Object представляет целое дерево объектов. С учетом этого, если передать объект, который помечен атрибутом [Serializable] и содержит в себе другие объекты [Serializable], то с помощью единственного вызова данного метода будет сохраняться весь набор объектов. К счастью, большинство типов из пространств имен System.Collections и System.Collections.Generic уже помечены атрибутом [Serializable]. Таким образом, чтобы сохранить множество объектов, просто добавьте это множество в контейнер (такой как ArrayList или List<T>) и сериализуйте данный объект в выбранный поток. Предположим, что класс JamesBondCar дополнен конструктором, принимающим два аргумента, для установки нескольких фрагментов данных состояния (обратите внимание, что должен быть также добавлен конструктор по умолчанию, как того требует XmlSerializer): [Serializable, XmlRoot (Namespace = "http://www.MyCompany.com11)] public class JamesBondCar : Car { public JamesBondCar(bool skyWorthy, bool seaworthy) { canFly = skyWorthy; canSubmerge = seaworthy; } // XmlSerializer требует конструктора по умолчанию1 public JamesBondCar(){} } Теперь можно сохранять любое количество объектов JamesBondCar, как показано ниже: static void SaveListOfCars () { // Сохранить список List<T> объектов JamesBondCar. List<JamesBondCar> myCars = new List<JamesBondCar>();
Глава 20. Файловый ввод-вывод и сериализация объектов 747 myCars.Add(new JamesBondCar(true, true)); myCars.Add(new JamesBondCar(true, false)); myCars.Add(new JamesBondCar(false, true)); myCars.Add(new JamesBondCar(false, false)); using(Stream fStream = new FileStream("CarCollection.xml", FileMode.Create, FileAccess.Write, FileShare.None)) { XmlSerializer xmlFormat = new XmlSerializer(typeof(List<JamesBondCar>)); xmlFormat.Serialize(fStream, myCars); } Console. WriteLine ("=> Saved list of cars!11); } Поскольку здесь используется XmlSerializer, необходимо указать информацию о типе для каждого из подобъектов внутри корневого объекта (которым в данном случае является List<JamesBondCar>). Если бы применялись типы BinaryFormatter или SoapFormatter, то логика была бы еще проще. Например: static void SaveListOfCarsAsBinary() { // Сохранить объект ArrayList (myCars) в двоичном виде. List<JamesBondCar> myCars = new List<JamesBondCar>(); BinaryFormatter binFormat = new BinaryFormatter(); using(Stream fStream = new FileStream("AllMyCars.dat", FileMode.Create, FileAccess.Write, FileShare.None)) { binFormat.Serialize(fStream, myCars); } Console.WriteLine("=> Saved list of cars in binary1"); } Исходный код. Проект SimpleSerialize доступен в подкаталоге Chapter 20. Настройка процессов сериализации SOAP и двоичной сериализации В большинстве случаев схема сериализации по умолчанию, предоставляемая платформой .NET, вполне подходит. Нужно лишь применить атрибут [Serializable] к связанным типам и передать дерево объектов выбранному форматеру для обработки. Однако в некоторых случаях может понадобиться вмешаться в процесс конструирования дерева и процесс сериализации. Например, может существовать бизнес-правило, которое гласит, что все поля данных должны сохраняться в определенном формате, или же необходимо добавить дополнительные данные в поток, которые напрямую не отображаются на поля сохраняемого объекта (временные метки, уникальные идентификаторы и т.п.). В пространстве имен System.Runtime.Serialization предусмотрено несколько типов, которые позволяют вмешаться в процесс сериализации объектов. В табл. 20.13 описаны некоторые из основных типов.
748 Часть V. Введение в библиотеки базовых классов .NET Таблица 20.13. Основные типы пространства имен System.Runtime.Serialization Тип Назначение ISerializable Этот интерфейс может быть реализован на типе [Serializable] для управления его сериализацией и десериализацией ObjectlDGenerator Этот тип генерирует идентификаторы для членов графа объектов [OnDeserialized] Этот атрибут позволяет указать метод, который будет вызван немедленно после десериализации объекта [OnDeserializing] Этот атрибут позволяет указать метод, который будет вызван перед процессом десериализации [OnSerialized] Этот атрибут позволяет указать метод, который будет вызван немедленно после того, как объект сериализован [OnSerializing] Этот атрибут позволяет указать метод, который будет вызван перед процессом сериализации [OptionalField] Этот атрибут позволяет определить поле типа, которое может быть пропущено в указанном потоке Serializationlnfo По существу это "мешок свойств", который поддерживает пары "имя/ значение", представляющие состояние объекта во время процесса сериализации Углубленный взгляд на сериализацию объектов Прежде чем приступить к изучению различных способов настройки процесса сериализации, полезно внимательнее присмотреться к тому, что происходит "за кулисами". Когда BinaryFormatter сериализует граф объектов, он отвечает за передачу следующей информации в указанный поток: • полностью квалифицированное имя объекта в графе (например, My Ар р. JamesBondCar); • имя сборки, определяющей граф объектов (например, MyApp.exe); • экземпляр класса Serializationlnfo, содержащего все данные состояния, которые поддерживаются членами графа объектов. Во время процесса десериализации BinaryFormatter использует ту же информацию для построения идентичной копии объекта с применением информации, извлеченной из потока-источника. Процесс, выполняемый SoapFormatter, очень похож. На заметку! Вспомните, что для обеспечения максимальной мобильности объекта XmlSerializer не сохраняет полностью квалифицированное имя типа или имя сборки, в которой он содержится. Этот тип может сохранять только общедоступные данные. Помимо перемещения необходимых данных в поток и обратно, форматеры также анализируют члены графа объектов на предмет перечисленных ниже частей инфраструктуры. • Проверка пометки объекта атрибутом [Serializable]. Если объект не помечен, генерируется исключение SerializationException. • Если объект помечен атрибутом [Serializable], выполняется проверка, реализует ли объект интерфейс ISerializable. Если да, то вызывается метод GetObjectData() на этом объекте.
Глава 20. Файловый ввод-вывод и сериализация объектов 749 • Если объект не реализует интерфейс ISerializable, используется процесс се- риализации по умолчанию, который обрабатывает все поля, не помеченные [NonSerialized]. В дополнение к определению того, поддерживает ли тип ISerializable, форматеры также отвечают за исследование типов на предмет поддержки членов, которые оснащены атрибутами [OnSerializing], [OnSerialized], [OnDeserializing] или [OnDeserialized]. Мы рассмотрим назначение этих атрибутов чуть позже, а сначала давайте посмотрим на предназначение ISerializable. Настройка сериализации с использованием интерфейса ISerializable Объекты, которые помечены атрибутом [Serializable], имеют опцию реализации интерфейса ISerializable. Реализация этого интерфейса позволяет вмешаться в процесс сериализации и выполнить необходимое форматирование данных до и после сериализации. На заметку! После выхода версии .NET 2.0 предпочтительный способ настройки процесса сериализации начал предусматривать использование атрибутов сериализации. Тем не менее, знание интерфейса ISerializable важно для сопровождения систем, которые уже существуют. Интерфейс ISerializable довольно прост, учитывая, что в нем определен единственный метод GetObjectData(): // Чтобы вмешаться в процесс сериализации, // необходимо реализовать ISerializable. public interface ISerializable { void GetObjectData(Serializationlnfo info, StreamingContext context); } Метод GetObjectDataO вызывается автоматически заданным форматером во время процесса сериализации. Реализация этого метода заполняет входной параметр Serializationlnfo последовательностью пар "имя/значение", которые (обычно) отображают данные полей сохраняемого объекта. В Serializationlnfo определены многочисленные вариации перегруженного метода AddValueO, а также небольшой набор свойств, которые позволяют устанавливать и получать имя типа, определять сборку и счетчик членов. Ниже показано частичное определение: public sealed class Serializationlnfo { public Serializationlnfo(Type type, IFormatterConverter converter); public string AssemblyName { get; set; } public string FullTypeName { get; set; } public int MemberCount { get; } public void AddValue(string name, short value); public void AddValue(string name, ushort value); public void AddValue(string name, int value); } Типы, реализующие интерфейс ISerializable, также должны определять специальный конструктор со следующей сигнатурой: // Необходимо предусмотреть специальный конструктор с такой сигнатурой, // чтобы позволить исполняющей среде устанавливать состояние объекта.
750 Часть V. Введение в библиотеки базовых классов .NET [Serializable] class SomeClass : ISerializable { protected SomeClass (Serializationlnfo si, StreamingContext ctx) { . . . } } Обратите внимание, что видимость конструктора указана как protected. Это приемлемо, учитывая, что форматер будет иметь доступ к этому члену независимо от его видимости. Такие специальные конструкторы обычно делают protected (или private), тем самым гарантируя, что небрежный пользователь объекта никогда не создаст объект подобным образом. Как видите, первым параметром конструктора является экземпляр типа Serializationlnfo (который был показан ранее). Второй параметр этого специального конструктора имеет тип StreamingContext и содержит информацию относительно источника или места назначения передаваемых данных. Наиболее информативным членом StreamingContext является свойство State, представляющее значение из перечисления StreamingContextStates. Значения этого перечисления представляют базовую композицию текущего потока. Если только вы не планируется реализовать какие-то низкоуровневые службы удаленного взаимодействия, то иметь дело с этим перечислением непосредственно требуется редко. Тем не менее, ниже приведены члены перечисления StreamingContextStates (подробности ищите в документации .NET Framework 4.0 SDK): public enum StreamingContextStates { CrossProcess, CrossMachine, File, Persistence, Remoting, Other, Clone, CrossAppDomain, All } Давайте рассмотрим пример настройки процесса сериализации с использованием ISerializable. Предположим, что имеется новый проект консольного приложения (по имени CustomSerialization), в котором определен тип класса, содержащего два элемента данных string. Также представим, что требуется обеспечить сериализа- цию этих объектов string в верхнем регистре, а десериализацию — в нижнем. Чтобы удовлетворить этим правилам, можно реализовать интерфейс ISerializable так, как показано ниже (не забудьте импортировать пространство имен System.Runtime. Serialization): [Serializable] class StringData : ISerializable { private string dataltemOne = "First data block"; private string dataItemTwo= "More data"; public StringData () {} protected StringData(Serializationlnfo si, StreamingContext ctx) { // Восстановить переменные-члены из потока. dataltemOne = si.GetString("First_Item").ToLower(); dataltemTwo = si.GetString("dataltemTwo").ToLower(); }
Глава 20. Файловый ввод-вывод и сериализация объектов 751 void ISerializable.GetObjectData (Serializationlnfo info, StreamingContext ctx) { // Наполнить объект Serializationlnfo форматированными данными. info.AddValue("First_Item", dataltemOne.ToUpper ()); info.AddValue("dataltemTwo", dataltemTwo.ToUpper ()); } } Обратите внимание, что при наполнении объекта типа Serializationlnfo внутри метода GetObjectDataO именовать элементы данных идентично именам внутренних переменных-членов типа не обязательно. Это очевидно пригодится, если планируется отвязать данные типа от формата хранения. Однако имейте в виду, что нужно получить данные внутри специального защищенного конструктора, используя те же имена, что были назначены в GetObjectDataO. Чтобы опробовать специализированную сериализацию, предположим, что экземпляр MyStringData сохраняется с использованием SoapFormatter (обновите соответствующим образом ссылки на сборки и директивы using): static void Main(string [ ] args) { Console.WriteLine("***** Fun with Custom Serialization *****"); // Вспомните, что этот тип реализует ISerializable. StringData myData = new StringDataO ; // Сохранить в локальный файл в формате SOAP. SoapFormatter soapFormat = new SoapFormatter(); using(Stream fStream = new FileStream("MyData.soap", FileMode.Create, FileAccess.Write, FileShare.None)) { soapFormat.Serialize(fStream, myData); } Console.ReadLine(); } Просматривая полученный файл *.soap, вы заметите, что строковые поля действительно сохранены в верхнем регистре (рис. 20.7). MyData.soap* X <SOAP-ENV:Envelope xmlns:xsi-"http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd^'http:>щ1 Э <SOAP-ENV:Body <al:StringData id*"ref-l" xmlns:al«"http://schemas.microsoft,com/clr/nsassem/CustofflSerl st_ItiMi id*"ref-3M>FIRST DATA BLOCK</First_Ite*> italteeTwo id-Vef-4" >M0RE DATA</dataItemTwo> </al:StringData> </SOAP-ENV:Body> </SOAP-ENV:Envelope> 1 Рис. 20.7. Настройка сериапизации с использованием интерфейса ISerializable Настройка сериализации с использованием атрибутов Хотя реализация интерфейса ISerializable является одним из возможных способов настройки процесса сериализации, с момента выхода версии .NET 2.0 предпочтительным способом такой настройки стало определение методов, оснащенных атрибутами из следующего перечня: [OnSerializing], [OnSerialized], [OnDeserializing] и [OnDeserialized]. Использование этих атрибутов дает менее громоздкий код, чем реализация интерфейса ISerializable, учитывая, что не приходится вручную взаимо-
752 Часть V. Введение в библиотеки базовых классов .NET действовать с параметром Serializationlnfo. Вместо этого можно напрямую модифицировать данные состояния, когда форматер работает с вашим типом. На заметку! Эти атрибуты сериализации определены в пространстве имен System.Runtime. Serialization. В случае применения этих атрибутов метод должен быть определен так, чтобы принимать параметр StreamingContext и не возвращать ничего (иначе будет сгенерировано исключение времени выполнения). Обратите внимание, что применять каждый из атрибутов сериализации не обязательно, а можно просто вмешаться в те стадии процесса сериализации, которые интересуют. Для иллюстрации ниже приведен новый тип [Serializable], к которому предъявлены те же требования, что и к StringData, но на этот раз он полагается на использование атрибутов [OnSerializing] и [OnDeserialized]: ' [Serializable] class MoreData { private string dataltemOne = "First data block"; private string dataItemTwo= "More data"; [OnSerializing] private void OnSerializing (StreamingContext context) { // Вызывается во время процесса сериализации. dataltemOne = dataltemOne.ToUpper() ; dataltemTwo = dataltemTwo.ToUpper() ; } [OnDeserialized] private void OnDeserialized(StreamingContext context) { // Вызывается no завершении процесса десериализации. dataltemOne = dataltemOne.ToLower(); dataltemTwo = dataltemTwo.ToLower (); } } Выполнив сериализацию этого нового типа, вы снова обнаружите, что данные сохраняются в верхнем регистре, а десериализуются — в нижнем. Исходный код. Проект CustomSerialization доступен в подкаталоге Chapter 20. Приведенный пример наглядно продемонстрировал основные детали служб сериализации объектов, включая различные способы настройки этого процесса. Как было показано, процессы сериализации и десериализации существенно упрощают задачу сохранения больших объемов данных, причем с меньшими затратами, чем работа с различными классами чтения/записи данных из пространства имен System. 10. Резюме Эта глава началась с рассмотрения использования типов Directory (Directorylnfo) и File (FileInfo). Вы узнали, что упомянутые классы позволяют манипулировать физическими файлами и каталогами на жестком диске. Затем вы ознакомились с несколькими типами, унаследованными от абстрактного класса Stream. Учитывая, что типы, унаследованные от Stream, оперируют низкоуровневым потоком байтов, пространство имен System. 10 предоставляет многочисленные типы для чтения/записи
Глава 20. Файловый ввод-вывод и сериализация объектов 753 (StreamWnter, StringWriter, BinaryWriter и т.п.), которые упрощают этот процесс. Попутно вы узнали о функциональности, предлагаемой типом DriveType, и научились наблюдать за файлами с применением типа FileSystemWatcher, а также взаимодействовать с потоками в асинхронном режиме. В главе также рассматривались службы сериализации объектов. Было показано, что платформа .NET использует граф объектов, чтобы учесть полный набор объектов, которые должны сохраниться в потоке. До тех пор, пока каждый объект в графе помечен атрибутом [Serializable], данные сохраняются в выбранном формате (двоичном или SOAP). Вы также ознакомились с возможностями настройки готового процесса сериализации посредством двух возможных подходов. Во-первых, вы узнали, как реализовать интерфейс ISerializable (и поддерживать специальный приватный конструктор), что позволяет вмешаться в процесс сохранения форматером данных объекта. Во-вторых, вы ознакомились с набором атрибутов .NET, которые упрощают процесс специальной сериализации. Все, что нужно — это просто применять атрибуты [OnSerializing], [OnSerialized], [OnDeserializing] или [OnDeserialized] к методам, принимающим параметр StreamingContext, и форматеры будут вызывать их на соответствующих фазах сериализации или десериализации.
ГЛАВА 21 ADO.NET, часть I: подключенный уровень Как и следовало ожидать, платформа .NET определяет ряд пространств имен, которые позволяют непосредственно взаимодействовать с локальными и удаленными реляционными базами данных. Все вместе эти пространства имен известны как ADO.NET. В данной главе вначале будет описана в общих чертах роль самой ADO.NET, а затем мы перейдем к теме поставщиков данных ADO.NET. Платформа .NET поддерживает множество различных поставщиков данных, каждый из которых оптимизирован на взаимодействие с конкретной СУБД (Microsoft SQL Server, Oracle, MySQL и т.д.). После того как мы разберемся с общими возможностями различных поставщиков данных, мы рассмотрим образец генератора поставщиков данных. Вы увидите, что имена из пространства имен System.Data.Common (и связанного с ним файла App.config) позволяют создать единую кодовую базу, которая может динамически выбрать соответствующий поставщик данных без необходимости перекомпиляции или нового развертывания кодовой базы данного приложения. Наверное, наиболее важно то, что в данной главе вы создадите собственную сборку библиотеки доступа к данным (AutoLotDAL.dll), в которой будут инкапсулированы различные операции, выполняемые в пользовательской базе данных по имени AutoLot. Эта библиотека, которая будет расширена в главах 23 и 24, неоднократно пригодится в последующих главах. И в завершение мы познакомимся с транзакциями баз данных. Высокоуровневое определение ADO.NET Если вы уже знакомы с предыдущей моделью Microsoft доступа к данным на основе COM (Active Data Objects — ADO), то сразу же предупреждаем: ADO.NET имеет очень мало общего с ADO, за исключением букв "A", "D" и "О". Хотя некоторая взаимосвязь между этими двумя системами и существует (например, в обеих имеется концепция объектов подключения и объектов команд), некоторые знакомые по ADO типы (например, Recordset) больше не существуют. Кроме того, в ADO.NET появился ряд новых типов, не имеющих прямых эквивалентов в классической ADO (например, адаптер данных). В отличие от классической ADO, которая была в основном предназначена для тесно связанных клиент-серверных систем, ADO.NET больше нацелена на автономную работу с помощью объектов DataSet. Эти типы представляют локальные копии любого количества взаимосвязанных таблиц данных, каждая из которых содержит набор строк и столбцов. Объекты DataSet позволяют вызывающей сборке (наподобие веб-страницы или программы, выполняющейся на настольном компьютере) работать с содержимым DataSet, изменять его, не требуя подключения к источнику данных, и отправлять об-
Глава 21. ADO.NET, часть I: подключенный уровень 755 * »о 15 i> {} Microsoft.SqlServer.5erver Ъ {} System.Data !> {} System.Data .Common > <} System.Data.Odbc f> {> System.Data.OteDb t> {} System.Data.Sqt > {} System.Data.SqfClient t> 0 System.Data.SqtTypes 0 О System.Xml ' f ' ill s C □ - ратно блоки измененных данных для обработки с помощью соответствующего адаптера данных. Но, пожалуй, самое фундаментальное различие между классической ADO и ADO.NET сострит в том, что ADO.NET является управляемой кодовой библиотекой, и, значит, подчиняется тем же правилам, что и любая управляемая библиотека. Типы, составляющие ADO.NET, используют протокол управления памятью CLR, принадлежат к той же системе типов (классы, интерфейсы, перечисления, структуры и делегаты), и доступ к ним воз- _. .TTVn Рис. 21.1. Ьэзовэя сооркэ можен с помощью любого языка .NET. *n^ ш-т « *. ~ *. ^-. -. _ ._Л.ТТОТ, ADO.NET-System.Data.dll С точки зрения программиста, тело ADO.NET составляет базовая сборка с именем System.Data.dll. В этом двоичном файле находится значительное количество пространств имен (рис. 21.1), многие из которых представляют типы конкретного поставщика данных ADO.NET (р которых речь пойдет ниже). Большинство шаблонов проектов Visual Studio 2010 автоматически ссылаются на эту ключевую библиотеку доступа к данным. Однако для импортирования нужных пространств имен необходимо изменить кодовые файлы, например: using System; // Задействование некоторых пространств имен AD0.NET using System.Data; using System.Data.SqlClient; namespace MyApp { class Program { static void Main(string [ ] args) { } } } Учтите также, что кроме System.Data.dll, существуют и другие ориентированные на ADO.NET сборки (например, System.Data.OracleClient.dll и System.Data. Entity.dll), которые необходимо вручную указывать в текущем проекте с помощью диалогового окна Add Reference (Добавление ссылки). Три стороны ADO.NET Библиотеки ADO.NET можно применять тремя концептуально различными способами: в подключенном режиме, в автономном режиме и с помощью технологии Entity Framework. При использовании подключенного уровня (connected layer), рассматриваемого в данной главе, кодовая база явно подключается к соответствующему хранилищу данных и отключается от него. При таком способе использования ADO.NET обычно происходит взаимодействие с хранилищем данных с помощью объектов подключения, объектов команд и объектов чтения данных. Автономный уровень (disconnected layer), который будет рассмотрен в главе 22, позволяет работать с набором объектов DataTable (содержащихся в DataSet), который представляет на стороне клиента копию внешних данных. При получении DataSet с помощью соответствующего объекта адаптера данных подключение открывается и закрывается автоматически. Понятно, что этот подход помогает быстро освобождать подключения для других вызовов и повышает масштабируемость систем.
756 Часть V. Введение в библиотеки базовых классов .NET Получив объект Data Set, вызывающий код может просматривать и обрабатывать данные без затрат на сетевой трафик. А если нужно занести изменения в хранилище данных, то адаптер данных (вместе с набором операторов SQL) задействуется для обновления данных — при этом подключение открывается заново для проведения обновлений в базе, а затем сразу же закрывается. После выпуска .NET 3.5 SP1 в ADO.NET появилась поддержка новую API, которая называется Entity Framework (сокращенно EF). Технология EF показывает, что многие низкоуровневые детали работы с базами данных (например, сложные SQL-запросы) скрыты от программиста и отрабатываются за него при генерации соответствующего LINQ- запроса (например, LINQ с Entities). Этот аспект ADO.NET будет рассмотрен в главе 23. Поставщики данных ADO.NET В отличие от других API для работы с базами данных, с которыми вам, возможно, приходилось работать раньше, в ADO.NET нет единого набора типов, которые взаимодействуют с различными системами управления базами данных (СУБД). Вместо этого в ADO.NET имеются различные поставщики данных (data provider), каждый из которых оптимизирован для взаимодействия с конкретной СУБД. Первая выгода этого подхода состоит в том, что можно запрограммировать особый поставщик данных для доступа к любым уникальным особенностям конкретной СУБД. Еще одна выгода — конкретный поставщик данных может напрямую подключиться к механизму соответствующей СУБД, не пользуясь междууровневым слоем отображения. В первом приближении поставщик данных можно рассматривать как набор типов, определенных в данном пространстве имен, который предназначен для взаимодействия с конкретным источником данных. Однако независимо от используемого поставщика данных, каждый из них определяет набор классов, обеспечивающих основную функциональность. В табл. 21.1 приведены некоторые общие основные объекты, их базовые классы (определенные в пространстве имен System.Data.Common) и основные интерфейсы (определенные в пространстве имен System.Data), которые они реализуют. Таблица 21.1. Основные объекты поставщиков данных ADO.NET Тип объекта Базовый класс оответствующие Назначение Connection DbConnection IDbConnection Позволяет подключаться к хранилищу данных и отключаться от него. Кроме того, объекты подключения обеспечивают доступ к соответствующим объектам транзакций Command DbCommand IDbCommand Представляет SQL-запрос или хранимую процедуру. Кроме того, объекты команд предоставляют доступ к объекту чтения данных конкретного поставщика данных DataReader DbDataReader IDataReader, Предоставляет доступ кдан- IDataRecord ным только для чтения в прямом направлении с помощью курсора на стороне сервера
Глава 21. ADO.NET, часть I: подключенный уровень 757 Окончание табл. 21.1 Тип объекта Базовый класс Соответствующие интерфейсы Назначение DataAdapter . DbDataAdapter Parameter DbParameter Transaction DbTransaction IDataAdapter, IDbDataAdapter IDataParameter, IDbDataParameter IDbTransaction Пересылает наборы данных из хранилища данных к вызывающему процессу и обратно. Адаптеры данных содержат подключение и набор из четырех внутренних объектов команд для выборки, вставки, изменения и удаления информации в хранилище данных Представляет именованный параметр в параметризованном запросе Инкапсулирует транзакцию в базе данных Конкретные имена этих основных классов различаются у различных поставщиков (например, SqlConnection, OracleConnection, OdbcConnection и MySqlConnection), но все эти объекты порождены от одного и того же базового класса (в случае объектов подключения это DbConnection), который реализует идентичные интерфейсы (вроде IDbConnection). Поэтому если вы научитесь работать с одним поставщиком данных, то легко справитесь и с остальными. На заметку! В AD0.NET термин "объект подключения" на самом деле относится к конкретному типу, порожденному от DbConnection; объекта подключения "вообще" нет. То же можно сказать и об "объекте команды", "объекте адаптера данных" и т.д. По соглашению имена объектов в конкретном поставщике данных имеют префиксы соответствующей СУБД (например, SqlConnection, OracleConnection, SqlDataReader и т.д.). На рис. 21.2 подробно показано, что означают поставщики данных в ADO.NET. На этой диаграмме "Клиентская сборка" может быть практически любым типом приложения .NET: консольная программа, приложение с использованием Windows-форм, вебстраница ASP.NET, WCF-служба, кодовая библиотека .NET и т.д. Кроме типов, показанных на рис. 21.2, поставщики данных содержат и другие типы. Но эти основные объекты определяют общие свойства всех поставщиков данных. Поставщики данных ADO.NET от Microsoft Дистрибутив .NET, поставляемый Microsoft, содержит множество различных поставщиков данных, например, поставщик в стиле Oracle, SQL Server и OLE DB/ODBC. В табл. 21.2 перечислены пространства имен и содержащие их сборки для всех поставщиков данных Microsoft ADO.NET. Таблица 21.2. Поставщики данных Microsoft AD0.NET Поставщик данных Пространство имен Сборка OLEDB Microsoft SQL Server Microsoft SQL Server Mobile ODBC System. Data. OleDb System. Data.SqlClient System. Data.SqlServerCe Sys tern. Data. Odbc System.Data.dll System.Data.dll System. Data.SqlServerCe.dll System.Data.dll
758 Часть V. Введение в библиотеки базовых классов .NET Клиентская сборка ч^~^~р • Поставщик данных платформы .NET Объект Connection Транзакция r\£L иоъект Command Коллекция параметров Объект DataReader Объект DataAdapter Команда Select Команда Insert Команда Update Команда Delete Рис. 21.2. Поставщики данных ADO.NET предоставляют доступ к конкретным СУБД На заметку! Поставщик данных, выполняющий непосредственное отображение на механизм Jet (и, следовательно, на Microsoft Access), отсутствует. Если вам нужно взаимодействие с файлом данных Access, это можно сделать с помощью поставщика данных OLE DB или ODBC. Поставщик данных OLE DB, состоящий из типов, которые определены в пространстве имен System.Data.OleDb, обеспечивает доступ к данным, находящимся в любом хранилище данных, если оно поддерживает классический протокол OLE DB на основе СОМ. Этот поставщик позволяет взаимодействовать с любой базой данных, совместимой с OLE DB — для этого потребуется лишь настроить сегмент Provider в строке подключения. Однако поставщик OLE DB неявно взаимодействует с различными СОМ-объектами, что может снизить производительность приложения. В общем-то, поставщик данных OLE DB нужен только для взаимодействия с какой-нибудь СУБД, для которой нет специального поставщика данных .NET. Но, учитывая тот факт, что сейчас у любой мало-мальски известной СУБД доступен для загрузки и специальный поставщик данных ADO.NET, следует рассматривать System.Data.OleDb как устаревшее пространство имен, которое вряд ли понадобится в мире .NET 4.0 (если к тому же учесть модель генератора поставщиков данных, введенную в .NET 2.0, о которой будет рассказано чуть ниже). На заметку! В одном случае использование типов из System.Data.OleDb все-таки необходимо, если понадобится взаимодействие с Microsoft SQL Server версии 6.5 или более ранней. Пространство имен System.Data.SqlClient может взаимодействовать только с Microsoft SQL Server версии 7.0 или более поздней. Поставщик данных Microsoft SQL Server предоставляет прямой доступ к хранилищам данных Microsoft SQL Server и только к хранилищам данных SQL Server (версии 7.0 или больше). Пространство имен System.Data.SqlClient содержит типы, необходимые для поставщика SQL Server, и предлагает ту же базовую функциональность, что и постав-
Глава 21. ADO.NET, часть I: подключенный уровень 759 щик OLE DB. Основное различие между ними состоит в том, что поставщик SQL Server не задействует уровень OLE DB и дает существенный выигрыш в производительности. А поставщик данных Microsoft SQL Server позволяет получить доступ к уникальным возможностям этой конкретной СУБД. Остальные поставщики, предоставляемые Microsoft (System.Data.OracleClient, System.Data.Odbc и System.Data.SqlClientCe), обеспечивают взаимодействие с ODBC-подключениями и доступ к SQL Server версии Mobile (которая обычно применяется на портативных устройствах наподобие Windows Mobile). Типы ODBC, определенные в пространстве имен System.Data.Odbc, обычно бывают нужны, только если требуется взаимодействие с СУБД, для которой нет специального поставщика данных .NET. Так бывает нередко, т.к. ODBC является широко распространенной моделью, которая предоставляет доступ к целому ряду хранилищ данных. 0 сборке System. Data. OracleClient. dll В предьщущих версиях платформы .NETимелась сборка System.Data.OracleClient.dll, которая, как понятно из названия, предоставляла поставщик данных для взаимодействия с базами данных Oracle. Однако в .NET 4.0 эта сборка помечена как устаревшая, а в дальнейшем будет считаться не рекомендуемой. Вы можете подумать, что ADO.NET постепенно концентрируется только на хранилищах данных от Microsoft, но это не так. Просто Oracle поставляет собственную сборку .NET, которая разработана на тех же общих принципах, что и поставщики данных, предоставляемые Microsoft. Если вам понадобится эта сборка, зайдите на страницу загрузки веб-сайта Oracle по адресу www.oracle.com/technology/tech/windows/odpnet/ index.html. Получение сторонних поставщиков данных ADO.NET Кроме поставщиков данных, поставляемых Microsoft (а также собственной библиотеки .NET для Oracle), существует и множество сторонних поставщиков данных для различных СУБД, как коммерческих, так и с открытым исходным кодом. Скорее всего, у вас не возникнет трудностей при получении поставщика данных ADO.NET непосредственно от разработчика СУБД, но на всякий случай возьмите на заметку следующий сайт: http://www.sqlsummit.com/DataProv.htm. Это один из множества сайтов, где собрана документация на все известные поставщики данных ADO.NET и ссылки на дополнительную информацию и сайты загрузки. Здесь перечислены различные поставщики ADO.NET: SQLite, IBM DB2, MySQL, PostgreSQL, TurboDB, Sybase и многие другие. Однако, несмотря на наличие множества поставщиков данных ADO.NET, в примерах этой главы будет использоваться поставщик данных Microsoft SQL Server (System.Data. SqlClient.dll). Этот поставщик позволяет взаимодействовать с Microsoft SQL Server версий 7.0 и выше, в том числе и с SQL Server Express Edition. Если вы хотите использовать ADO.NET для работы с другой СУБД, проблем возникнуть не должно, если вы уясните материал, изложенный ниже. Дополнительные пространства имен ADO.NET Кроме пространств имен .NET, которые определяют типы конкретных поставщиков данных, в библиотеках базовых классов .NET содержатся дополнительные пространства имен, ориентированные HaADO.NET. Некоторые из них перечислены в табл. 21.3 (сборки и пространства имен, относящиеся к Entity Framework, будут описаны в главе 24).
760 Часть V. Введение в библиотеки базовых классов .NET Таблица 21.3. Дополнительные пространства имен для работы с ADO.NET Пространство имен Назначение Microsoft.SqlServer.Server Содержит типы для работы службы интеграции CLR и SQL Server 2005 System.Data Определяет основные типы ADO.NET, используемые всеми поставщиками данных, в том числе общие интерфейсы и разнообразные типы, представляющие автономный уровень (например, DataSet и DataTable) System.Data.Common Содержит типы для общего использования всеми поставщиками ADO.NET, в том числе и общие абстрактные базовые классы Systern.Data. Sql Содержит типы, позволяющие обнаружить экземпляры Microsoft SQL Server, которые установлены в текущей локальной сети Sys tern. Data. SqlTypes Содержит типы данных, используемые Microsoft SQLServer. Можно применять и соответствующие типы CLR, но пространство SqlTypes оптимизировано для работы с SQL Server (например, если база данных SQL Server содержит целое значение, его можно представить либо как int, либо как SqlTypes.Sqllnt32) Мы не собираемся изучать каждый тип в каждом пространстве имен ADO.NET (эта задача потребовала бы отдельной книги), однако важно ознакомиться с типами из пространства имен System.Data. Типы из пространства имен System.Data System.Data является "наименьшим общим знаменателем" всех пространств имен ADO.NET. Если нужен доступ к данным, то приложения ADO.NET невозможно создать без указания этого пространства имен. Оно содержит общие типы, используемые всеми поставщиками данных ADO.NET, независимо от непосредственного хранилища данных. Кроме ряда исключений, специфичных для баз данных (NoNullAllowedException, RowNotlnTableException и MissingPrimaryKeyException), System.Data содержит типы, представляющие различные примитивы баз данных (например, таблицы, строки, столбцы и ограничения), а также общие интерфейсы, реализованные объектами поставщиков данных. Некоторые из основных типов перечислены в табл. 21.4. Таблица 21.4. Основные члены пространства имен System.Data Тип Назначение Constraint Ограничение для данного объекта DataColumn DataColumn Один столбец в объекте DataTable DataRelation Отношение "родительский-дочерний" между двумя объектами DataTable DataRow Одна строка в объекте DataTable DataSet Находящийся в памяти кэш данных, который состоит из любого количества взаимосвязанных объектов DataTable DataTable Табличный блок данных, находящихся в памяти DataTableReader Позволяет обращаться с DataTable как с примитивным курсором (доступ к данным только для чтения и только в прямом направлении)
Глава 21. ADO.NET, часть I: подключенный уровень 761 Окончание табл. 21.4 Тип Назначение DataView Специализированное представление DataTable для сортировки, фильтрации, поиска, редактирования и навигации iDataAdapter Определяет основное поведение объекта адаптера данных IDataParameter Определяет основное поведение объекта параметра IDataReader Определяет основное поведение объекта чтения данных IDbCommand Определяет основное поведение объекта команды IDbDataAdapter Расширяет IDataAdapter для получения дополнительных возможностей объекта адаптера данных iDbTransaction Определяет основное поведение объекта транзакции Подавляющее большинство классов из System.Data применяется при программировании для автономного уровня ADO.NET. В следующей главе вы ближе познакомитесь с DataSet и связанными с ним объектами (например, DataTable, DataRelation и Data Row), а также научитесь применять их (и соответствующие адаптеры данных) для представления копий удаленных данных на стороне клиента и работы с ними. Однако следующей задачей будет знакомство с основными интерфейсами System.Data на высоком уровне: это поможет лучше разобраться в общей функциональности любого поставщика данных. Конкретные детали вы будете узнавать на протяжении данной главы, а пока просто рассмотрим общее поведение произвольного типа интерфейса. Роль интерфейса IDbConnection Тип IDbConnection реализован объектом подключения (connection object) поставщика данных. Этот интерфейс определяет набор членов, применяемых для настройки подключения к конкретному хранилищу данных, а, кроме того, позволяет получить объект транзакции поставщика данных. Вот формальное определение IDbConnection: public interface IDbConnection : IDisposable { string ConnectionString { get; set; } int ConnectionTimeout { get; } string Database { get; } ConnectionState State { get; } IDbTransaction BeginTransaction () ; IDbTransaction BeginTransaction(IsolationLevel ll); void ChangeDatabase(string databaseName); void Close (); IDbCommand CreateCommand(); void Open(); } На заметку! Как и многие другие типы в библиотеках базовых классов .NET, метод Close () функционально эквивалентен непосредственному или косвенному вызову метода Dispose() с помощью области видимости С# (см. главу 8).
762 Часть V. Введение в библиотеки базовых классов .NET Роль интерфейса IDbTransaction Перегруженный метод BeginTransactionO, определенный в IDbConnection, предоставляет доступ к объекту транзакции (transaction object) поставщика. Члены, определенные в IDbTransaction, позволяют программным образом взаимодействовать с сеансом транзакций и соответствующим хранилищем данных: public interface IDbTransaction : IDisposable { IDbConnection Connection { get; } IsolationLevel IsolationLevel { get; } void Commit(); void Rollback(); } Роль интерфейса IDbCommand Интерфейс IDbCommand реализуется объектом команды (command object) поставщика данных. Как и другие объектные модели доступа к данным, объекты команды позволяют программно работать с операторами SQL, хранимыми процедурами и параметризованными запросами. Кроме того, объекты команды обеспечивают доступ к типу чтения данных поставщика данных с помощью перегруженного метода ExecuteReader(): public interface IDbCommand : IDisposable { string CommandText { get; set; } int CommandTimeout { get; set; } CommandType CommandType { get; set; } IDbConnection Connection { get; set; } IDataParameterCollection Parameters { get; } IDbTransaction Transaction { get; set; } UpdateRowSource UpdatedRowSource { get; set; } void Cancel (); IDbDataParameter CreateParameter(); int ExecuteNonQuery(); IDataReader ExecuteReader(); IDataReader ExecuteReader(CommandBehavior behavior); object ExecuteScalar (); void Prepare (); Роль интерфейсов IDbDataParameter и IDataParameter Свойство Parameters интерфейса IDbCommand возвращает строго типизированную коллекцию, которая реализует IDataParameterCollection. Этот интерфейс предоставляет доступ к набору классов, совместимых с IDbDataParameter (объекты параметров): public interface IDbDataParameter : IDataParameter { byte Precision { get; set; } byte Scale { get; set; } int Size { get; set; } } Интерфейс IDbDataParameter расширяет интерфейс IDataParameter с целью получения дополнительных возможностей:
Глава 21. ADO.NET, часть I: подключенный уровень 763 public interface IDataParameter { DbType DbType { get; set; } ParameterDirection Direction { get; set; } bool IsNullable { get; } string ParameterName { get; set; } string SourceColumn { get; set; } DataRowVersion SourceVersion { get; set; } object Value { get; set; } } Как видите, функциональность интерфейсов IDbDataParameter и IDataParameter позволяет использовать параметры в командах SQL (в том числе и в хранимых процедурах) с помощью особых объектов параметров ADO.NET вместо жестко закодированных строковых литералов. Роль интерфейсов IDbDataAdapter и IDataAdapter Адаптеры данных (data adapter) используются для выборки и занесения наборов данных DataSet в конкретное хранилище данных. Поэтому интерфейс IDbDataAdapter определяет набор свойств, предназначенных для поддержки операторов SQL для соответствующих операций выборки, вставки, изменения и удаления: public interface IDbDataAdapter : IDataAdapter { IDbCommand DeleteCommand { get; set; } IDbCommand InsertCommand { get; set; } IDbCommand SelectCommand { get; set; } IDbCommand UpdateCommand { get; set; } } Кроме этих четырех свойств, адаптер данных ADO.NET заодно получает поведение, определенное в базовом интерфейсе IDataAdapter. Этот интерфейс определяет основную функцию типа адаптера данных: возможность пересылать объекты DataSet между вызывающим процессом и непосредственным хранилищем данных с помощью методов Fill() и Update(). Кроме того, посредством свойства TableMappings интерфейс IDataAdapter позволяет отобразить имена столбцов из базы данных на более понятные отображаемые имена: public interface IDataAdapter { MissingMappingAction MissingMappingAction { get; set; } MissingSchemaAction MissingSchemaAction { get; set; } ITableMappingCollection TableMappings { get; } int Fill(System.Data.DataSet dataSet); DataTable[] FillSchema(DataSet dataSet, SchemaType schemaType); IDataParameter[] GetFillParameters (); int Update(DataSet dataSet); } Роль интерфейсов IDataReader и IDataRecord Еще один интерфейс, о котором важно знать — это IDataReader, который представляет общие функции, поддерживаемые конкретным объектом чтения данных. Получив от поставщика данных ADO.NET тип, совместимый с IDataReader, вы сможете просматривать результирующий набор, но только в прямом направлении и только просматривать.
764 Часть V. Введение в библиотеки базовых классов .NET public interface IDataReader : IDisposable, IDataRecord { int Depth { get; } bool IsClosed { get; } int RecordsAffected { get; } void Close () ; DataTable GetSchemaTable (); bool NextResult () ; bool Pead () ; } И, наконец, IDataReader расширяет интерфейс IDataRecord, в котором определено значительное количество членов, позволяющих сразу извлекать из потока строго типизированные значения, а не приводить к нужному типу обобщенный тип System.Object, который получен от перегруженного метода индексатора из объекта чтения данных. Ниже приведена часть листинга различных методов GetXXXO, определенных в интерфейсе IDataRecord (полный листинг см. в документации по .NET Framework 4.0 SDK): public interface IDataRecord { int FieldCount { get; } object this [ string name ] { get; } object this [ int i ] { get; } bool GetBoolean(int l) ; byte GetByte(int l) ; char GetChar(int l) ; DateTime GetDateTime(int l) ; Decimal GetDecimal (int l) ; float GetFloatdnt l) ; short Getlntl6(int l) ; int Getlnt32(int l) ; long Getlnt64(int l) ; bool IsDBNull(int l) ; 1 На заметку! Метод IDataReader.IsDBNull () позволяет программным способом узнать, установлено ли в конкретном поле значение null, прежде чем выбрать значение из объекта чтения данных (чтобы не возникло исключение времени выполнения). Вспомните, что С# поддерживает типы данных nullable (см. главу 4), идеально подходящие для взаимодействия со столбцами данных, которые могут иметь пустые значения в таблице базы данных. Абстрагирование поставщиков данных с помощью интерфейсов Теперь вы уже должны лучше разбираться в общей функциональности всех поставщиков данных .NET. И хотя точные имена реализованных типов отличаются в различных поставщиках данных, в программах они используются однотипно — в этом вся прелесть полиморфизма на основе интерфейсов. К примеру, если определить метод, принимающий параметр IDbConnection, то в него можно передать любой объект подключения ADO.NET: public static void OpenConnection(IDbConnection en) { // Открытие входящего подключения для вызывающего процесса. en.Open(); }
Глава 21. ADO.NET, часть I: подключенный уровень 765 На заметку! Использование интерфейсов не обязательно. Тот же результат можно получить, применяя в качестве параметров или возвращаемых значений абстрактные базовые классы (наподобие DbConnection). То же относится и к возвращаемым значениям. Рассмотрим, например, следующий простой проект консольного приложения (с именем MyConnectionFactory), в котором конкретный объект подключения выбирается в зависимости от одного из значений специального перечисления. Для диагностики мы просто выводим соответствующий объект подключения с помощью службы отображения: using System; using System.Collections .Generic- using System.Linq; using System.Text; // Необходимо для того, чтобы иметь определения общих интерфейсов //и различные объекты подключения для тестирования. using System.Data; using System.Data.SqlClient; using System.Data.Odbc; using System.Data.OleDb; namespace MyConnectionFactory { // Список возможных поставщиков. enum DataProvider { SqlServer, OleDb, Odbc, Oracle, None } class Program { static void Main(string [ ] args) { Console.WriteLine ("**** Очень простой генератор подключений *****\пм); // Получение конкретного подключения. IDbConnection myCn = GetConnection (DataProvider.SqlServer); Console.WriteLine("Ваше подключение— {0}", myCn.GetType().Name); // Открытие, использование и закрытие подключения... Console.ReadLine(); } // Этот метод возвращает конкретный объект подключения //на основе значения перечисления DataProvider. static IDbConnection GetConnection(DataProvider dp) { IDbConnection conn = null; switch (dp) { case DataProvider.SqlServer: conn = new SqlConnection () ; break; case DataProvider.OleDb: conn = new OleDbConnection (); break; case DataProvider.Odbc: conn = new OdbcConnection (); break; } return conn; } } }
766 Часть V. Введение в библиотеки базовых классов .NET Преимущество работы с обобщенными интерфейсами из System.Data (т.е. с абстрактными базовыми классами из System.Data.Common) состоит в том, что при этом имеется гораздо больше возможностей для создания гибкой кодовой базы, которую со временем можно развивать. Допустим, что сегодня вы создаете приложение для Microsoft SQL Server, но что, если спустя несколько месяцев ваша компания перейдет на Oracle? Если в своем решении вы жестко закодировали типы System.Data.SqlClient, ориентированные конкретно на MS SQL Server, то понятно, что при изменении СУБД на сервере придется снова выполнять редактирование, компиляцию и развертывание сборки. Повышение гибкости с помощью конфигурационных файлов приложения Для повышения гибкости приложений ADO.NET можно использовать клиентский файл *.conf ig, элемент <appSettings> которого может содержать произвольные пары ключ/значение. Вспомните: в главе 14 было сказано, что пользовательские данные, хранящиеся в файле *. con fig, можно получить программным образом с помощью типов из пространства имен System.Configuration. Предположим, например, что в каком- нибудь конфигурационном файле задано следующее значение поставщика данных: <configuration> <appSettings> <•-- Ключ и значение для соответствия одному из значений перечисления —> <add key="provider11 value=llSqlServer"/> </appSettings> </configuration> Теперь можно изменить метод Main(), чтобы программно получить соответствующий поставщик данных. По сути, при этом создается генератор объектов подключения, который позволяет изменить поставщик, но без необходимости перекомпиляции кодовой базы (нужно лишь изменить файл *. с on fig). Ниже приведены необходимые изменения в Main(): static void Main(string[] args) { Console.WriteLine ("**** Очень простой генератор подключений *****\n"); // Чтение ключа поставщика. string dataProvString = ConfigurationManager.AppSettings["provider"]; // Преобразование строки в перечисление. DataProvider dp = DataProvider.None; if (Enum.IsDefined(typeof(DataProvider) , dataProvString) ) dp = (DataProvider)Enum.Parse(typeof(DataProvider), dataProvString); else Console.WriteLine ("К сожалению, поставщик отсутствует."); // Получение конкретного подключения. IDbConnection myCn = GetConnection(dp); if(myCn != null) Console .WriteLine ("Ваше подключение — {О}11, myCn.GetType () .Name) ; // Открытие, использование и закрытие подключения . . . Console.ReadLine(); }
Глава 21. ADO.NET, часть I: подключенный уровень 767 На заметку! Для использования типа Conf igurationManager необходимо поместить ссылку на сборку System.Configuration.dll и импортировать пространство имен System. Configuration. Теперь у нас имеется код ADO.NET, позволяющий динамически задать нужное подключение. Очевидно, здесь есть одна проблема: эта абстракция используется только в приложении MyConnectionFactory.exe. Но если этот код оформить в библиотеку кода .NET (например, MyConnectionFactory.dll), то можно будет создавать любое количество клиентов, которые смогут получать различные объекты подключения с помощью уровней абстракции. Однако получение объекта подключения — лишь один аспект работы с ADO.NET Чтобы разработать хоть чего-то стоящую библиотеку генераторов поставщиков данных, необходимо учитывать объекты команд, объекты чтения данных, адаптеры данных, объекты транзакций и другие типы, ориентированные на работу с данными. Создание такой библиотеки кода не обязательно будет трудным, но все же потребует существенного объема кодирования и времени. Начиная с выпуска .NET 2.0, эта возможность встроена непосредственно в библиотеки базовых классов .NET. Ниже мы рассмотрим этот формальный API-интерфейс, но вначале понадобится создать собственную базу данных для работы на протяжении как этой главы, так и многих последующих глав. Исходный код. Проект MyConnectionFactory доступен в подкаталоге Chapter 21. Создание базы данных AutoLot Ниже в этой главе мы будем выполнять запросы к простой тестовой базе данных SQL Server с именем AutoLot. В продолжение постоянно используемой автомобильной темы, эта база данных будет содержать три взаимосвязанных таблицы (Inventory, Orders и Customers), содержащих различные данные о заказах гипотетической компании по продаже автомобилей. Ниже предполагается, что у вас имеется копия Microsoft SQL Server G.0 или выше) или копия Microsoft SQL Server 2008 Express Edition (http://msdn.microsoft.com/ vstudio/express/sql). Этот облегченный сервер баз данных отлично подходит для наших потребностей: он бесплатен, предоставляет графический интерфейс (SQL Server Management Tool) для создания и администрирования баз данных и интегрирован с Visual Studio 2010/Visual C# 2010 Express Edition. Для демонстрации последнего пункта остаток этого раздела будет посвящен созданию базы данных AutoLot с помощью Visual Studio 2010. Если вы пользуетесь Visual С# Express, то сможете выполнить аналогичные действия в окне проводника баз данных (Database Explorer, открывается с помощью пункта меню ViewO Other Windows (Вид^Другие окна)). На заметку! База данных AutoLot будет использоваться до конца книги. Создание таблицы Inventory Чтобы приступить к созданию тестовой базы данных, запустите Visual Studio 2010 и откройте Server Explorer через меню View (Просмотр). Затем щелкните правой кнопкой мыши на узле Data Connections (Подключения к данным) и выберите в контекстном меню пункт Create New SQL Server Database (Создать новую базу данных SQL Server).
768 Часть V. Введение в библиотеки базовых классов .NET В открывшемся диалоговом окне подключитесь к SQL Server, установленному на вашей локальной машине (с именем (local)), и укажите в поле имени базы данных AutoLot (рис. 21.3). Для наших целей можно оставить утентификацию Windows. Create New SQL Server Database ! 0 №l*J Enter information to connect to a SQL Server, then specify the name of a database to create Server name (!ocal)\SQL EXPRESS fiefresh J Log on to the server • Use Windows Authentication Use SQL Server Authentication Save my password New database name Рис. 21.3. Создание новой базы данных SQL Server 2008 Express с помощью Visual Studio 2010 На заметку! При использовании SQL Server Express можно просто ввести в текстовом поле Server name (Имя сервера) имя (local)\SQLEXPRESS. Сейчас база данных AutoLot совершенно пуста и не содержит никаких объектов (таблиц, хранимых процедур и т.п.). Для добавления новой таблицы щелкните правой кнопкой мыши на узле Tables (Таблицы) и выберите в контекстном меню пункт Add New Table (рис. 21.4). Server Explorer л ,jj Data Connections a ijL? andrewpc\sqlexpress.AutoLot.dbo j_3 Database Diagrams _i Tables;- :._J Views' Add New Table _i Stored Compare Data _J Functi. fj^ Que|y ■ СЛ Synonj С ClTypes|^ 2| Assemj & ;> f|| Servers !> |*0 SharePoint Connections Рис. 21.4. Добавление таблицы Inventory С помощью редактора таблиц добавьте в таблицу четыре столбца данных: Car ID (Идентификатор автомобиля), Make (Модель), Color (Цвет) и PetName (Дружественное имя). У столбца Car ID должно быть установлено свойство Primary Key (первичный ключ) — для этого щелкните правой кнопкой мыши на строке Car ID и выберите в контекстном меню пункт Set Primary Key (Установить первичный ключ). Окончательные параметры таблицы показаны на рис. 21.5. На панели Column Properties (Свойства столбца) ничего делать не надо, просто запомните типы данных для каждого столбца.
Глава 21. ADO.NET, часть I: подключенный уровень 769 dbo.Tablel: TableUsqlecpress-AutoLot)* X I Column Name ►tf| CarlD Make Color PetName Data Type varcharE0) varcharE0) varcharE0) Allow Nulls 1 D m m i Column Properties j (Name) Allow Nulls Data Type Default Value or Binding CarlD No int G| Рис. 21.5. Структура таблицы Inventory Сохраните и закройте новую таблицу; новый объект базы данных должен иметь имя Inventory. Теперь таблица Inventory должна быть видна под узлом Tables (Таблицы) в Server Explorer. Щелкните правой кнопкой мыши на ее значке и выберите в контекстном меню пункт Show Table Data (Просмотр данных таблицы). Введите информацию о нескольких новых автомобилях по своему усмотрению (чтобы было интереснее, пусть у некоторых автомобилей совпадают цвета и модели). Один из возможных вариантов списка товаров приведен на рис. 21.6. Inventory. Query(a...sqlexpress.AutoLot) CarlD Make 1000 BMW 1001 BMW 904 VW 83 Ford 107 Ford 678 Yugo 1992 Saab * \null null И i 3 of 7 ► И ► с Color Black Tan Black Rust Red Green Pink NULL ш 5Н5ЯЗНЕИ ИЗ PetName Bimmer Daisy Hank Rusty Snake Clunker Pinkey NULL Рис. 21.6. Заполнение таблицы Inventory Создание хранимой процедуры GetPetNameQ Ниже в данной главе будет показано, как вызывать хранимые процедуры в ADO.NET. Возможно, вы уже знаете, что хранимые процедуры — это подпрограммы, хранимые непосредственно в базе данных; обычно они работают с данными таблиц и возвращают какое-то значение. Мы добавим в базу данных одну хранимую процедуру, которая по идентификатору автомобиля будет возвращать его дружественное имя. Для этого щелкните правой кнопкой мыши на узле Stored Procedures (Хранимые процедуры) базы данных AutoLot в Server Explorer и выберите в контекстном меню пункт Add New Stored Procedure (Добавить новую хранимую процедуру). В появившемся окне редактора введите следующий текст:
770 Часть V. Введение в библиотеки базовых классов .NET Server Explorer (Jjf Data Connections л У^ andrewpc\sqlexpress.AutoLot.dbo 0 C3 Database Diagrams л CM Tables c> 13 Inventory £> Q| Views л Cj Stored Procedures 3 ©carlD J& ©petName !> Qa Functions о CM Synonyms > CM Types j> CM Assemblies Щ Servers jfti SharePomt Connections Рис. 21.7. Хранимая процедура GetPetName CREATE PROCEDURE GetPetName @carID int, @petName char A0) output AS SELECT @petName = PetName from Inventory- where CarlD = @carID При сохранении этой процедуре автоматически будет присвоено имя GetPetName, взятое из оператора CREATE PROCEDURE (учтите, что при первом сохранении Visual Studio 2010 автоматически изменяет имя SQL-сценария на "ALTER PROCEDURE..."). После этого новая хранимая процедура будет видна в Server Explorer (рис. 21.7). На заметку! Хранимые процедуры не обязательно должны возвращать данные через выходные параметры, как это сделано здесь; однако это пригодится, когда речь пойдет о свойстве Direction объектов SqlParameter ниже в данной главе. Создание таблиц Customers и Orders В нашей тестовой базе данных должны быть еще две таблицы: Customers (Клиенты) и Orders (Заказы). Таблица Customers будет содержать список клиентов и состоять из трех столбцов: CustID (Идентификатор клиента; должен быть первичным ключом), FirstName (Имя) и LastName (Фамилия). Повторите шаги, которые были выполнены для создания таблицы Inventory, и создайте таблицу Customers, пользуясь схемой, приведенной на рис. 21.8. dbo.Tablei Table(...sqlexpressAutoLot)* Column Name Data Type Щ CustiD int FirstName varcharE0) LastName varcharE0) Allow Nulls □ m В Column Properties л (General) (Name) Allow Nulls Data Type Default Value or Binding ТэЫе Designer CustID No 1 Щ (General) Рис. 21.8. Структура таблицы Customers После сохранения этой таблицы добавьте в нее несколько записей (рис. 21.9). Последняя наша таблица — Orders — предназначена для связи клиентов и интересующих их автомобилей. Для этого выполняется отображение значений OrderlD на CarlD/CustID. Ее структура показана на рис. 21.10 (здесь OrderlD также является первичным ключом).
Глава 21. ADO.NET, часть I: подключенный уровень 771 Customers Query( CustID > '1 2 3 4 * \null И < |l i...qle cf 4 xpress-AutoLot) FirstName Dave Matt Steve Pat NULL ► и ► X »' лшш^^шв LastName Brenner Walton Hagen Walton NULL Рис. 21.9. Заполнение таблицы Customers аЬо.ТаЫеЗ: TableUsqlexpressAutoLot)* X | Column Name Data Type Wj OrdedD int CustID int CarlD int В П о Column Properties .|Sl>t;jj., л (General) (Name) Allow Nulls Data Type (General) I OrderlD No int ' Рис. 21.10. Структура таблицы Orders Теперь добавьте в таблицу Orders данные. Выберите для каждого значения CustID уникальное значение CarlD (предположим, что значения OrderlD начинаются с 1000 (рис. 21.11)). Orders: Query(andr...sqlexpress.AutoLot) X OrderlD ► 1000 1001 jl002 |l003 * \NULL и < ■ i of 4 CustID 1 2 3 4 NULL ► N ► ® CarlD 1000 678 904 1992 NULL : ! Рис. 21.11. Заполнение таблицы Orders Например, в соответствии с информацией, приведенной на рисунках, видно, что Дэйв Бреннер (Dave Brenner, CustID = 1) мечтает о черном BMW (CarlD = 1000), а Пэт Уолтон (Pat Walton, CustID = 4) приглянулся розовый Saab (CarlD = 1992).
772 Часть V. Введение в библиотеки базовых классов .NET Визуальное создание отношений между таблицами И, наконец, между таблицами Customers, Orders и Inventory нужно установить отношения "родительский-дочерний". В Visual Studio 2010 это выполняется очень просто, т.к. она позволяет вставить новую диаграмму базы данных на этапе проектирования. Для этого откройте Server Explorer, щелкните правой кнопкой мыши на узле Database Diagrams базы AutoLot и выберите пункт контекстного меню Add New Diagram (Добавить новую диаграмму). Откроется диалоговое окно, в котором можно выбирать таблицы и добавлять их в диаграмму. Выберите все таблицы из базы данных AutoLot (рис. 21.12). Рис. 21.12. Выбор таблиц для диаграммы Чтобы начать устанавливать отношения между таблицами, щелкните на ключе CardID таблицы Inventory, а затем (не отпуская кнопку мыши) перетащите его на поле CardID таблицы Orders. Когда вы отпустите кнопку, появится диалоговое окно; согласитесь со всеми предложенными в нем значениями по умолчанию. Теперь повторите те же действия для отображения ключа CustID таблицы Customers на поле CustID таблицы Orders. После этого вы увидите диалоговое окно классов, показанное на рис. 21.13 (было включено отображение отношений между таблицами за счет щелчка правой кнопкой мыши на конструкторе и выбора в контекстном меню пункта Show Relationship Labels (Показывать метки взаимосвязи)). Вот и все, база AutoLot полностью готова. Конечно, это лишь бледное подобие реальных корпоративных баз данных, но она отлично послужит нам до конца книги. И теперь, имея тестовую базу, можно начать подробно разбираться в модели генератора поставщиков данных ADO.NET. Модель генератора поставщиков данных AD0.NET Генератор поставщиков данных .NET позволяет создать единую кодовую базу с помощью обобщенных типов доступа к данным. Более того, посредством конфигурационных файлов приложения (и подэлемента <connectionStrings>) можно получить поставщики и строки подключения без необходимости в повторной компиляции или развертывания сборки, в которой используются API ADO.NET.
Глава 21. ADO.NET, часть I: подключенный уровень 773 Inventory * FK_Orders_InventoTY ksQ ~ OCJ ^ СагЮ Make Color PetName Orders * 9 OrderiD CustID CarlD FK_Orders_CustDmers Customers * 9 CustID FirstName LastName UJ j№ Рис. 21.13. Отношения между таблицами Orders, Inventory и Customers Чтобы разобраться в реализации генератора поставщиков данных, вспомните из табл. 21.1, что все классы в поставщике данных порождены от одних и тех же базовых классов, определенных в пространстве имен System.Data.Common: • DbCommand — абстрактный базовый класс для всех объектов команд; • Disconnection — абстрактный базовый класс для всех объектов.подключений; • DbDataAdapter — абстрактный базовый класс для всех объектов адаптеров данных; • DbDataReader — абстрактный базовый класс для всех объектов чтения данных; • DbParameter — абстрактный базовый класс для всех объектов параметров; • Db Transaction — абстрактный базовый класс для всех объектов транзакций, Все поставщики данных, разработанные Microsoft, содержат класс, порожденный от System.Data.Common.DbProviderFactory. В этом базовом классе определен ряд методов, которые выбирают объекты данных, характерные для конкретных поставщиков. Вот фрагмент с членами DbProviderFactory: DbProviderFactory public virtual DbCommand CreateCommand(); public virtual DbCommandBuilder CreateCommandBuilder(); public virtual DbConnection CreateConnection(); public virtual DbConnectionStringBuilder CreateConnectionStringBuilder(); public virtual DbDataAdapter CreateDataAdapter(); public virtual DbDataSourceEnumerator CreateDataSourceEnumerator(); public virtual DbParameter CreateParameter() ; } Для получения типа, порожденного от DbProviderFactory, непосредственно для вашего поставщика данных в пространстве имен System.Data.Common имеется класс DbProviderFactories. С помощью метода GetFactoryO можно получить конкретный объект DbProviderFactory для указанного поставщика данных. Для этого нужно указать строковое имя, которое представляет пространство имен .NET, содержащее функциональность поставщика:
774 Часть V. Введение в библиотеки базовых классов .NET static void Main(string [ ] args) { // Получение генератора для поставщика данных SQL. DbProviderFactory sqlFactory = DbProviderFactories.GetFactory("System.Data.SqlClient"); } Разумеется, генератор можно получить не с помощью жестко закодированного строкового литерала, а, например, прочитать эту информацию из клиентского файла *. con fig (приблизительно также, как в предыдущем примере с MyConnectionFactory). Вскоре вы увидите, как это сделать, а пока после получения генератора для поставщика данных можно получить связанные с ним объекты данных (например, подключения, команды и объекты чтения данных). На заметку! Для всех практических целей можно рассматривать аргумент, передаваемый в DbProviderFactories.GetFactory (), как имя пространства имен .NET для поставщика данных. В реальности это строковое значение используется в значении machine.config для динамической загрузки нужной библиотеки из глобального кэша сборок. Полный пример генератора поставщиков данных Для примера мы создадим новое консольное С#-приложение с именем DataProviderFactory, которое выводит список всех автомобилей из базы данных AutoLot. Поскольку это первый пример, мы жестко закодируем логику доступа к данным непосредственно в сборке DataProviderFactory.exe (чтобы пока не усложнять программирование). Но когда мы начнем разбираться в деталях модели программирования ADO.NET, мы изолируем логику данных в специальную кодовую библиотеку .NET, которая будет использоваться на протяжении всего оставшегося текста. Вначале добавьте ссылку на сборку System.Configuration.dll и импортируйте пространство имен System.Configuration. Затем добавьте файл Арр.config в текущий проект и определите пустой элемент <appSettings>. Добавьте новый ключ по имени provider, который отображает в пространство имен нужное имя поставщика данных (System.Data.SqlClient). Кроме того, определите строку подключения, которая описывает подключение к базе данных AutoLot (с помощью локального экземпляра SQL Server Express): <?xml version="l.0м encoding=Mutf-8M ?> <configuration> <appSettings> <!-- Поставщик --> <add key="provider11 value="System. Data. SqlClient" /> <!-- Строка подключения --> <add key=,,cnStr" value="Data Source= (local)\SQLEXPRESS; Initial Catalog=AutoLot; Integrated Secunty=True"/> </appSettings> </configuration> На заметку! Чуть ниже мы рассмотрим строки подключения подробнее. А пока учтите, что если выбрать в Server Explorer значок базы данных AutoLot, то можно скопировать и вставить правильную строку подключения из свойства Connection String (Строка подключения) в окне Properties (Свойства) Visual Studio 2010.
Глава 21. ADO.NET, часть I: подключенный уровень 775 Теперь у вас есть корректный файл *.config, и вы можете прочитать значения provider и cnStr с помощью индексатора ConfigurationManager.AppSettings. Значение provider нужно передать в метод DbProviderFactories.GetFactoryO, чтобы получить тип генератора для необходимого поставщика данных. Значение cnStr будет использовано для установки свойства ConnectionString в типе, порожденном от DbConnection. Если вы импортировали пространства имен System.Data и System.Data.Common, метод Main () можно изменить следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Работа с генераторами поставщиков данных *****\п"); // Получение строки подключения и поставщика из *.config. string dp = ConfigurationManager.AppSettings["provider"]; string cnStr = ConfigurationManager.AppSettings["cnStr"]; // Получение генератора поставщика. DbProviderFactory df = DbProviderFactories.GetFactory(dp); // Получение объекта подключения. using (DbConnection en = df.CreateConnection()) { Console.WriteLine("Ваш объект подключения: {0}", en.GetType().Name); en.ConnectionString = cnStr; en.Open () ; // Создание объекта команды. DbCommand cmd = df.CreateCommand(); Console.WriteLine ("Ваш объект команды: {0}", cmd.GetType() .Name); cmd.Connection = en; cmd.CommandText = "Select * From Inventory"; // Вывод данных с помощью объекта чтения данных. using (DbDataReader dr = cmd.ExecuteReader ()) { Console.WriteLine("Ваш объект чтения данных: {0}", dr.GetType () .Name); Console.WriteLine (M\n***** Текущее содержимое Inventory *****"); while (dr.Read()) Console.WriteLine ("-> Автомобиль N'{0} — {1} . " , dr["CarlD"], dr["Make"] .ToString ()); } } Console.ReadLine(); } Здесь в целях диагностики с помощью службы рефлексии выводятся полностью определенные имена соответствующих объектов подключения, команды и чтения данных. Если запустить это приложение, то на консоль будут выведены текущие данные из таблицы Inventory базы данных AutoLot: ***** Работа с генераторами поставщиков данных ***** Ваш объект подключения: SqlConnection Ваш объект команды: SqlCommand Ваш объект чтения данных: SqlDataReader
776 Часть V. Введение в библиотеки базовых классов .NET ***** Текущее содержимое Inventory ***** -> Автомобиль N'83 — Ford. -> Автомобиль N'107 — Ford. -> Автомобиль N'678 — Yugo. -> Автомобиль N'904 - VW. -> Автомобиль N'1000 - BMW. -> Автомобиль N'1001 - BMW. -> Автомобиль N'1992 - Saab. Теперь укажите в файле *.config в качестве поставщика данных System.Data. OleDb (и измените строку подключения и сегмент Provider): <configuration> <appSettings> <!-- Поставщик --> <add key="provider11 value="System. Data .OleDb" /> <!-- Строка подключения --> <add key="cnStr11 value= "Provider=SQLOLEDB;Data Source=(local)\SQLEXPRESS; Integrated Security=SSPI;Initial Catalog=AutoLot"/> </appSettings> </configuration> Вы обнаружите, что неявно были задействованы типы System.Data.OleDb: ***** Работа с генераторами поставщиков данных ***** Ваш объект подключения: OleDbConnection Ваш объект команды: OleDbCommand Ваш объект чтения данных: OleDbDataReader ***** Текущее содержимое Inventory ***** -> Автомобиль N'83 — Ford. -> Автомобиль N'107 — Ford. -> Автомобиль N'678 — Yugo. -> Автомобиль N'904 — VW. -> Автомобиль N'1000 - BMW. -> Автомобиль N'1001 - BMW. -> Автомобиль N'1992 - Saab. Конечно, при недостатке опыта работы с ADO.NET вы можете не быть уверенными, что именно делают объекты подключения, команды и чтения данных. Пока не вдавайтесь в детали (ведь в этой главе еще много страниц!) и просто уясните, что модель генератора поставщиков данных ADO.NET позволяет создать единую кодовую базу, которая может использовать различные поставщики данных в декларативной манере. Возможные трудности с моделью генератора поставщиков Это действительно очень мощная модель, но все же нужно проверить, что в кодовой базе используются только типы и методы, общие для всех поставщиков как потомков абстрактных базовых классов. Поэтому при разработке кодовой базы следует ограничиться членами из DbConnection, DbCommand и других типов из пространства имен System. Data. Common. Но такой обобщенный подход не позволит непосредственно задействовать дополнительные возможности конкретной СУБД. Если все же потребуются вызовы специфических членов конкретного поставщика (например, SqlConnection), то это можно сделать с помощью явного преобразования типа, как в данном примере:
Глава 21. ADO.NET, часть I: подключенный уровень 777 using (DbConnection en = df.CreateConnection()) { Console.WriteLine("Ваш объект подключения: {0}", en.GetType () .Name); en.ConnectionString = cnStr; en.Open(); if (en is SqlConnection) { // Вывод используемой версии SQL Server. Console.WriteLine(((SqlConnection)en).ServerVersion); } } Однако при этом сопровождение кодовой базы несколько затруднится (а гибкость снизится), поскольку придется еще добавить ряд проверок времени выполнения. И все же если понадобится создать библиотеки доступа к данным наиболее гибким способом, то модель генератора поставщиков данных предоставляет для этого замечательный механизм. Элемент <connectionStrings> Пока наша строка подключения находится в элементе <appSettings> файла *.config. В конфигурационных файлах приложения может быть определен элемент <connectionStrings>. В этом элементе можно задать любое количество пар имя/значение, которые программа может прочитать в память с помощью индексатора ConfigurationManager.ConnectionStrings. Одним из преимуществ данного подхода (по сравнению с использованием элемента <appSettings> и индексатора ConfigurationManager.AppSettings) является то, что при этом можно однотипным образом определить несколько строк подключения для одного приложения. Чтобы увидеть все это в действии, модифицируйте текущий файл App.conf ig следующим образом (обратите внимание, что каждая строка подключения описывается с помощью атрибутов name и ConnectionString, а не атрибутов key и value, как в <appSettings>): <configuration> <appSettings> <!— Поставщик —> <add key=MproviderM value="System.Data.SqlClient" /> </appSettings> <!— Строки подключения --> <connectionStrings> <add name =llAutoLotSqlProviderM ConnectionString = "Data Source=(local)\SQLEXPRESS; Integrated Security=SSPI;Initial Catalog=AutoLot"/> <add name ="AutoLot01eDbProvider11 ConnectionString = "Provider=SQLOLEDB;Data Source=(local)\SQLEXPRESS; Integrated Security=SSPI;Initial Catalog=AutoLot"/> </connectionStrings> </configuration> Теперь можно изменить метод Main(): static void Main(string[] args) { Console.WriteLine("***** Работа с генераторами поставщиков данных *****\п"); string dp = ConfigurationManager.AppSettings["provider"]; string cnStr = ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString; }
778 Часть V. Введение в библиотеки базовых классов .NET Мы получили приложение, которое может выводить содержимое таблицы Inventory базы данных AutoLot, используя нейтральную кодовую базу. Вынесение имени поставщика и строки подключения во внешний файл *. с on fig позволяет модели генератора поставщиков данных самостоятельно динамически загружать нужный поставщик. Итак, первый пример закончен, и теперь можно углубиться в детали работы с подключенным уровнем ADO.NET. На заметку! Теперь вы уже оценили роль генераторов поставщиков данных в ADO.NET, и в последующих примерах данной книги мы будем явно использовать типы из пространства имен System. Data.SqlClient, чтобы не отвлекаться от текущих задач. Если вы пользуетесь другой СУБД (например, Oracle), то вам надо будет соответствующим образом изменить кодовую базу. Исходный код. Проект DataProviderFactory доступен в подкаталоге Chapter 21. Подключенный уровень ADO.NET Вспомните, что подключенный уровень ADO.NET позволяет взаимодействовать с базой данных с помощью объектов подключения, чтения данных и команд конкретного поставщика данных. Вы уже использовали эти объекты в предыдущем приложении DataProviderFactory, но все же мы рассмотрим весь процесс еще раз на расширенном примере. При необходимости подключиться к базе данных и прочитать записи с помощью объект чтения данных нужно выполнить следующие шаги. 1. Создайте, настройте и откройте объект подключения. 2. Создайте и настройте объект команды, указав объект подключения в аргументе конструктора или через свойство Connection. 3. Вызовите метод ExecuteReader () настроенного объекта команды. 4. Обрабатывайте каждую запись с помощью метода Read() объекта чтения данных. А теперь к делу. Создайте новое консольное приложение по имени AutoLotDataReader и импортируйте пространства имен System.Data и System.Data.SqlClient. Вот полный код метода Main(), который будет проанализирован ниже: class Program { static void Main(string [] args) { Console.WriteLine ("***** Работа с объектами чтения данных *****\п"); // Создание открытого подключения. using(SqlConnection en = new SqlConnection()) { en.ConnectionString = @"Data Source=(local)\SQLEXPRESS;Integrated Security=SSPI;" + "Initial Catalog=AutoLot"; en.Open(); // Создание объекта команды SQL. string strSQL = "Select * From Inventory"; SqlCommand myCommand = new SqlCommand(strSQL, en) ; // Получение объекта чтения данных с помощью ExecuteReader(). using(SqlDataReader myDataReader = myCommand.ExecuteReader()) {
Глава 21. ADO.NET, часть I: подключенный уровень 779 // Просмотр всех результатов. while (myDataReader.Read()) { Console.WriteLine("-> Make: {0}, PetName: {1}, Color: {2}.", myDataReader ["Make"] .ToStringO .Trim() , myDataReader["PetName"].ToString().Trim(), myDataReader["Color"].ToString().Trim()); } } } Console.ReadLine(); } } Работа с объектами подключения Первое, что нужно сделать при работе с поставщиком данных — это установить сеанс с источником данных с помощью объекта подключения (порожденного, как вы помните, от DbConnection). У объектов подключения .NET имеется форматированная строка подключения, которая содержит ряд пар имя/значение, разделенных точками с запятой. Эта информация содержит имя машины, к которой нужно подключиться, необходимые параметры безопасности, имя базы данных на этой машине и другую информацию, зависящую от поставщика. Из приведенного выше кода можно понять, что имя Initial Catalog относится к базе данных, с которой нужно установить сеанс. Имя Data Source определяет имя машины, на которой расположена база данных. Элемент (local) позволяет указать текущую локальную машину (независимо от конкретного имени этой машины), а элемент \SQLEXPRESS сообщает поставщику SQL Server, что вы подключаетесь к стандартной инсталляции SQL Server Express (если вы создали AutoLot с помощью полной версии SQL Server 2005 или более ранней, укажите Data Source= (local)). Кроме того, можно указать любое количество элементов, которые задают полномочия безопасности. В нашем примере имени Integrated Security присвоено значение SSPI (что эквивалентно true), которое использует для аутентификации пользователя текущие полномочия учетной записи Windows. На заметку! Назначение каждой пары имя/значение для вашей СУБД можно узнать в документации по .NET Framework 4.0 SDK, в описании свойства ConnectionString объекта подключения для вашего поставщика данных. При наличии строки подключения вызов Ореп() устанавливает соединение с СУБД. В дополнение к членам ConnectionString, Open() и Close () объект подключения содержит ряд членов, которые позволяют настроить дополнительные параметры подключения, например, время тайм-аута и информацию, относящуюся к транзакциям. В табл. 21.5 приведены некоторые члены базового класса DbConnection. Таблица 21.5. Члены типа DbConnection Член Назначение BeginTransaction () Используется для начала транзакции базы данных ChangeDatabaseO Изменяет базу данных для открытого подключения ConnectionTimeout Свойство только для чтения. Возвращает время ожидания при установке подключения, после которого ожидание прекращается и выдается сообщение об ошибке (по умолчанию 15 секунд). Для изменения этого времени нужно изменить в строке подключения сегмент Connect Timeout (например, Connect Timeout=30)
780 Часть V. Введение в библиотеки базовых классов .NET Окончание табл. 21.5 Член Назначение Database Свойство только для чтения. Содержит имя базы данных, с которой связан объект подключения DataSource Свойство только для чтения. Содержит местоположение базы данных, с которой связан объект подключения GetSchema () Этот метод возвращает объект DataTable, содержащий информацию схемы из источника данных State Свойство только для чтения. Содержит текущее состояние подключения в виде одного из значений перечисления ConnectionState Свойства типа DbConnection предназначены в основном только для чтения и поэтому нужны, если требуется получить характеристики подключения во время выполнения. Если понадобится изменить стандартные значения, необходимо будет изменить саму строку подключения. Например, можно изменить время тайм-аута с 15 на 30 секунд: static void Main(string[] args) { Console.WriteLine ("***** Работа с объектами чтения данных *****\п"); using(SqlConnection en = new SqlConnection ()) { en.ConnectionString = @"Data Source=(local)\SQLEXPRESS;" + "Integrated Security=SSPI;Initial Catalog=AutoLot;Connect Timeout=30"; en.Open(); // Новая вспомогательная функция (см. ниже). ShowConnectionStatus (en); } } В этом коде объект подключения передается в качестве параметра новому статическому вспомогательному методу ShowConnectionStatus () из класса Program, который реализован следующим образом: static void ShowConnectionStatus(DbConnection en) { // Вывод различных сведений о текущем объекте подключения. Console.WriteLine("***** Информация о подключении *****"); Console.WriteLine("Местоположение базы данных: {0}", en.DataSource); Console.WriteLine("Имя базы данных: {0}", en.Database); Console.WriteLine("Тайм-аут: {0}", en.ConnectionTimeout); Console.WriteLine("Состояние подключения: {0}\n", en.State.ToString()); } Все эти свойства понятны без объяснений, за исключением разве что свойства State. В принципе ему можно присвоить любое значение из перечисления ConnectionState: public enum ConnectionState { Broken, Closed, Connecting, Executing, Fetching, Open }
Глава 21. ADO.NET, часть I: подключенный уровень 781 Тем не менее, допустимыми считаются только значения ConnectionState.Open и ConnectionState.Closed (остальные значения зарезервированы для будущего использования). И учтите, что подключение можно закрыть без проблем, если его состояние равно ConnectionState.Closed. Работа с объектами ConnectionStringBuilder Программная работа со строками подключения может оказаться несколько затруднительной, поскольку они часто представлены в виде строковых литералов, которые трудно обрабатывать и контролировать на наличие ошибок. Поставщики данных ADO.NET, разработанные Microsoft, поддерживают объекты построителей строк подключения (connection string builder object), которые позволяют устанавливать пары имя/значение с помощью строго типизированных свойств. Рассмотрим следующую модификацию нашего метода Main (): static void Main(string [ ] args) { Console.WriteLine ("***** Работа с объектами чтения данных *****\п"); // Создание строки подключения с помощью объекта построителя. SqlConnectionStringBuilder cnStrBuilder = new SqlConnectionStringBuilder(); cnStrBuilder.InitialCatalog = "AutoLot"; cnStrBuilder.DataSource = @"(local)\SQLEXPRESS"; cnStrBuilder.ConnectTimeout = 30; cnStrBuilder.IntegratedSecurity = true; using(SqlConnection en = new SqlConnection ()) { cn.ConnectionString = cnStrBuilder.ConnectionString; en.Open (); ShowConnectionStatus(en); } Console.ReadLine(); } В этом варианте создается экземпляр SqlConnectionStringBuilder, устанавливаются его свойства, и выбирается внутренняя строка из свойства ConnectionString. Здесь использован стандартный конструктор типа. При этом можно также создать экземпляр объекта построителя для строки подключения поставщика данных, передав в качестве отправной точки существующую строку подключения (это может оказаться удобным при динамическом чтении значений из файла App.config). После наполнения объекта начальными строковыми данными можно изменить отдельные пары имя/ значение с помощью соответствующих свойств, как в следующем примере: static void Main(string[] args) { Console.WriteLine ("***** Работа с объектами чтения данных *****\п"); // Допустим, мы получили значение cnStr из файла *.config. string cnStr = @"Data Source=(local)\SQLEXPRESS;" + "Integrated Security=SSPI;Initial Catalog=AutoLot"; SqlConnectionStringBuilder cnStrBuilder = new SqlConnectionStringBuilder(cnStr); // Изменение значения тайм-аута. cnStrBuilder.ConnectTimeout = 5;
782 Часть V. Введение в библиотеки базовых классов .NET Работа с объектами команд Теперь, когда мы разобрались с ролью объекта подключения, можно рассмотреть, как отправлять SQL-запросы в базу данных. Тип SqlCommand (порожденный от DbCommand) представляет собой объектно-ориентированное представление SQL-запроса, имени таблицы или хранимой процедуры. Тип команды указывается свойством CommandType, которое принимает значения из перечисления CommandType: public enum CommandType { StoredProcedure, TableDirect, Text // Значение по умолчанию. } При создании объекта команды можно сразу задать SQL-запрос, передав его с помощью параметра конструктора или непосредственно свойства CommandText. Кроме того, при создании объекта команды необходимо указать подключение, которое будет в нем применяться. Это тоже можно сделать либо через параметр конструктора, либо с помощью свойства Connection, как в следующем фрагменте кода: // Создание объекта команды с помощью аргументов конструктора. string strSQL = "Select * From Inventory"; SqlCommand myCommand = new SqlCommand(strSQL, en); // Создание еще одного объекта команды с помощью свойств. SqlCommand testCommand = new SqlCommand(); testCommand.Connection = en; testCommand.CommandText = strSQL; Учтите, что на этом этапе SQL-запрос еще не отправляется в базу данных AutoLot, здесь лишь подготавливается состояние объекта команды для дальнейшего использования. В табл. 21.6 описаны некоторые дополнительные член типа DbCommand. Таблица 21.6. Члены типа DbCommand Член Назначение CommandTimeout Выдает или устанавливает время ожидания при выполнении команды до прекращения попытки и генерации ошибки. По умолчанию равно 30 секунд Connection Выдает или устанавливает объект DbConnection, используемый данным DbCommand Parameters Выдает коллекцию типов DbParameter, используемых для параметризованного запроса Cancel () Отменяет выполнение команды ExecuteReader () Выполняет SQL-запрос и возвращает объект DbDataReader поставщика данных, позволяющий доступ к результату запроса только для чтения в прямом направлении ExecuteNonQuery () Выполняет не запросный SQL (например, вставка, обновление, удаление или создание таблицы) ExecuteScalarO Облегченная версия метода ExecuteReader (), созданная специально для одноэлементных запросов (наподобие получения количества записей) Prepare () Создает подготовленную (или скомпилированную) версию команды в источнике данных. Подготовленные запросы выполняются несколько быстрее, и их имеет смысл использовать, если требуется многократное выполнение одного и того же запроса (обычно каждый раз с различными параметрами)
Глава 21. ADO.NET, часть I: подключенный уровень 783 Работа с объектами чтения данных После установки подключения и создания объекта команды можно отправлять запросы к источнику данных. Это делается несколькими способами. Самым простым и быстрым способом получения информации из хранилища данных является тип DbDataReader (реализующий интерфейс IDataReader). Вспомните, что объекты чтения данных представляют поток данных, допускающий только чтение в прямом направлении, и возвращают каждый раз по одной записи. Поэтому объекты чтения данных применяются только для выдачи SQL-запросов на выборку информации из хранилища данных. Объекты чтения данных удобны, если нужно быстро просмотреть большой объем данных без необходимости представлять их в памяти. Например, если запросить из таблицы 20 000 записей, чтобы сохранить их в текстовом файле, то хранение этой информации BDataSet будет излишней затратой памяти (ведь DataSet полностью хранит результат запроса в памяти). Гораздо лучше создать объект чтения данных, который будет максимально быстро выдавать по одной записи. Учтите, что объекты чтения данных (в отличие от объектов адаптеров данных, которые мы рассмотрим ниже) поддерживают открытое подключение к источнику данных, пока сеанс не будет явно закрыт. Объект чтения данных можно получить из объекта команды с помощью вызова ExecuteReader(). Объект чтения данных представляет текущую запись, прочитанную из базы данных. В нем имеется метод индексатора (например, синтаксис [] в С#), который обеспечивает доступ к столбцам текущей записи. Доступ к конкретному столбцу возможен либо по имени, либо по целочисленному индексу, начиная с нуля. В приведенном ниже примере использования объекта чтения данных применяется метод Read() — чтобы определить, когда достигнут конец данных (при этом он возвращает значение false). Для каждой прочитанной из базы данных записи с помощью индексатора типа выводится модель, дружественное имя и цвет каждого автомобиля. Обратите внимание, что сразу после завершения обработки записей вызывается метод Close(), чтобы освободить объект подключения: static void Main(string[] args) { // Получение объекта чтения данных с помощью ExecuteReader () . using (SqlDataReader myDataReader = myCommand.ExecuteReader()) { // Цикл по результатам. while (myDataReader.Read()) { Console.WriteLine ("-> Make: {0}, PetName: {1}, Color: {2}.", myDataReader["Make"] .ToString () .Trim(), myDataReader["PetName"].ToString().Trim(), myDataReader["Color"] .ToString () .Trim()); } } Console.PeadLine(); } В этом фрагменте индексатор объекта чтения данных перегружен, чтобы он мог принимать либо string (имя столбца), либо int (порядковый номер столбца). Это позволяет прояснить логику объекта чтения (и избежать применения жестко закодированных строковых имен) с помощью следующего изменения (обратите внимание на использование свойства FieldCount):
784 Часть V. Введение в библиотеки базовых классов .NET while (myDataReader.Read()) { Console.WriteLine ("***** Запись *****"); for (int i = 0; i < myDataReader.FieldCount; i++) { Console.WriteLine ("{0} = {1} ", myDataReader.GetName(l), myDataReader.GetValue(i) .ToString () .Trim()); } Console.WriteLine(); } Если скомпилировать и запустить этот проект, то будет выдан список всех автомобилей из таблицы Inventory базы данных AutoLot. Следующие выходные данные содержат несколько первых записей из моей версии AutoLot: ***** Работа с объектами чтения данных ***** ***** Информация о подключении ***** Местоположение : (local)\SQLEXPRESS Имя базы данных: AutoLot Тайм-аут: 30 Состояние подключения: Open ***** Запись ***** CarlD = 83 Make = Ford Color = Rust PetName = Rusty ***** запись ***** CarlD = 107 Make = Ford Color = Red PetName = Snake Получение множественных результатов с помощью объекта чтения данных Объекты чтения данных могут получать множественные результирующие наборы с помощью одного объекта команды. Например, если нужно получить все строки из таблицы Inventory, а также все строки из таблицы Customers, то можно указать сразу оба оператора SQL, разделив их точкой с запятой: string strSQL = "Select * From Inventory;Select * from Customers"; После получения объекта чтения данных можно просмотреть все записи результирующего набора с помощью метода NextResult(). Учтите, что автоматически возвращается всегда первый результирующий набор. И если требуется просмотреть все строки каждой таблицы, нужно будет создать следующую конструкцию перебора: do { while (myDataReader.Read()) { Console.WriteLine (••***** Запись *****"); for (int i=0; l < myDataReader.FieldCount; i++) { Console.WriteLine ("{0} = {1}", myDataReader.GetName(l), myDataReader.GetValue(i) .ToString ()); } Console.WriteLine(); } } while (myDataReader.NextResult());
Глава 21. ADO.NET, часть I: подключенный уровень 785 Теперь вы лучше ознакомились с возможностями, которые имеют таблицы благодаря объектам чтения данных. Не забывайте, что объект чтения данных может обрабатывать только SQL-операторы Select и не может использоваться для изменения существующей таблицы базы данных с помощью запросов Insert, Update или Delete. Чтобы понять, как изменять существующую базу данных, потребуется дальнейшее изучение объектов команд. Исходный код. Проект AutoLotDataReader доступен в подкаталоге Chapter 21. Создание повторно используемой библиотеки доступа к данным Метод ExecuteReader () извлекает объект чтения данных, который позволяет просматривать результаты SQL-оператора Select с помощью потока информации, доступного только для чтения в прямом направлении. Однако если требуется выполнить операторы SQL, модифицирующие таблицу данных, то нужен вызов метода ExecuteNonQueryO данного объекта команды. Этот единый метод предназначен для выполнения вставок, изменений и удалений, в зависимости от формата текста команды. На заметку! Понятие не запросный (nonquery) означает оператор SQL, который не возвращает результирующий набор. Следовательно, операторы Select представляют собой запросы, а операторы Insert, Update и Delete — нет. Соответственно, метод ExecuteNonQueryO возвращает значение int, содержащее количество строк, на которые повлияли эти операторы, а не новое множество записей. Чтобы показать, как модифицировать содержимое существующей базы данных с помощью только запроса ExecuteNonQueryO, следующим шагом будет создание собственной библиотеки доступа к данным, в которой инкапсулируется процесс работы с базой данных AutoLot. В реальной производственной среде ваша логика ADO.NET почти наверняка будет изолирована в .dll-сборке .NET по одной простой причине — повторное использование кода! В первых примерах данной главы это не было сделано, чтобы не отвлекать вас от решаемых задач. Но было бы лишними затратами времени разрабатывать ту же самую логику подключения, ту же самую логику чтения данных и ту же самую логику выполнения команд для каждого приложения, которому понадобится работать с базой данных AutoLot. В результате изоляции логики доступа к данным в кодовой библиотеке .NET различные приложения с любыми пользовательскими интерфейсами (консольный, в стиле рабочего стола, в веб-стиле и т.д.) могут обращаться к существующей библиотеке даже независимо от языка. И если разработать библиотеку доступа к данным на С#, то другие программисты в .NET смогут создавать свои пользовательские интерфейсы на любом языке (например, VB или C++/CLI). В данной главе наша библиотека доступа к данным (AutoLot DAL. dl 1) будет содержать единое пространство имен (AutoLotConnectedLayer), которое будет взаимодействовать с базой AutoLot с помощью подключенных типов ADO.NET. В следующей главе в эту же библиотеку будет добавлено новое пространство имен (AutoLotDisconnectionLayer), которое будет содержать типы для взаимодействия с базой AutoLot с помощью автономного уровня. После этого данная библиотека будет применяться в различных приложениях, которые будут разработаны в последующем тексте.
786 Часть V. Введение в библиотеки базовых классов .NET Начните с создания нового проекта библиотеки классов (С# Class Library) по имени AutoLotDAL (сокращенно от 'Au to Lot Data Access Layer" — "Уровень доступа к данным AutoLot"), а затем смените первоначальное имя файла С#-кода на AutoLotConnDAL.cs. Потом переименуйте область действия пространства имен в AutoLotConnectedLayer и измените имя первоначального класса на InventoryDAL, т.к. этот класс будет определять различные члены, предназначенные для взаимодействия с таблицей Inventory базы данных AutoLot. И, наконец, импортируйте следующие пространства имен .NET: using System; using System.Collections.Generic; using System.Text; //Мы будем применять поставщик SQL Server; //но для большей гибкости можно воспользоваться //и генератором поставщиков ADO.NET. using System.Data; using System.Data.SqlClient; namespace AutoLotConnectedLayer { public class InventoryDAL { } } На заметку! Вспомните, о чем говорилось в главе 8: когда в объектах используются типы, управляющие "сырыми" ресурсами (наподобие подключения к базе данных), рекомендуется реализовать интерфейс IDisposable и написать нужный финализатор В производственной среде то же самое делают классы, подобные InventoryDAL, но здесь мы это делать не будем, чтобы не отвлекаться от деталей ADO.NET. Добавление логики подключения Первая наша задача — определить методы, позволяющие вызывающему процессу подключаться к источнику данных с помощью допустимой строки подключения и отключаться от него. Поскольку в нашей сборке AutoLotDAL.dll будет жестко закодировано использование типов класса System.Data.SqlClient, определите приватную переменную SqlConnection, которая будет выделяться при создании объекта InventoryDAL. Кроме того, определите метод OpenConnectionO, а затем еще CloseConnectionO, которые будут взаимодействовать с этой переменной: public class InventoryDAL { // Этот член будет использоваться всеми методами. private SqlConnection sqlCn = null; public void OpenConnection(string connectionString) { sqlCn = new SqlConnection () ; sqlCn.ConnectionString = connectionString; sqlCn.Open (); } public void CloseConnectionO { sqlCn.Close (); } }
Глава 21. ADO.NET, часть I: подключенный уровень 787 Для краткости тип InventoryDAL не будет проверять все возможные исключения, и не будет генерировать пользовательские исключения при возникновении различных ситуаций (например, когда строка подключения неверно сформирована). Однако при создании производственной библиотеки доступа к данным вам наверняка пришлось бы задействовать технику структурированной обработки исключений, чтобы учитывать все аномалии, которые могут возникнуть во время выполнения. Добавление логики вставки Вставка новой записи в таблицу Inventory сводится к форматированию SQL- оператора Insert (в зависимости от введенных пользователем данных) и вызову метода ExecuteNonQueryO с помощью объекта команды. Для этого добавьте в класс InventoryDAL общедоступный метод InsertAuto(), принимающий четыре параметра, которые соответствуют четырем столбцам таблицы Inventory (CarlD, Color, Make и PetName). На основании этих аргументов сформируйте строку для добавления новой записи. И, наконец, выполните SQL-оператор с помощью объекта SqlConnection: public void InsertAuto(int id, string color, string make, string petName) { // Формирование и выполнение оператора SQL. string sql = string.Format("Insert Into Inventory" + " (CarlD, Make, Color, PetName) Values" + " ('{0}', '{1}'/ 42}', ' {3}')", id, make, color, petName); // Выполнение с помощью нашего подключения. using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn)) { cmd.ExecuteNonQuery() ; } } Синтаксически этот метод вполне корректен, но можно предложить перегруженную версию, которая позволяет вызывающему методу передать объект строго типизированного класса, представляющий данные для новой строки. Для этого определите новый класс NewCar, который соответствует новой строке в таблице Inventory: public class NewCar ( public int CarlD { get; set; } public string Color { get; set; } public string Make { get; set; } public string PetName { get; set; } } Теперь добавьте в класс InventoryDAL следующий вариант InsertAuto (): public void InsertAuto(NewCar car) { // Формирование и выполнение оператора SQL. string sql = string.Format("Insert Into Inventory" + "(CarlD, Make, Color, PetName) Values" + "(•{0}', '{1}'/ '{2}', '{3}')", car.CarlD, car.Make, car.Color, car.PetName); // Выполнение с помощью нашего подключения. using (SqlCommand cmd = new SqlCommand(sql, this.sqlCn)) { cmd.ExecuteNonQuery(); } }
788 Часть V. Введение в библиотеки базовых классов .NET Определение классов, представляющих записи в реляционной базе данных — распространенный способ создания библиотеки доступа к данным. Вообще-то, как будет сказано в главе 23, ADO.NET Entity Framework автоматически генерирует строго типизированные классы, которые позволяют взаимодействовать с данными базы. Кстати, автономный уровень ADO.NET (см. главу 22) генерирует строго типизированные объекты DataSet для представления данных из заданной таблицы в реляционной базе данных. На заметку! Создание оператора SQL с помощью конкатенации строк может оказаться опасным с точки зрения безопасности (вспомните атаки вставкой в SQL). Текст команды лучше создавать с помощью параметризованного запроса, который будет описан чуть ниже. Добавление логики удаления Удаление существующей записи не сложнее вставки новой записи. В отличие от кода Insert Auto (), будет показана одна важная область try/catch, которая обрабатывает возможную ситуацию, когда выполняется попытка удаления автомобиля, уже заказанного кем-то из таблицы Customers. Добавьте в класс InventoryDAL следующий метод: public void DeleteCar(int id) { // Получение идентификатора автомобиля перед его удалением. string sql = string.Format("Delete from Inventory where CarlD = ' { 0}'", id); using (SqlCommand cmgl = new SqlCommand (sql, this.sqlCn)) { try { cmd.ExecuteNonQuery() ; } catch(SqlException ex) { Exception error = new Exception("К сожалению, эта машина заказана!", ex); throw error; } } } Добавление логики изменения Когда дело доходит до обновления существующей записи в таблице Inventory, то сразу же возникает очевидный вопрос: что именно можно позволить изменять вызывающему процессу: цвет автомобиля, дружественное имя, модель или все сразу? Один из способов максимального повышения гибкости — определение метода, принимающего параметр типа string, который может содержать любой оператор SQL, но это, по меньшей мере, рискованно. В идеале лучше иметь набор методов, которые позволяют вызывающему процессу изменять записи различными способами. Однако для нашей простой библиотеки доступа к данным мы определим единый метод, который позволяет вызывающему процессу изменить дружественное имя указанного автомобиля: public void UpdateCarPetName (int id, string newPetName) { // Получение идентификатора автомобиля и нового дружественного имени для него. string sql = string.Format("Update Inventory Set PetName = '{O}1 Where CarlD = ' {1} '", newPetName, id) ;
Глава 21. ADO.NET, часть I: подключенный уровень 789 using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn)) 1 cmd.ExecuteNonQuery() ; } } Добавление логики выборки Теперь необходимо добавить метод для выборки записей. Как было показано ранее в этой главе, объект чтения данных конкретного поставщика данных позволяет выбирать записи с помощью курсора, допускающего только чтение в прямом направлении. Посредством вызова метода Read() можно обработать каждую запись поочередно. Все это замечательно, но теперь необходимо разобраться, как возвратить эти записи вызывающему уровню приложения. Одним из подходов может быть получение данных с помощью метода Read() с последующим заполнением и возвратом многомерного массива (или другого объекта вроде обобщенного List<T>). Вот другой подход получения данных из таблицы Inventory: public List<NewCar> GetAllInventoryAsList () { // Здесь будут находиться записи. List<NewCar> inv = new List<NewCar> (); // Подготовка объекта команды. string sql = "Select * From Inventory"; using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn)) { SqlDataReader dr = cmd.ExecuteReader(); while (dr.Read()) { inv.Add(new NewCar { CarlD = (int)dr["CarID"], Color = (string)dr["Color"], Make = (string)dr["Make"], PetName = (string)dr["PetName"] }); } dr.Close () ; } return inv; } Еще один способ — возврат объекта System.Data.DataTable, который вообще-то принадлежит автономному уровню ADO.NET. Полное описание автономного уровня будет приведено в следующей главе, а пока достаточно уяснить, что DataTable — это класс, представляющий табличный блок данных (наподобие бумажной или электронной таблицы). Класс DataTable содержит данные в виде коллекции строк и столбцов. Эти коллекции можно заполнять программным образом, но в типе DataTable имеется метод Load(), который может автоматически заполнять их с помощью объекта чтения данных! Вот пример, где данные из таблицы Inventory возвращаются в виде DataTable: public DataTable GetAllInventoryAsDataTable () { // Здесь будут находиться записи. DataTable inv = new DataTable ();
790 Часть V. Введение в библиотеки базовых классов .NET // Подготовка объекта команды. string sql = "Select * From Inventory"; using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn)) { SqlDataReader dr = cmd.ExecuteReader (); // Заполнение DataTable данными из объекта чтения и зачистка. inv.Load (dr); dr.Close(); } return inv; } Работа с параметризованными объектами команд Пока в логике вставки, изменения и удаления для типа Invent or у DAL мы использовали жестко закодированные строковые литералы для каждого SQL-запроса. Вы, видимо, знаете о существовании параметризованных запросов, которые позволяют рассматривать параметры SQL как объекты, а не просто кусок текста. Работа с SQL- запросами в более объектно-ориентированной манере не только помогает сократить количество опечаток (при наличии строго типизированных свойств), ведь параметризованные запросы обычно выполняются значительно быстрее запросов в виде строковых литералов, поскольку они анализируются только один раз (а не каждый раз, как это происходит, если свойству CommandText присваивается SQL-строка). Кроме того, параметризованные запросы защищают от атак внедрением в SQL (широко известная проблема безопасности доступа к данным). Для поддержки параметризованных запросов объекты команд ADO.NET поддерживают коллекцию отдельных объектов параметров. По умолчанию эта коллекция пуста, но в нее можно занести любое количество объектов параметров, которые соответствуют параметрам-заполнителям (placeholder parameter) в SQL-запросе. Если нужно связать параметр SQL-запроса с членом коллекции параметров некоторого объекта команды, поставьте перед параметром SQL символ @ (по крайней мере, при работе с Microsoft SQL Server, хотя не все СУБД поддерживают это обозначение). Задание параметров с помощью типа DbParameter Прежде чем приступить к созданию параметризованных запросов, ознакомимся с типом DbParameter (базовый класс для объектов параметров поставщиков). У этого класса есть ряд свойств, которые позволяют задать имя, размер и тип параметра, а также другие характеристики, например, направление просмотра параметра. Некоторые важные свойства типа DbParameter приведены в табл. 21.7. Таблица 21.7. Основные члены типа DbParameter Свойство Назначение DbType Выдает или устанавливает тип данных из параметра, представляемый в виде типа CLR Direction Выдает или устанавливает вид параметра: только для ввода, только для вывода, для ввода и для вывода или параметр для возврата значения IsNullable Выдает или устанавливает, может ли параметр принимать пустые значения ParameterName Выдает или устанавливает имя DbParameter Size Выдает или устанавливает максимальный размер данных для параметра (полезно только для текстовых данных) Value Выдает или устанавливает значение параметра
Глава 21. ADO.NET, часть I: подключенный уровень 791 Для демонстрации заполнения коллекции объектов команд совместимыми с DBParameter объектами переделаем метод InsertAutoO так, что он будет использовать объекты параметров (аналогично можно переделать и все остальные методы, но нам будет достаточно и настоящего примера): public void InsertAuto(int id, string color, string make, string petName) { // "Заполнители" в SQL-запросе. string sql = string.Format("Insert Into Inventory" + 11 (CarlD, Make, Color, PetName) Values" + "(@CarID, @Make, @Color, @PetName)"); // У этой команды будут внутренние параметры. using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn)) { // Заполнение коллекции параметров. SqlParameter param = new SqlParameter(); param.ParameterName = "@CarID"; param.Value = id; param.SqlDbType = SqlDbType.Int; cmd.Parameters.Add(param); param = new SqlParameter () ; param.ParameterName = "@Make"; param. Value = make; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param); param = new SqlParameter () ; param.ParameterName = "@Color"; param.Value = color; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param); param = new SqlParameter () ; param.ParameterName = "@PetName"; param.Value = petName; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param); cmd.ExecuteNonQuery(); } Обратите внимание, что здесь SQL-запрос также содержит четыре символа-заполнителя, перед каждым из которых находится символ @. С помощью свойства ParameterName в типе SqlParameter можно описать каждый из этих заполнителей и задать различную информацию (значение, тип данных, размер и т.д.), причем строго типизированным образом. После подготовки всех объектов параметров они добавляются в коллекцию объекта команды с помощью вызова Add(). На заметку! Для оформления объектов параметров здесь используются различные свойства. Однако учтите, что объекты параметров поддерживают ряд перегруженных конструкторов, которые позволяют задавать значения различных свойств (что дает более компактную кодовую базу). Учтите также, что в Visual Studio 2010 имеются различные графические конструкторы, которые автоматически создадут за вас большой объем этого утомительного кода работы с параметрами (см. главы 22 и 23).
792 Часть V. Введение в библиотеки базовых классов .NET Создание параметризованного запроса часто приводит к большему объему кода, но в результате получается более удобный способ для программной настройки SQL- операторов, а также более высокая производительность. Эту технику можно применять для любых SQL-запросов, хотя параметризованные запросы наиболее удобны, если нужно запускать хранимые процедуры. Выполнение хранимой процедуры Вспомните, что хранимая процедура (stored procedure) — это именованный блок SQL-кода, хранимый в базе данных. Хранимые процедуры можно создавать для возврата набора строк или скалярных типов данных, а также выполнения других нужных действий (например, вставки, обновления или удаления); они могут принимать любое количество необязательных параметров. В результате получается рабочая единица, которая ведет себя как типичная функция, только она находится в хранилище данных, а не в двоичном бизнес-объекте. На данном этапе в базе данных AutoLot определена одна хранимая процедура с именем GetPetName, имеющая следующий формат: GetPetName @carID int, @petName char A0) output AS SELECT @petName = PetName from Inventory where CarlD = @carID Теперь рассмотрим следующий финальный метод типа InventoryDAL, который вызывает эту хранимую процедуру: public string LookUpPetName(int carlD) { string carPetName = string.Empty; // Задание имени хранимой процедуры. using (SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn)) { cmd.CommandType = CommandType.StoredProcedure; // Входной параметр. SqlParameter param = new SqlParameter(); param.ParameterName = "@carID"; param.SqlDbType = SqlDbType.Int; param.Value = carlD; param.Direction = ParameterDirection.Input; cmd.Parameters.Add(param); //По умолчанию параметры считаются входными, но все же для ясности: param.Direction = ParameterDirection.Input; cmd.Parameters.Add(param); // Выходной параметр. param = new SqlParameter () ; param.ParameterName = "@petName"; param.SqlDbType = SqlDbType.Char; param.Size = 10; param.Direction = ParameterDirection.Output; cmd.Parameters.Add(param); // Выполнение хранимой процедуры. cmd.ExecuteNonQuery(); // Возврат выходного параметра. carPetName = ((string)cmd.Parameters["@petName"].Value).Trim(); } return carPetName;
Глава 21. ADO.NET, часть I: подключенный уровень 793 Один важный аспект, касающийся вызова хранимых процедур: вспомните, что объект команды может представлять оператор SQL (по умолчанию) или имя хранимой процедуры. Если необходимо сообщить объекту команды, что он должен вызывать хранимую процедуру, то нужно передать имя этой процедуры (через аргумент конструктора или с помощью свойства CommandText) и установить в свойстве CommandType значение CommandType.StoredProcedure (иначе вы получите исключение времени выполнения, т.к. по умолчанию объект команды ожидает оператор SQL): SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn); cmd.CommandType = CommandType.StoredProcedure; Далее обратите внимание, что свойство Direction объекта параметра позволяет указать направление действия каждого параметра, передаваемого хранимой процедуре (например, входной параметр, выходной параметр, входной/выходной параметр или возвращаемое значение). Как и ранее, все объекты параметров добавляются в коллекцию параметров для данного объекта команды: // Входной параметр. SqlParameter param = new SqlParameter(); param.ParameterName = "QcarlD"; f param.SqlDbType = SqlDbType.Int; param.Value = carlD; param.Direction = ParameterDirection.Input; cmd.Parameters.Add(param); После завершения работы хранимой процедуры с помощью вызова ExecuteNonQuery () значение выходного параметра можно получить с помощью просмотра коллекции параметров для данного объекта команды и соответствующего приведения типа: // Возврат выходного параметра. carPetName = (string)cmd.Parameters["QpetName"].Value; И вот первый вариант библиотеки доступа к данным AutoLotDAL.dll готов! С помощью этой сборки можно создавать произвольные интерфейсы для вывода и редактирования данных (консольные приложения, Windows-приложения, приложения на основе Windows Presentation Foundation или веб-приложения на базе HTML). Вы еще не умеете создавать графические пользовательские интерфейсы, так что придется протестировать полученную библиотеку из нового консольного приложения. Исходный код. Проект AutoLotDAL доступен в подкаталоге Chapter 21. Создание консольного пользовательского интерфейса Создайте новое консольное приложение по имени AutoLotCUIClient. После создания нового проекта не забудьте добавить в него ссылку на сборку AutoLotDAL.dll, а также на System.Configuration.dll, и добавьте в код С# следующие операторы using: using AutoLotConnectedLayer; using System.Configuration; using System.Data;
794 Часть V. Введение в библиотеки базовых классов .NET После этого вставьте в проект новый файл App.config, содержащий элемент <соп- nectionString>, который нужен для подключения к вашему экземпляру базы данных AutoLot, например: <configuration> <connectionStrings> <add name ="AutoLotSglProvider11 connectionString = "Data Source=(local)\SQLEXPRESS;" + "Integrated Security=SSPI;Initial Catalog=AutoLot"/> </connectionStrings> </configuration> Реализация метода MainQ Метод Main () будет отвечать за подсказку пользователю его возможных действий и выполнение запросов с помощью оператора switch. Эта программа позволит пользователю вводить следующие команды: • I — вставка новой записи в таблицу Inventory; • U — изменение существующей записи в таблице Inventory; • D — удаление существующей записи в таблице Inventory; • L — вывод имеющихся в наличии автомобилей с помощью объекта чтения данных; • S — вывод списка возможных команд; • Р — вывод дружественного имени автомобиля по его идентификатору; • Q — завершение работы программы. Каждая из этих команд выполняется специальных статическим методом из класса Program. Ниже приведена полная реализация метода Main(). Все методы, вызываемые в цикле do while (за исключением метода ShowInstructionsO), принимают в качестве единственного параметра объект InventoryDAL. static void Main(string[] args) { Console.WriteLine ("***** Консольный пользовательский интерфейс AutoLot *****\n"); // Получение строки подключения из App.config. string cnStr = ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString; bool userDone = false; string userCommand = ""; // Создание объекта InventoryDAL. InventoryDAL invDAL = new InventoryDAL(); invDAL.OpenConnection(cnStr) ; // Запросы пользовательских команд, пока не будет нажата клавиша <Q>. try { Showlnstructions (); do { Console.Write("ХпВведите команду: " ) ; userCommand = Console.ReadLine(); Console.WriteLine(); switch (userCommand.ToUpper()) {
Глава 21. ADO.NET, часть I: подключенный уровень 795 case "I": InsertNewCar(invDAL); break; case "U": UpdateCarPetName(invDAL); break; case "D": DeleteCar(invDAL); break; case "L": Listlnventory(invDAL) ; break; case "S": Showlnstructions (); break; case "P": LookUpPetName(invDAL); break; case "Q": userDone = true; breaks- default : Console.WriteLine("Неверно' Попробуйте еще раз"); break; } } while (luserDone); catch (Exception ex) Console.WriteLine (ex.Message); finally invDAL.CloseConnection(); } Реализация метода Showlnstructions () Метод Showlnstructions () делает то, что понятно из его названия ("Вывод инструкций"): private static void Showlnstructions () { Console.WriteLine ("I: Добавление нового автомобиля."); Console.WriteLine ("U: Изменение существующего автомобиля."); Console.WriteLine("D: Удаление существующего автомобиля."); Console.WriteLine ("L: Вывод автомобилей в наличии."); Console.WriteLine ("S : Вывод этих инструкций."); Console.WriteLine("Р: Вывод дружественного имени автомобиля."); Console.WriteLine("Q: Завершение работы."); } Реализация метода ListlnventoryO Метод ListlnventoryO ("Список наличия") можно реализовать двумя способами, в зависимости от того, как была создана библиотека доступа к данным. Вспомните, что метод GetAllInventoryAsDataTableO из класса InventoryDAL возвращает объект DataTable. Этот подход можно реализовать так:
796 Часть V. Введение в библиотеки базовых классов .NET private static void Listlnventory(InventoryDAL invDAL) { // Вывод автомобилей в наличии. DataTable dt = invDAL.GetAllInventory(); // Передача DataTable вспомогательной функции для вывода. DisplayTable (dt); } Вспомогательный метод DisplayTable () выводит данные таблицы, используя свойства Rows и Columns входного параметра DataTable (подробно DataTable будет рассмотрен в следующей главе, а пока не обращайте внимания на детали): private static void DisplayTable(DataTable dt) { // Вывод имен столбцов. for (int curCol = 0; curCol < dt.Columns.Count; curCol++) { Console.Write(dt.Columns[curCol].ColumnName.Trim() + "\t"); } Console .WriteLine (" \n ") ; // Вывод содержимого DataTable. for (int curRow = 0; curRow < dt.Rows.Count; curRow++) { for (int curCol = 0; curCol < dt.Columns.Count; curCol++) { Console.Write(dt.Rows[curRow] [curCol] .ToString () .Trim() + "\t"); } Console.WriteLine(); } } А если вам больше нравится метод Get AllInventoryAsList () из класса InventoryDAL, то можно реализовать метод ListInventoryViaList(): private static void ListlnventoryViaList(InventoryDAL invDAL) { // Получение списка содержимого. List<NewCar> record = invDAL.GetAllInventoryAsList (); foreach (NewCar с in record) { Console.WriteLine ("CarlD: {0}, Make: {1}, Color: {2}, PetName: {3}", c.CarlD, c.Make, c.Color, c.PetName); Реализация метода DeleteCar () Удаление существующего автомобиля сводится к запросу у пользователя идентификатора этого автомобиля, с последующим обращением к таблице данных и передачей этого идентификатора методу DeleteCar () типа InventoryDAL: private static void DeleteCar(InventoryDAL invDAL) { // Получение идентификатора удаляемого автомобиля. Console.Write("Введите ID удаляемого автомобиля: " ) ; int id = int.Parse(Console.ReadLine()); //На случай нарушения ограничений первичного ключа!
Глава 21. ADO.NET, часть I: подключенный уровень 797 try { invDAL.DeleteCar(id); } catch(Exception ex) { Console.WriteLine(ex.Message); } } Реализация метода InsertNewCar() Для вставки новой записи в таблицу Inventory нужно запросить у пользователя информацию о новом автомобиле (с помощью вызовов Console.ReadLineO) и передать эти данные в метод Insert Auto() библиотеки Inventor у DAL: private static void InsertNewCar(InventoryDAL invDAL) { // Получение данных от пользователя. int newCarlD; string newCarColor, newCarMake, newCarPetName; Console.Write("Введите ID автомобиля: ") ; newCarlD = int.Parse (Console.ReadLine()); Console.Write("Введите цвет автомобиля: "); newCarColor = Console.ReadLine(); Console.Write("Введите модели автомобиля: "); newCarMake = Console.ReadLine(); Console.Write("Введите дружественного имени: ") ; newCarPetName = Console.ReadLine (); // Передача информации в библиотеку доступа к данным. invDAL.InsertAuto (newCarlD, newCarColor, newCarMake, newCarPetName); } Вспомните, что у нас имеется перегруженный вариант InsertAuto (), который принимает новый объект NewCar, а не набор независимых аргументов. Он позволяет реализовать метод InsertNewCar() так: private static void InsertNewCar(InventoryDAL invDAL) { // Получение данных от пользователя. // Передача информации в библиотеку доступа к данным. NewCar с = new NewCar { CarlD = newCarlD, Color = newCarColor, Make = newCarMake, PetName = newCarPetName }; invDAL.InsertAuto(c); } Реализация метода UpdateCarPetNameQ Реализация метода UpdateCarPetNameO выглядит аналогично: private static void UpdateCarPetName(InventoryDAL invDAL) { // Получение данных от пользователя. int carlD; string newCarPetName; Console.Write("Введите ID автомобиля: "); car ID = int.Parse(Console.ReadLine());
798 Часть V. Введение в библиотеки базовых классов .NET Console.Write ("Введите новое дружественное имя: "); newCarPetName = Console.ReadLine(); // Передача информации в библиотеку доступа к данным. invDAL.UpdateCarPetName(carlD, newCarPetName); Реализация метода LookUpPetName() Получение дружественного имени указанного автомобиля также мало чем отличается от предыдущих методов, поскольку все низкоуровневые вызовы ADO.NET инкапсулированы в библиотеке доступа к данным: private static void LookUpPetName(InventoryDAL invDAL) { // Получение идентификатора автомобиля. Console.Write("Введите ID автомобиля: ") ; int id = int.Parse(Console.ReadLine()); Console.WriteLine("Имя {0} - {1}.", id, invDAL.LookUpPetName(id)); } На этом консольный интерфейс завершен. Можно запустить полученную программу и проверить работу каждого метода. Вот часть выходных данных, где проверяются команды L, Р и Q: ***** Консольный пользовательский интерфейс AutoLot ***** I: Добавление нового автомобиля. U: Изменение существующего автомобиля. D: Удаление существующего автомобиля. L: Вывод автомобилей в наличии. S: Вывод этих инструкций. Р: Вывод дружественного имени автомобиля. Q: Завершение работы. Введите команду: L CarlD 83 107 678 904 1000 1001 Make Ford Ford Yugo VW BMW BMW Color Rust Red Green Black Black Tan PetName Rusty Snake Clunker Hank Bimmer Daisy 1992 Saab Pink Pinkey Введите команду: Р Введите ID автомобиля: 904 Имя 904 - Hank. Введите команду: Q Press any key to continue . . Исходный код. Проект AutoLotCUIClient доступен в подкаталоге Chapter 21.
Глава 21. ADO.NET, часть I: подключенный уровень 799 Транзакции баз данных И в завершение нашего изучения подключенного уровня ADO.NET рассмотрим концепцию транзакций базы данных. Попросту говоря, транзакция — это набор операций в базе данных, которые должны быть либо все выполнены, либо все не выполнены. Транзакции применяются для обеспечения безопасности, верности и непротиворечивости данных в таблице. Транзакции очень важны тогда, когда при работе с базой данных требуется взаимодействие с несколькими таблицами или несколькими хранимыми процедурами (или с сочетанием неделимых объектов базы данных). Классический пример транзакции — процесс перевода денежных средств с одного банковского счета на другой. Например, если вам понадобилось перевести $500 с депозитного счета на текущий счет, то нужно выполнить в режиме транзакции следующие шаги: • банк должен снять $500 с вашего депозитного счета; • затем банк должен добавить $500 на ваш текущий счет. Вряд ли вам бы понравилось, если бы деньги были сняты с депозитного счета, но не переведены (из-за какой-то банковской ошибки) на текущий счет. Но если эти шаги упаковать в транзакцию базы данных, то СУБД гарантирует, что все взаимосвязанные шаги будут выполнены как единое целое. Если любая часть транзакции выполнится неудачно, то будет произведен откат (rollback) всей транзакции в исходное состояние. А если все шаги будут выполнены успешно, то транзакция будет зафиксирована (committed). На заметку! Если вы уже читали о транзакциях, то, возможно, вам встречалось сокращение ACID. Оно означает четыре ключевых свойства классической транзакции: атомарность (Atomic — все или ничего), целостность (Consistent — на протяжении транзакции данные остаются в непротиворечивом состоянии), изолированность (Isolated — транзакции не мешают одна другой) и устойчивость (Durable — транзакции сохраняются и протоколируются). Оказывается, в платформе .NET есть несколько способов поддержки транзакций. В данной главе мы рассмотрим объект транзакции для поставщика данных ADO.NET (SqlTransaction в случае System.Data.SqlClient). Библиотеки базовых классов ADO.NET также обеспечивают поддержку транзакций в многочисленных API- интерфейсах, которые перечислены ниже. • System.EnterpriseServices. Это пространство имен (из сборки System. EnterpriseServices.dll) содержит типы, позволяющие интеграцию с уровнем времени выполнения СОМ+, в том числе и поддержку распределенных транзакций. • System.Transactions. Это пространство имен (из сборки System.Transactions.dll) содержит классы, позволяющие писать собственные транзакционные приложения и диспетчеры ресурсов для различных служб (MSMQ, ADO.NET, СОМ+ и т.д.). • Windows Communication Foundation. WCF API предоставляет службы для работы с транзакциями с различными классами распределенного связывания. • Windows Workflow Foundations. WF API предоставляет транзакционную поддержку для рабочих потоков. Кроме встроенной поддержки транзакций в библиотеках базовых классов .NET, можно пользоваться и возможностями языка SQL используемой СУБД. Например, можно написать хранимую процедуру, в которой задействованы операторы BEGIN TRANSACTION, ROLLBACK и COMMIT.
800 Часть V. Введение в библиотеки базовых классов .NET Основные члены объекта транзакции ADO.NET Типы для работы с транзакциями существуют во всех библиотеках базовых классов, но мы будем рассматривать объекты транзакции, которые имеются в поставщиках данных ADO.NET — все они порождены от DBTransaction и реализуют интерфейс IDbTransaction. Вспомните, в начале этой главы было сказано, что IDbTransaction определяет ряд членов: public interface IDbTransaction : IDisposable i IDbConnection Connection { get; } IsolationLevel IsolationLevel { get; } void Commit (); void Rollback(); \ Обратите внимание на свойство Connection, которое возвращает ссылку на объект подключения, инициировавший данную транзакцию (как мы увидим, объект транзакции можно получить от данного объекта подключения). Метод Commit () вызывается, если все операции в базе данных завершились успешно. При этом все ожидающие изменения фиксируются в хранилище данных. А метод Commit () можно вызвать при возникновении исключения времени выполнения, чтобы сообщить СУБД, что все ожидающие изменения следует отменить и оставить первоначальные данные без изменений. На заметку! Свойство IsolationLevel объекта транзакции позволяет указать степень защиты транзакции от действий параллельных транзакций. По умолчанию транзакции полностью изолируются до их фиксации. Полную информацию о значениях перечисления IsolationLevel можно найти в документации по .NET Framework 4.0 SDK. Кроме членов, определенных интерфейсом IDbTransaction, в типе SqlTransaction определен дополнительный член Save(), который предназначен для определения точек сохранения (save point). Эта концепция позволяет откатить неудачную транзакцию до указанной точки, не выполняя откат всей транзакции. При вызове метода Save() с помощью объекта SqlTransaction можно задать произвольный строковый псевдоним. А при вызове Rollback() можно указать этот псевдоним в качестве аргумента, чтобы выполнить частичный откат (partial rollback). При вызове Rollback() без аргументов будут отменены все ожидающие изменения. Добавление таблицы CreditRisks в базу данных AutoLot А теперь рассмотрим, как можно использовать транзакции в ADO.NET. Вначале откройте окно Server Explorer из Visual Studio 2010 и добавьте в базу данных AutoLot новую таблицу с именем CreditRisks, которая содержит точно такие же столбцы, что и таблица Customers, созданная ранее в этой главе: CustID (первичный ключ), FirstName и LastName. Эта таблица предназначена для отсеивания нежелательных клиентов с плохой кредитной историей. После добавления новой таблицы в диаграмму базы данных AutoLot ее реализация будет такой, как показано на рис. 21.14. Как и предыдущий пример с пересылкой денег с депозита на текущий счет, этот пример, где подозрительный клиент перемещается из таблицы Customers в таблицу CreditRisks, должен работать под недремлющим оком транзакционной области (ведь вы хотите запомнить идентификаторы и имена некредитоспособных клиентов). А именно, необходимо гарантировать, что либо будет выполнено успешное удаление текущих кредитных рисков из таблицы Customers с последующим их добавлением в таблицу CreditRisks, либо ни одна из этих операций не будет выполнена.
Глава 21. ADO.NET, часть I: подключенный уровень 801 dbo.CarDiagramFilc...qlexpr6S5.AutoLot)* X | Inventory FK_Orders_Inventory 9 CarlD' Make Color DotNsmp « m a ► Orders S OrderlD CustID CarlD FK_Orders_Customers CreditRisks S CustID FirstName LastName Customers 9 CustID FirstName LastName Ы J 2Щ Рис. 21.14. Взаимосвязанные таблицы Orders, Inventory и Customers На заметку! В производственной среде вам не понадобится создавать совершенно новую таблицу базы данных для подозрительных клиентов. Достаточно добавить в таблицу Customers логический столбец isCreditRisk. Эта новая таблица предназначена просто для опробования работы с простыми транзакциями. Добавление метода транзакции в InventoryDAL А теперь посмотрим, как работать с транзакциями ADO.NET программным образом. Откройте созданный ранее проект AutoLotDAL Code Library и добавьте в класс InventoryDAL новый общедоступный метод processCreditRisk(), предназначенный для работы с кредитными рисками (в данном примере для простоты не используется параметризованный запрос, но в производственном методе его следует задействовать): // Новый член класса InventoryDAL. public void ProcessCreditRisk(bool throwEx, int custID) { // Вначале выборка имени по идентификатору клиента. string fName = string.Empty; string IName = string.Empty; SqlCommand cmdSelect = new SqlCommand( string.Format("Select * from Customers where CustID using (SqlDataReader dr = cmdSelect.ExecuteReader()) { if (dr.HasRows) { dr. Read () ; fName = (string)dr["FirstName"] ; IName = (string)dr["LastName"] ; } else return; } custID), sqlCn);
802 Часть V. Введение в библиотеки базовых классов .NET // Создание объектов команд для каждого шага операции. SqlCommand cmdRemove = new SqlCommand( string.Format("Delete from Customers where CustID = {0}", custID), sqlCn) ; SqlCommand cmdlnsert = new SqlCommand(string.Format("Insert Into CreditRisks" + "(CustID, FirstName, LastName) Values" + "({0}, '{1}\ '{2}')", custID, fName, IName), sqlCn) ; // Получаем из объекта подключения. SqlTransaction tx = null; try { tx = sqlCn.BeginTransaction (); // Включение команд в транзакцию. cmdlnsert.Transaction = tx; cmdRemove.Transaction = tx; // Выполнение команд. cmdlnsert.ExecuteNonQuery(); cmdRemove.ExecuteNonQuery(); // Имитация ошибки. if (throwEx) { throw new ApplicationException ("Ошибка базы данных! Транзакция завершена неудачно."); } // Фиксация. tx.Commit(); } catch (Exception ex) { Console.WriteLine^ex.Message) ; // При возникновении любой ошибки выполняется откат транзакции. tx.Rollback(); } } Здесь используется входной параметр типа bool, который указывает, нужно ли генерировать произвольное исключение при попытке обработки нежелательного клиента. Это позволит имитировать непредвиденные ситуации, которые могут привести к неудачному завершению транзакции. Понятно, что здесь это сделано лишь в демонстрационных целях; в реальности метод транзакции не должен позволять вызывающему процессу разрушать всю логику по своему усмотрению! Мы используем два объекта SqlCommand, представляющие каждый шаг предстоящей транзакции. После получения имени и фамилии клиента по входному параметру custID с помощью метода BeginTransactionO объекта подключения получаем нужный объект SqlTransaction. Если этого не сделать, логика вставки/удаления не будет выполняться в транзакционном контексте. Если (и только если) значение логического параметра равно true, то после вызова ExecuteNonQuery () для обеих команд генерируется исключение. В этом случае все ожидающие подтверждения операции базы данных аннулируются. Если исключение не было сгенерировано, оба шага фиксируются в таблицах базы данных при вызове Commit (). Скомпилируйте измененный проект AutoLotDAL и проверьте, нет ли ошибок. Тестирование транзакции в нашей базе данных Можно было модифицировать существующее приложение AutoLotCUIClient, чтобы оно содержало метод ProcessCreditRisk(). Но мы для этой цели создадим новое кон-
Глава 21. ADO.NET, часть I: подключенный уровень 803 сольное приложение с именем AdoNetTransaction. Создайте в нем ссылку на сборку AutoLotDAL.dll и импортируйте пространство имен AutoLotConnectedLayer. После этого откройте таблицу Customers, щелкнув правой кнопкой мыши на значке таблицы в Server Explorer и выбрав в контекстном меню пункт Show Table Data (Просмотр данных таблицы). Создайте нового клиента с плохой кредитной историей, например: • CustID: 333 • FirstName: Homer • LastName: Simpson Теперь измените метод Main() следующим образом: static void Main(string [ ] arqs) { Console.WriteLine("***™ Простой пример работы с транзакциями *****\п"); // Простой способ разрешить или запретить успешное выполнение транзакции. bool throwEx = true; string userAnswer = string.Empty; Console.Write("Сгенерировать исключение? (Y или N) : "); userAnswer = Console.ReadLine (); if (userAnswer.ToLower() == "n") { throwEx = false; } InventoryDAL dal = new InventoryDAL(); dal.OpenConnection(@"Data Source=(local)\SQLEXPRESS;Integrated Security=SSPI;" + "Initial Catalog=AutcLDt"); // Обработка клиента 333. dal.ProcessCreditRisk(throwEx, 333); Console.WriteLine("Проверьте результаты в таблице CreditRisk"); Console.ReadLine(); } Если запустить программу и выбрать генерацию исключения, то клиент Homer не будет удален из таблицы Customers, т.к. вся транзакция будет отменена. Но при отсутствии исключения окажется, что клиент номер 333 находится уже не в таблице Customers, а в таблице CreditRisks. Исходный код. Проект AdoNetTransaction доступен в подкаталоге Chapter 21. Резюме ADO.NET — технология доступа к данным, непосредственно встроенная в платформу .NET. Ее можно использовать тремя различными способами: подключенным, автономным и с помощью Entity Framework. В данной главе был рассмотрен подключенный уровень, а также роль поставщиков данных, которые представляют собой конкретные реализации нескольких абстрактных базовых классов (в пространстве имен System. Data.Common) и типов интерфейсов (в пространстве имен System.Data). Вы увидели, как создавать кодовую базу, не зависящую от поставщика, с помощью модели генератора поставщиков данных ADO.NET. Вы также узнали, что объекты подключений, объекты транзакций, объекты команд и объекты чтения данных из подключенного уровня позволяют выбирать, изменять, вставлять и удалять записи. Не забывайте также, что объекты команд поддерживают внутреннюю коллекцию параметров, которые повышают безопасность SQL-запросов и весьма полезны для запуска хранимых процедур.
ГЛАВА 22 ADO.NET, часть II: автономный уровень В предыдущей главе мы рассмотрели подключенный уровень ADO.NET, который позволяет отправлять в базу данных запросы с помощью объектов подключения, объектов команд и объектов чтения данных соответствующего поставщика данных. А в данной главе вы познакомитесь с автономным уровнем (disconnected layer) ADO.NET. Этот аспект ADO.NET позволяет смоделировать в памяти данные из базы данных — конкретнее, в вызывающем уровне с помощью многочисленных членов пространства имен System.Data (в основном это DataSet, DataTable, DataRow, DataColumn, DataView и DataRelation). При этом возникает иллюзия, что вызывающий уровень постоянно подключен к внешнему источнику данных, хотя на самом деле все операции выполняются с локальной копией реляционных данных. Этот автономный аспект ADO.NET можно использовать даже без подключения к реляционной базе данных, но все-таки чаще всего заполненные объекты DataSet получают с помощью объекта адаптера данных конкретного поставщика данных. Как будет показано, объекты адаптеров данных выполняют связующую роль между клиентским уровнем и реляционной базой данных. С их помощью можно получить объекты DataSet, поработать с их содержимым и отправить измененные строки обратно для дальнейшей обработки. В результате получается широко масштабируемое .NET-приложение обработки данных. В данной главе будут также продемонстрированы некоторые приемы привязки к данным с помощью контекста графических приложений Windows Forms GUI и рассмотрена роль строго типизированного DataSet. Мы добавим в библиотеку AutoLotDAL.dll, созданную в главе 21, новое пространство имен, в котором используется автономный уровень ADO.NET. И в завершение мы рассмотрим технологию LINQ to DataSet, которая позволяет применять LINQ-запросы к находящемуся в памяти кэшу данных. На заметку! Технологии привязки к данным для приложений на основе Windows Presentation Foundation и ASP.NET будут описаны ниже в данной книге. Знакомство с автономным уровнем ADO.NET Как было показано в предыдущей главе, работа с подключенным уровнем позволяет взаимодействовать с базой данных с помощью первичных объектов подключения, команд и чтения данных. Этот небольшой набор типов позволяет выбирать, вставлять, изменять и удалять записи (а также вызывать хранимые процедуры или выполнять
Глава 22. ADO.NET, часть II: автономный уровень 805 другие операции над данными — например, операторы DDL для создания таблицы и DCL для назначения полномочий). Но вы увидели лишь половину ADO.NET, поскольку с помощью объектной модели ADO.NET можно работать и в автономном режиме. Автономные типы позволяют эмулировать реляционные данные с помощью модели объектов, находящихся в памяти. Кроме простого моделирования табличных данных, состоящих из строк и столбцов, типы из System.Data позволяют воспроизводить отношения между таблицами, ограничения столбцов, первичные ключи, представления и другие примитивы баз данных. К смоделированным данным можно применять фильтры, отправлять запросы и сохранять (или загружать) данные в формате XML и двоичном формате. И все это можно делать, даже не подключаясь к СУБД (откуда и термин "автономный уровень") — достаточно загрузить данные из локального XML-файла или программным образом создать объект DataSet. Автономные типы действительно можно использовать без подключения к базе данных, но все-таки обычно применяются подключения и объекты команд. Кроме того, используется и особый объект — адаптер данных (расширяющий абстрактный тип Db Data Adapter), который как раз поставляет и обновляет данные. Но в отличие от подключенного уровня, данные, полученные через адаптер данных, не обрабатываются с помощью объектов чтения данных. Вместо этого объекты адаптеров пересылают данные между вызывающим процессом и источником данных с помощью объектов DataSet. Тип DataSet представляет собой контейнер для любого количества объектов DataTable, каждый из которых содержит коллекцию объектов DataRow и DataColumn. Объект адаптера данных конкретного поставщика данных автоматически обслуживает подключение к базе данных. Для повышения масштабируемости адаптеры данных держат подключение открытым минимально возможное время. Как только вызывающий процесс получит объект DataSet, вызывающий уровень полностью отключается от базы данных и остается с локальной копией удаленных данных. Теперь в нем можно вставлять, удалять или изменять строки различных объектов DataTable, но физическая база данных не обновляется, пока вызывающий процесс явно не передаст DataSet адаптеру данных для обновления. По сути, объекты DataSet имитируют постоянное подключение клиентов, хотя на самом деле они работают с находящейся в памяти базой данных (рис. 22.1). Клиентское приложение Объект . DataSet ^ч*--—. —"^^ 4 ^ Адаптер данных w Р\ База данных Рис. 22.1. Объекты адаптеров данных пересылают информацию в клиентский уровень и из него Поскольку эпицентром автономного уровня является тип DataSet, то первоочередная задача данной главы — научиться вручную оперировать с ним. После этого у вас не будет проблем с обработкой содержимого DataSet, полученного от объекта адаптера данных. Роль объектов DataSet Как уже было сказано, объект DataSet является представлением реляционных данных, находящимся в памяти. Конкретнее, это класс, содержащий внутри себя три внутренних строго типизированных коллекции (рис, 22.2).
806 Часть V. Введение в библиотеки базовых классов .NET DataSet DataTableCollection DataRelationCollection PropertyCollection Рис. 22.2. Внутреннее устройство класса DataSet Свойство Tables класса DataSet предоставляет доступ к коллекции DataTableCollection, которая содержит отдельные объекты DataTable. В DataSet используется еще одна важная коллекция — DataRelationCollection. Поскольку DataSet является автономной версией схемы базы данных, его можно использовать для программного представления отношений родительский/дочерний между ее таблицами. Например, с помощью типа DataRelation можно создать отношение между двумя таблицами, имитирующее ограничение внешнего ключа. Затем с помощью свойства Relations этот объект можно добавить в коллекцию DataRelationCollection. Теперь при поиске данных можно перемещаться по взаимосвязанным таблицам. Как это сделать, будет рассказано далее в главе. Свойство ExtendedProperties предоставляет доступ к объекту PropertyCollect ion, который позволяет связать с DataSet любую дополнительную информацию в виде пар имя/значение. Эта информация может быть совершенно произвольной, даже не имеющей отношения к самим данным. К примеру, с каким-либо объектом DataSet можно связать название компании, которое будет играть роль находящихся в памяти метаданных. Другими примерами расширенных свойств могут служить временные метки, зашифрованный пароль, который необходим для доступа к содержимому DataSet, число, означающее частоту обновления данных, и многое другое. На заметку! Классы DataTable и DataColumn поддерживают также свойство ExtendedProperties. Основные свойства класса DataSet Прежде чем окунуться в разнообразные программные мелочи, рассмотрим некоторые основные члены класса DataSet. Кроме свойств Tables, Relations и ExtendedProperties, в табл. 22.1 приведено несколько дополнительных полезных свойств. Таблица 22.1. Свойства класса DataSet Свойство Назначение CaseSensitive DataSetName EnforceConstraints HasErrors RemotingFormat Указывает, чувствительны ли к регистру букв сравнения строк в объектах DataTable. По умолчанию равно false (сравнения строк выполняются без учета регистра букв) Задает понятное имя для данного DataSet. Обычно это значение передается через параметр конструктора Задает или получает значение, определяющее, применяются ли правила ограничений при выполнении любых обновлений (по умолчанию равно true) Получает значение, определяющее, имеются ли ошибки в любой строке любого из объектов DataTable данного DataSet Позволяет определить, как DataSet должен сериализовать свое содержимое (в виде двоичного файла или, по умолчанию, XML)
Глава 22. ADO.NET, часть II: автономный уровень 807 Основные методы класса DataSet Методы класса DataSet работают в сочетании с некоторыми функциями, которые обеспечивают описанные выше свойства. Кроме взаимодействия с потоками XML, DataSet содержит методы, позволяющие копировать содержимое DataSet, перемещаться между внутренними таблицами и устанавливать начальные и конечные точки пакетных обновлений. Некоторые основные методы перечислены в табл. 22.2. Таблица 22.2. Методы класса DataSet Метод Назначение AcceptChanges() С1еаг() Clone() СоруО GetChangesO HasChangesO Merge() ReadXmlO RejectChangesO WriteXmK) Отправляет все изменения, выполненные в данном DataSet после его загрузки или последнего вызова AcceptChanges () Полностью очищает DataSet, удаляя все строки в каждом DataTable Клонирует структуру DataSet, в том числе и всех DataTable, а также все отношения и ограничения Копирует структуру и данные текущего DataSet Возвращает копию DataSet, содержащую все изменения, которые были выполнены в данном DataSet после его загрузки или последнего вызова AcceptChanges (). У этого метода есть перегруженные варианты, которые позволяют получить только новые строки, только измененные строки или только удаленные строки Выдает, содержит ли DataSet изменения, т.е. новые, удаленные или измененные строки Объединяет данный DataSet с указанным DataSet Позволяет определить структуру объекта DataSet и заполнить его данными на основе XML-схемы и данных из потока Отменяет все изменения, которые были выполнены в данном DataSet после его загрузки или последнего вызова AcceptChanges () Позволяет записать содержимое DataSet в поток Создание DataSet Теперь, когда вы уже лучше понимаете роль DataSet (и имеете некоторое представление о его возможностях), создайте новое консольное приложение по имени SimpleDataSet и импортируйте пространство имен System.Data. В методе Main() определите новый объект DataSet с тремя расширенными свойствами, представляющими временную метку, уникальный идентификатор (типа System.Guid) и название компании: static void Main(string[] args) { Console.WriteLine ("***** Работа с объектами DataSet *****\n"); // Создание объекта DataSet и добавление нескольких свойств. DataSet carsInventoryDS = new DataSet("Car Inventory"); carsInventoryDS.ExtendedProperties["TimeStamp"] = DateTime.Now; carsInventoryDS.ExtendedProperties["DataSetID"] = Guid.NewGuid(); carsInventoryDS.ExtendedProperties["Company" ] = "Супер-гипер-магазин Mikko"; Console.ReadLine();
808 Часть V. Введение в библиотеки базовых классов .NET Если вы не знакомы с концепцией глобально уникальных идентификаторов (globally unique identifier — GUID), просто считайте, что это статически уникальное 128-битное число. Хотя идентификаторы GUID применяются в среде СОМ для идентификации различных сущностей СОМ (классы, интерфейсы, приложения и т.д.), тип System.Guid очень полезен и в .NET, когда нужно быстро сгенерировать уникальный идентификатор. В любом случае объект DataSet не очень-то интересен, если он не содержит хоть немного объектов DataTable. Значит, теперь нужно изучить внутреннее устройство класса DataTable, начиная с типа DataColumn. Работа с объектами DataColumn Тип DataColumn представляет один столбец в объекте DataTable. Вообще-то множество всех объектов DataColumn, содержащихся в данном объекте DataTable, содержит всю информацию схемы таблицы. Например, если понадобится создать копию таблицы Inventory из базы данных AutoLot (см. главу 21), нужно будет создать четыре объекта DataColumn, по одному для каждого столбца (CarlD, Make, Color и PetName). После создания объектов DataColumn они обычно добавляются в коллекцию столбцов типа DataTable (с помощью свойства Columns). Возможно, вы уже знаете, что любому столбцу в таблице базы данных можно назначить набор ограничений (в виде первичного ключа, значения по умолчанию, разрешения только на чтение информации и т.д.). Кроме того, каждый столбец таблицы должен относиться к одному из разрешенных в СУБД типов данных. К примеру, схема таблицы Inventory требует, чтобы столбец CarlD содержал целые числа, а столбцы Make, Color и PetName — массив символов. Класс DataColumn имеет ряд свойств для указания таких моментов. Список некоторых основных свойств приведен в табл. 22.3. Таблица 22.3. Свойства класса DataColumn Свойство Назначение AllowDBNull Указывает, может ли данный столбец содержать пустые значения. По умолчанию содержит значение true Autolncrement Применяются для настройки поведения автоинкремента для данного AutoIncrementSeed столбца. Это может оказаться удобным, если нужно обеспечить уни- AutoIncrementStep кальность значений в этом DataColumn (например, если он содержит первичные ключи). По умолчанию DataColumn не поддерживает автоинкрементное поведение Caption Задает или получает заголовок, который должен отображаться для данного столбца. Это позволяет определить более наглядные варианты для имен столбцов в базе данных ColumnMapping Определяет представление DataColumn при сохранении DataSet в виде XML-документа с помощью метода DataSet .WriteXml (). Можно указать, что столбец данных должен быть записан как XML- элемент, XML-атрибут, простое текстовое содержимое, либо его следует полностью проигнорировать ColumnName Задает или получает имя столбца из коллекции Columns (т.е его внутреннее представление в DataTable). Если не занести значение в ColumnName явно, то по умолчанию там находится слово "Column" с числовыми суффиксами по формуле л+1 (т.е. Columnl, Column2, Column3 и т.д.)
Глава 22. ADO.NET, часть II: автономный уровень 809 Окончание табл. 22.3 Свойство Назначение DataType Определяет тип данных (логический, строковый, с плавающей точкой и т.д.), хранящихся в данном столбце Def aultValue Задает или получает значение по умолчанию, заносимое в данный столбец при вставке новых строк Expression Задает или получает выражение для фильтрации строк, вычисления значения столбца или создания агрегированного столбца Ordinal Задает или получает числовое положение столбца в коллекции Columns, содержащейся в DataTable Readonly Определяет, предназначен ли данный столбец только для чтения после добавления строки в таблицу. По умолчанию равно false Table Получает объект DataTable, содержащий данный DataColumn Unique Задает или получает значение, указывающее, должны ли быть уникальными значения во всех строках данного столбца, или допустимы совпадения. При присвоении столбцу ограничения первичного ключа свойство Unique должно содержать значение true Создание объекта DataColumn Продолжаем создание проекта SimpleDataSet (и демонстрацию применения типа DataColumn). Предположим, что вам нужно смоделировать столбцы таблицы Inventory. Поскольку столбец CarlD должен быть первичным ключом таблицы, его необходимо создать только для чтения, содержащим уникальные значения и не допускающим пустые значения (с помощью свойств Readonly, Unique и AllowDBNull). Вставьте в класс Program новый метод FillDataSet(), предназначенный для создания четырех объектов DataColumn. Он принимает в качестве единственного параметра объект DataSet: static FillDataSet(DataSet ds) { // Создание столбцов данных, соответствующих "реальным" // столбцам таблицы Inventory из базы данных AutoLot. DataColumn carlDColumn = new DataColumn ("CarlD", typeof(int)); carlDColumn.Caption = "Car ID"; carlDColumn.Readonly = true; carlDColumn.AllowDBNull = false; carlDColumn.Unique = true; DataColumn carMakeColumn = new DataColumn("Make", typeof(string)); DataColumn carColorColumn = new DataColumn("Color", typeof(string)); DataColumn carPetNameColumn = new DataColumn("PetName", typeof(string)); carPetNameColumn.Caption = "Друж.Имя"; } Обратите внимание, что при конфигурировании объекта carlDColumn было присвоено значение свойству Caption. Это позволяет определить строковое значение для отображения при выводе данных, которое может отличаться от имени столбца (имена столбцов в таблицах баз данных обычно более удобны для программирования (например, au_f name), чем для отображения (например, Author First Name)). По той же причине здесь задано заглавие столбца PetName, т.к. Друж.Имя понятнее конечному пользователю, чем PetName.
810 Часть V. Введение в библиотеки базовых классов .NET Включение автоинкрементных полей Одним из аспектов DataColumn, допускающим настройку, является возможность ав- ттюинкремента (autoincrement). Если в какую-либо таблицу добавляется новая строка, то значение автоинкрементного поля устанавливается автоматически, на основании предыдущего значения и шага автоинкремента. Это удобно, когда нужно, чтобы в каком-либо столбце не было повторяющихся значений (обычно это первичный ключ). Такое поведение регулируется свойствами Autoincrement, AutoIncrementSeed и AutoIncrementStep. Значение AutoIncrementSeed используется для задания начального значения в столбце, AutoIncrementStep — для задания числа, которое прибавляется для каждого последующего значения. Рассмотрим следующую модификацию создания carlDColumn: static void FillDataSet(DataSet ds) { DataColumn carlDColumn = new DataColumn("CarlD" , typeof ( mt)); carlDColumn.Readonly = true; carlDColumn.Caption = "Car ID"; carlDColumn.AllowDBNull = false; carlDColumn.Unique = true; carlDColumn.Autoincrement = true; carlDColumn.AutoIncrementSeed = 0; carlDColumn.AutoIncrementStep = 1; } Здесь объект carlDColumn настроен так, что при добавлении новых строк его значения увеличиваются на 1. Поскольку первоначальное значение задано равным 0, в столбце будут содержаться числа 0, 1, 2, 3 и т.д. Добавление объектов DataColumn в DataTable Обычно тип DataColumn не применяется обособленно, а вставляется в нужный объект DataTable. Для демонстрации создайте новый объект DataTable (который вскоре будет подробно рассмотрен) и вставьте каждый объект DataColumn в коллекцию столбцов с помощью свойства Columns: static void FillDataSet(DataSet ds) { // Добавление объектов DataColumn в DataTable. DataTable inventoryTable = new DataTable ("In /ontor_, ") ; inventoryTable.Columns.AddRange(new DataColumn[] { carlDColumn, carMakeColumn, carColorColumn, carPetNameColumn }); } Теперь объект DataTable содержит четыре объекта DataColumn, которые представляют схему находящейся в памяти таблицы Inventory. Но пока эта таблица еще не содержит данных и не входит в коллекцию таблиц, принадлежащих конкретному DataSet. Мы сделаем и то, и то, а начнем с заполнения таблицы данными с помощью объектов DataRow. Работа с объектами DataRow Мы видели, что коллекция объектов DataColumn представляет схему объекта DataTable. А коллекция объектов DataRow представляет конкретные данные в таблице. И если на складе имеются 20 автомобилей, то для хранения информации о них нуж-
Глава 22. ADO.NET, часть II: автономный уровень 811 но 20 объектов Data Row. Некоторые (но не все) члены класса Data Row перечислены в табл. 22.4. Таблица 22.4. Основные члены типа DataRow Член Назначение HasErrors GetColumnsInError() GetColumnError () ClearErrors() RowError ItemArray RowState Table AcceptChanges() RejectChangesO BeginEdit() EndEditO CancelEditO DeleteO IsNullO Свойство HasErrors возвращает логическое значение, означающее наличие ошибок. Если они есть, то метод GetColumnsInError () позволяет получить ошибочные столбцы, а метод GetColumnError () — получить описание ошибки. Аналогично, метод ClearErrorsO удаляет из строки всю информацию об ошибках. Свойство RowError позволяет создать текстовое описание ошибки для данной строки Свойство, задающее или получающее все значения столбцов строки в виде массива объектов Свойство, позволяющее зафиксировать текущее состояние объекта DataRow в содержащем его DataTable с помощью значений перечисления RowState (новый, измененный, не измененный или удаленный) Свойство, позволяющее получить ссылку на объект DataTable, содержащий данный объект DataRow Методы для фиксации или отмены всех изменений, выполненных в данной строке с момента последнего вызова AcceptChanges () Методы, начинающие, заканчивающие или отменяющие операцию редактирования для объекта DataRow Метод, помечающий данную строку для удаления при вызове метода AcceptChanges() Метод, получающий значение, которое указывает, содержит ли заданный столбец пустое значение Работа с объектами DataRow несколько отличается от работы с DataColumn: невозможно напрямую создать экземпляр данного типа, т.к. у него нет общедоступного конструктора: // Ошибка! Нет общедоступного конструктора! DataRow г = new DataRow(); Вместо этого новый объект DataRow можно получить из конкретного DataTable. Предположим, например, что в таблицу Inventory нужно вставить две строки. Метод DataTable.NewRow() позволяет получить очередное место в таблице, после чего можно заполнить каждый столбец с помощью индексатора типа. При этом можно указать либо строковое имя, присвоенное объекту DataColumn, либо номер его позиции (начиная с нуля): static void FillDataSet(DataSet ds) // Добавление нескольких строк в таблицу Inventory. DataRow carRow = inventoryTable.NewRow (); carRow["Make"] = "BMW"; carRow["Color"] = "Black"; carRow["PetName"] = "Hamlet"; inventoryTable.Rows.Add(carRow); carRow = inventoryTable.NewRow() ;
812 Часть V. Введение в библиотеки базовых классов .NET // Столбец 0 содержит автоинкрементное // поле, поэтому начинаем с первого. carRow[l] = "Saab"; carRow[2] = "Red"; carRow[3] = "Sea Breeze"; inventoryTable.Rows.Add(carRow); } На заметку! Если передать методу индексатора типа DataRow неверное имя столбца или позицию, будет сгенерировано исключение времени выполнения. Теперь у вас есть один объект DataTable, содержащий две строки. Конечно, этот общий процесс можно повторить, чтобы создать ряд объектов DataTable, определить их схемы и заполнить данными. Но прежде чем вставить объект inventoryTable в объект DataSet, необходимо разобраться с очень важным свойством RowState. Свойство RowState Свойство RowState применяется для программной идентификации множества всех строк таблицы, которые изменили свое первоначальное значение, были вставлены и т.п. Это свойство может принимать любое значение из перечисления DataRowState. Возможные значения приведены в табл. 22.5. Таблица 22.5. Значения перечисления DataRowState Значение Назначение Added Строка была добавлена в DataRowCollection, a AcceptChanges () еще не был вызван Deleted Строка была помечена для удаления с помощью метода Delete () класса DataRow, a AcceptChanges () еще не был вызван Detached Строка была создана, но не включена ни в какой DataRowCollection. Объект DataRow находится в этом состоянии после его создания, но до занесения в какую-либо коллекцию, либо после исключения из коллекции Modified Строка была изменена, a AcceptChanges () еще не был вызван Unchanged Строка не была изменена после последнего вызова AcceptChanges () При программной работе со строками объекта DataTable значения в свойство RowState заносятся автоматически. Для примера добавьте в свой класс Program новый метод, который обрабатывает локальный объект DataRow, заодно и выводя состояние его строк: private static void ManipulateDataRowState () { // Создание нового DataTable для демонстрационных целей. DataTable temp = new DataTable("Temp")/ temp.Columns.Add(new DataColumn("TempColumn", typeof (int))); // RowState = Detached (т.е. еще не принадлежит никакому DataTable). DataRow row = temp.NewRow(); Console.WriteLine("После вызова NewRow(): {0}", row.RowState); // RowState = Added. temp.Rows.Add(row); Console.WriteLine("После вызова Rows.Add(): {0}", row.RowState);
Глава 22. ADO.NET, часть II: автономный уровень 813 // RowState = Added. row["TempColumn"] = 10; Console.WriteLine("После первого присваивания: {0}", row.RowState)/ // RowState = Unchanged. temp.AcceptChanges() ; Console.WriteLine("После вызова AcceptChanges(): {0}", row.RowState); // RowState = Modified. row["TempColumn"] =11; Console.WriteLine("После второго присваивания: @}"r row.RowState); // RowState = Deleted. temp.Rows[0].Delete(); Console.WriteLine("После вызова Delete(): @}"r row.RowState); } Объект ADO.NET DataRow вполне разумно отслеживает свое состояние. Поэтому владеющий этим объектом объект DataTable может определить добавленные, измененные или удаленные строки. Это очень важная возможность DataSet, потому что когда наступит время послать информацию в хранилище данных, будут отправлены только измененные данные. Свойство DataRowVersion Кроме отслеживания текущего состояния строк с помощью свойства RowState, объект DataRow отслеживает три возможные версии содержащихся в нем данных с помощью свойства DataRowVersion. При первоначальном создании объект DataRow содержит лишь одну копию данных, которая считается "текущей версией". Но при программной работе с объектом DataRow (с помощью вызовов различных методов) появляются дополнительные версии данных. Конкретнее, свойство DataRowVersion может содержать любое значение соответствующего перечисления DataRowVersion (см. табл. 22.6). Таблица 22.6. Значения перечисления DataRowVersion Значение Назначение Current Представляет текущее значение строки, даже после выполнения изменений Default Стандартный вариант DataRowState. Если значение DataRowState равно Added, Modified или Deleted, то стандартной версией является Current. Для значения DataRowState, равного Detached, стандартной версией является Proposed Original Представляет значение, первоначально вставленное в DataRow, или значение при последнем вызове AcceptChanges () Proposed Значение строки, редактируемой в настоящий момент с помощью вызова BeginEditO Как показано в табл. 22.6, значение свойства DataRowVersion в большинстве случаев зависит от значения свойства DataRowState. А как было сказано ранее, значение свойства DataRowVersion автоматически изменяется при вызовах различных методов объекта DataRow (а в некоторых случаях DataTable). Ниже представлена схема влияния этих методов на значение свойства DataRowVersion произвольной строки. • При изменении значения строки поле вызова метода DataRow.BeginEditO становятся доступны значения Current и Proposed. • При вызове метода DataRow.CancelEditO значение Proposed удаляется.
814 Часть V. Введение в библиотеки базовых классов .NET • После вызова DataRow.EndEditO значение Proposed меняется на Current. • После вызова метода DataRow.AcceptChangesO значение Original становится равным значению Current. To же самое происходит и при вызове DataTable. AcceptChanges(). • После вызова DataRow.RejectChanges () значение Proposed отбрасывается, и версия становится равной Current. Да, все это несколько запутанно — особенно из-за того, что в любой данный момент времени DataRow может иметь, а может и не иметь все версии (при попытке получения версии строки, которая в данный момент не отслеживается, возникнут исключения времени выполнения). Но поскольку DataRow отслеживает три копии данных, то, несмотря на эту сложность, можно без особого труда создать пользовательский интерфейс, который позволяет конечному пользователю изменить значения, потом передумать и отказаться от этих изменений или зафиксировать их, чтобы они хранились постоянно. В остальной части этой главы вы увидите различные примеры использования этих методов. Работа с объектами DataTable Тип DataTable определяет значительное количество членов, многие из которых совпадают по именам и функциям с аналогичными членами DataSet. В табл. 22.7 приведены некоторые основные члены типа DataTable, кроме Rows и Columns. Таблица 22.7. Основные члены типа DataTable Член Назначение CaseSensitive Указывает, чувствительны ли к регистру символов строковые сравнения в таблице. По умолчанию равно false ChildRelations Возвращает коллекцию дочерних отношений для данного DataTable (если они есть) Constraints Получает коллекцию ограничений, поддерживаемых данной таблицей Сору () Метод, копирующий схему и дату DataTable в новый экземпляр DataSet Получает DataSet, содержащий данную таблицу (если он есть) Def aultview Получает специализированное представление таблицы, которое может содержать отфильтрованное представление или позицию курсора ParentRelations Получает коллекцию родительских отношений для данного DataTable PrimaryKey Получает или задает массив столбцов, которые выступают в качестве первичных ключей для таблицы данных RemotingFormat Позволяет определить формат сериализации объектом DataSet его содержимого (двоичный или XML) для уровня .NET Remoting TableName Получает или задает имя таблицы. Значение этого свойства можно также задать через параметр конструктора В продолжение нашего примера занесем в свойство PrimaryKey объекта DataTable объект DataColumn по имени carlDColumn. Учтите, что свойству PrimaryKey соответствует коллекция объектов DataColumn, чтобы можно было обрабатывать ключи из нескольких столбцов. Но в нашем случае нужно указать только столбец CarlD (который находится в таблице в самой первой позиции):
Глава 22. ADO.NET, часть II: автономный уровень 815 static void FillDataSet(DataSet ds) { // Указание первичного ключа для данной таблицы. inventoryTable.PrimaryKey = new DataColumn[] { inventoryTable.Columns[0] }; } Вставка объектов DataTable в DataSet Наш объект DataTable завершен. Осталось вставить его в объект carsInventoryDS типа DataSet с помощью коллекции Tables: static void FillDataSet(DataSet ds) { // После чего добавление таблицы в DataSet. ds.Tables.Add(inventoryTable); Теперь нужно вставить в метод Main() вызов FillDataSet () и передать ему в качестве аргумента локальный объект DataSet. Затем передадим этот объект новому (еще не написанному) вспомогательному методу PrintDataSetO: static void Main(string[] args) { Console.WriteLine ("***** Работа с объектами DataSet *****\n"); FillDataSet(carsInventoryDS); PrintDataSet(carsInventoryDS); Console.ReadLine(); } Получение данных из объекта DataSet Метод PrintDataSetO просто перебирает все метаданные DataSet (используя коллекцию ExtendedProperties) и все DataTable в этом DataSet, выводя имена столбцов и значения строк с помощью индексаторов: static void PrintDataSet(DataSet ds) { // Вывод имени и расширенных свойств. Console.WriteLine("Имя DataSet: {0}", ds.DataSetName); foreach (System.Collections.DictionaryEntry de in ds.ExtendedProperties) { Console.WriteLine("Ключ = {0}, Значение = {1}", de.Key, de.Value); } Console.WriteLine(); // Вывод каждой таблицы. foreach (DataTable dt in ds.Tables) { Console.WriteLine("=> Таблица {0}:", dt .TableName); // Вывод имен столбцов. for (int curCol = 0; curCol < dt.Columns.Count/ curCol++) { Console.Write(dt.Columns[curCol].ColumnName + "\t"); } Console.WriteLine ("\n ") ; // Вывод содержимого DataTable. for (int curRow = 0; curRow < dt.Rows.Count; curRow++)
816 Часть V. Введение в библиотеки базовых классов .NET for (int curCol = 0; curCol < dt.Columns.Count/ curCol++) { Console.Write(dt.Rows[curRow][curCol].ToStringO + "\t"); } Console.WriteLine (); } } } Если теперь запустить эту программу, буде получен следующий результат (конечно, с другими отметками времени и значением GUID): ***** Работа с объектами DataSet ***** Имя DataSet: Car Inventory Ключ = TimeStamp, Значение = 1/22/2010 6:41:09 AM Ключ = DataSetID, Значение = Ilc533ed-dlaa-4c82-96d4-b0f88893ab21 Ключ = Company, Значение = Супер-гипер-магазин Mikko => Таблица Inventory: CarlD Make Color Др.Имя 0 BMW Black Hamlet 1 Saab Red Sea Breeze Обработка данных из DataTable с помощью объектов DataTableReader Вспомните вашу работу в предыдущем примере, и вы поймете, что способы обработки данных с помощью подключенного уровня (т.е. с помощью объектов чтения данных) и автономного уровня (т.е. с помощью объектов DataSet) весьма различаются. При работе с объектом чтения данных обычно пишется цикл while, вызывается метод Read(), и с помощью индексатора выбираются пары имя/значение. А при обработке DataSet нужен целый ряд циклических конструкций, чтобы добраться до данных, находящихся в таблицах, строках и столбцах (не забывайте, что для DataReader нужно открытое подключение к базе данных, чтобы он мог прочитать данные из реальной базы). Объекты DataTable поддерживают метод CreateDataReader(). Этот метод позволяет получать данные из DataTable с помощью схемы навигации, похожей на тип чтения данных (объект чтения данных читает данные из находящейся в памяти таблицы DataTable, а не из реальной базы данных, поэтому здесь подключение к базе данных не требуется). Основное преимущество такого подхода состоит в том, что теперь при обработке данных используется единая модель, независимо от уровня ADO.NET, применяемого для получения этих данных. Допустим, в класс Program добавлена следующая вспомогательная функция по имени PrintTable(): static void PrintTable(DataTable dt) { // Создание объекта DataTableReader. DataTableReader dtReader = dt.CreateDataReader(); // DataTableReader работает так же, как и DataReader. while (dtReader.Read()) { for (int 1=0; l < dtReader.FieldCount; i++) { Console.Write ("{0}\t", dtReader.GetValue(l) .ToStringO .Trim()); } Console.WriteLine () ; } dtReader.Close(); }
Глава 22. ADO.NET, часть II: автономный уровень 817 Тип DataTableReader работает точно так же, как и объект чтения данных конкретного поставщика данных. Он очень удобен, если нужно быстро загрузить данными объект DataTable, не путаясь во внутренних коллекциях строк и столбцов. А теперь изменим предыдущий метод PrintDataSet(), чтобы в нем применялись вызовы PrintTableO, а не перебор коллекций Rows и Columns: static void PrintDataSet(DataSet ds) ( // Вывод имени и всех расширенных свойств. Console.WriteLine("Имя DataSet: {0}", ds.DataSetName); foreach (System.Collections.DictionaryEntry de in ds.ExtendedProperties) { Console.WriteLine("Ключ = {0}, Значение = {1}", de.Key, de.Value); } Console.WriteLine(); foreach (DataTable dt in ds.Tables) { Console.WriteLine ("=> Таблица {0}:", dt.TableName) ; // Вывод имен столбцов. for (int curCol = 0; curCol < dt.Columns.Count; curCol++) { Console.Write(dt.Columns[curCol] .ColumnName.Trim () + "\t"); } Console .WriteLine ("\n ") ; // Вызов нового вспомогательного метода. PrintTable(dt); После запуска этого приложения вы увидите выходные данные, полностью совпадающие с приведенными выше. Единственным отличием будет внутренний доступ к содержимому DataTable. Сериализация объектов DataTable и DataSet в формате XML И тип DataSets, и тип DataTables поддерживают методы WriteXmlO и ReadXml(). Метод WriteXmlO позволяет сохранить содержимое объекта в локальном файле (а также в любом типе, производном от System. 10.Stream) в виде XML-документа. Метод ReadXmlO позволяет получить состояние DataSet (или DataTable) из XML-документа. Кроме этого, типы DataSet и DataTable поддерживают методы WriteXmlSchema () и ReadXmlSchemaO, которые сохраняют в файле *.xsd только схему и загружают ее оттуда. Вы можете проверить их работу самостоятельно. Для этого измените метод Main(), чтобы он вызывал следующую финальную вспомогательную функцию (которой передается единственный параметр типа DataSet): static void DataSetAsXml(DataSet carsInventoryDS) I // Сохранение данного DataSet в виде XML. carsInventoryDS.WriteXml("carsDataSet.xml"); carsInventoryDS.WriteXmlSchema("carsDataSet.xsd"); // Очистка DataSet. carsInventoryDS.Clear() ; // Загрузка DataSet из XML-файла. carsInventoryDS.ReadXml("carsDataSet.xml")/
818 Часть V. Введение в библиотеки базовых классов .NET Если открыть файл carsDataSet.xml (который находится в папке \bin\Debug вашего проекта), то можно увидеть, что каждый столбец таблицы закодирован в виде XML- элемента: <?xml version="l.0м standalone="yes"?> <Car_x0020_Inventory> <Inventory> <CarID>0</CarID> <Make>BMW</Make> <Color>Black</Color> <PetName>Hamlet</PetName> </Inventory> <Inventory> <CarID>K/CarID> <Make>Saab</Make> <Color>Red</Color> <PetName>Sea Breeze</PetName> </Inventory> </Car_x0020_Inventory> Если в Visual Studio дважды щелкнуть на сгенерированном .xsd-файле (который также находится в папке \bin\Debug), то откроется встроенный редактор схемы XML (рис. 22.3). carsDataSetJtsd %& Inventory * CarlD Make Color PetName Рис. 22.3. Редактор XSD из Visual Studio 2010 На заметку! В главе 24 будет описан LINQ to XML API, который сейчас рекомендуется для обработки XML-данных в платформе .NET. Сериализация объектов DataTable и DataSet в двоичном формате Содержимое объекта DataSet (или отдельного DataTable) можно также сохранить в компактном двоичном формате. Это особенно удобно при необходимости переслать объект DataSet за пределы компьютера (в случае распределенного приложения), ведь одним из недостатков представления данных в виде XML является его многословность, что приводит к большому объему данных. Чтобы сохранить объект DataTable или DataSet в двоичном формате, просто занесите в свойство RemotingFormat значение SerializationFormat.Binary. После этого, как нетрудно догадаться, можно воспользоваться типом BinaryFormatter (см.
Глава 22. ADO.NET, часть II: автономный уровень 819 главу 20). Рассмотрим следующий финальный метод проекта SimpleDataSet (не забудьте импортировать пространства имен System.10 и System.Runtime.Serialization. Formatters. Binary): static void DataSetAsBinary(DatiSet carsInventoryDS) { // Установка признака двоичной сериализации. carsInventoryDS. RemotingFormat = SenalizationFormat .Binary ; // Сохранение данного DataSet в двоичном виде. FileStream fs = new FileStream("BinaryCars.bin", FileMode.Create); BinaryFormatter bFormat = new BinaryFormatter(); bFormat.Serialize(fs, carsInventoryDS); fs.Close(); // Очистка DataSet. carsInventoryDS.Clear (); // Загрузка DataSet из двоичного файла. fs = new FileStream("BinaryCars.bin", FileMode.Open); DataSet data = (DataSet)bFormat.Deserialize(fs); } После выполнения этого метода из Main() файл *.bin будет находиться в папке bin\ Debug. На рис. 22.4 показано содержимое файла BinaryCars.bin. BinaryCars.bin X carsDataSetjesd Prograrn.cs Object Brovw 00002890 000028а0 000028Ь0 000028с0 000028d0 000028е0 000028f0 00002900 00002910 00002920 00002930 00002940 00002950 00002960 00002970 00002980 00002990 000029а0 000029Ь0 00 00 00 00 00 00 65 6D 2Е 62 02 5F 02 5F 68 00 00 00 02 02 АЕ 21 29 01 05 00 00 00 08 00 00 00 00 100 04 53 2D 00 00 52 65 64 log о$ 48 00 10 1А 00 00 00 47 75 69 63 02 5F 02 5F 69 00 00 00 A3 DB 7D 21 00 00 00 05 00 00 00 00 06 2В 00 61 20 42 00 09 31 00 00 00 02 00 00 61 61 62 00 05 42 11 25 00 61 6D 6С 00 00 04 IF 64 0В 64 02 02 5F 00 08 39 9С 00 09 00 00 01 00 00 00 72 65 65 00 00 00 ОС 00 00 00 01 28 11 24 6С 61 00 00 65 74 7А 65 02 00 00 09 00 00 00 00 00 00 00 00 5F 65 6А 02 07 07 D5 4F 00 00 0F 22 00 00 03 D2 00 00 63 6В 02 00 _0бГ30~ 01 26 00 00 32 00 00 ОС 00 00 00 10 1В 00 00 0В 53 79 73 74 Syst .Guid. b._c._d._e •_h._i._j. !). .}9..0. 00 02 5F 61 02 5F 02 5F 66 02 5F 67 5F 6B 00 00 00 00 02 02 02 02 02 02 93 DB 74 BB FF 72 00 09 2A 00 00 00 00 00 00 02 00 00 11 23 00 00 00 02 # 4D 57 06 2C 00 00| 00 02 00 00 00 06 06 2E 00 00 00 03 00 00 06 2F 00 QQl 00 00 00 0A 53 65 00 00 00 0C 00 00 02 00 00 00 01 27 00 00 02 00 00 00 00 00 00 09 33 00 Рис. 22.4. Сериапизация объекта DataSet в двоичном формате Исходный код. Проект SimpleDataSet доступен в подкаталоге Chapter 22. Привязка объектов DataTable к графическим интерфейсам Windows Forms К данному моменту вы уже научились вручную создавать, заполнять и просматривать содержимое объекта DataSet с помощью внутренней объектной модели ADO.NET. Все это довольно важно, но с платформой .NET поставляются многочисленные API- интерфейсы, которые автоматически могут связывать данные с элементами пользовательского интерфейса. Например, в Windows Forms — "родном" инструментальном наборе GUI .NET — имеется элемент управления D at aG r id View, который может отображать содержимое объекта DataSet или DataTable с помощью всего нескольких строк кода. ASP.NET (API-
820 Часть V. Введение в библиотеки базовых классов .NET интерфейс для веб-разработки в составе .NET) и Windows Presentation Foundation API (новый, гораздо более мощный API-интерфейс для построения графических пользовательских интерфейсов, появившийся в .NET 3.0) тоже поддерживают привязку данных. Вы научитесь привязывать данные к графическим элементам WPF и ADO.NET немного позже; но в данной главе вы уже начнете применять Windows Forms, т.к. это довольно простая и понятная модель программирования. На заметку! В следующем примере предполагается, что у вас есть некоторый опыт использования Windows Forms для создания графических пользовательских интерфейсов. Если это не так, можете просто открыть готовое решение и проследить за изложением, либо вернуться к данному разделу после прочтения приложения А. Теперь мы создадим приложение Windows Forms, которое будет выводить в пользовательском интерфейсе содержимое объекта DataTable. Заодно вы научитесь фильтровать и изменять данные в таблице, а также познакомитесь с ролью объекта DataView. Сначала создайте новое рабочее пространство проекта Windows Forms по имени WindowsFormsDataBinding. Смените в Solution Explorer имя первоначального файла Forml.cs на более понятное MainForm.cs. Затем в Visual Studio 2010 Toolbox перетащите элемент DataGridView (переименованный в carlnventoryGridView с помощью свойства Name в окне Properties (Свойства)) на поверхность конструктора. При этом активизируется контекстное меню, которое позволяет выполнить подключение к физическому источнику данных. Пока не обращайте на это внимание, поскольку привязка объекта DataTable будет выполнена программно. И, наконец, добавьте метку с понятным текстом. Один из возможных вариантов приведен на рис. 22.5. Рис. 22.5. Первоначальный пользовательский интерфейс приложения Windows Forms Заполнение DataTable из обобщенного List<T> Аналогично предыдущему примеру SimpleDataSet, в приложении WindowsForms DataBmding будет создан объект DataTable, содержащий несколько DataColumn, которые будут представлять различные столбцы и строки данных. Но на этот раз строки будут заполняться с помощью переменной-члена List<T>. Сначала вставьте в проект новый класс С# с именем Саг, определенный следующим образом:
Глава 22. ADO.NET, часть II: автономный уровень 821 public class Car { public int ID { get; set; } public string PetName { get; set; } public string Make { get; set; } public string Color { get; set; } } Теперь добавьте в стандартный конструктор главной формы заполнение переменной-члена List<T> (с именем listCars) набором новых объектов Саг: public partial class MainForm : Form { // Коллекия объектов Car. List<Car> listCars = null; public MainForm() { InitializeComponent () ; // Добавление в список нескольких автомобилей. listCars = new List<Car>() { new Car { ID = 100, PetName = "Списку", Make = "BMW", Color = "Green" }, new Car { ID = 101, PetName = "Tiny", Make = "Yugo", Color = "White" }, new Car { ID = 102, PetName = "Ami", Make = "Jeep", Color = "Tan" }, new Car { ID = 103, PetName = "Pain Inducer", Make = "Caravan", Color = "Pink" }, new Car { ID = 104, PetName = "Fred", Make = "BMW", Color = "Green" }, new Car { ID = 105, PetName = "Sidd", Make = "BMW", Color = "Black" }, new Car { ID = 106, PetName = "Mel", Make = "Firebird", Color = "Red" }, new Car { ID = 107, PetName = "Sarah", Make = "Colt", Color = "Black" }, }; } } Добавьте в класс MainForm новую переменную-член типа DataTable по имени inventoryTable: public partial class MainForm : Form { // Коллекция объектов Car. List<Car> listCars = null; // Информация об автомобилях на складе. DataTable inventoryTable = new DataTable (); Теперь добавьте в этот же класс новую вспомогательную функцию CreateDataTable (), а ее вызов — в стандартный конструктор класса MainForm: private void CreateDataTable () { // Создание схемы таблицы. DataColumn carlDColumn = new DataColumn("ID", typeof(int)) ; DataColumn carMakeColumn = new DataColumn("Make", typeof(string)); DataColumn carColorColumn = new DataColumn("Color", typeof(string)); DataColumn carPetNameColumn = new DataColumn("PetName", typeof(string)); carPetNameColumn.Caption = "Pet Name"; inventoryTable.Columns.AddRange (new DataColumn[] { carlDColumn, carMakeColumn, carColorColumn, carPetNameColumn });
822 Часть V. Введение в библиотеки базовых классов .NET // Последовательное создание строк из элементов списка. foreach (Car с in listCars) { DataRow newRow = inventoryTable.NewRow(); newRow["Make"] = c.carMake; newRow["Color"] = c.carColor/ newRow["PetName"] = с.carPetName; inventoryTable.Rows.Add(newRow)/ } // Привязка DataTable к carlnventoryGridView. carlnventoryGridView.DataSource = inventoryTable; } Реализация метода начинается с создания схемы DataTable; для этого создаются три объекта DataColumn (для простоты автоинкрементное поле CarlD не добавлялось), а потом они добавляются в переменную-член DataTable. Содержимое строк переносится из поля List<T> в DataTable с помощью конструкции foreach и объектной модели ADO.NET. В последнем операторе кода метода CreateDataTableO таблица inventoryTable присваивается свойству DataSource объекта DataGridView. Данное свойство — единственное, что нужно для привязки DataTable к объекту DataGridView. Этот графический элемент выполняет внутреннее чтение коллекций строк и столбцов, примерно так же, как это делал метод PrintDataSetO в примере SimpleDataSet. Теперь можно запустить приложение и увидеть содержимое DataTable в элементе DataGridView (рис. 22.6). ndows Forms Data Bmdin Here is what we have in stock HDBB ► ID 101 102 103 104 105 106 1П7 Make BMW Yugo Jeep Caravan BMW BMW Firebird n* Color Green White Tan P«* Green Black Red ffcrt PetName Ш *| Cbucky Tiny a™ W-\ Pain Inducer Fred Skid ILJ Mel S*ah iH Рис. 22.6. Привязка объекта DataTable к элементу DataGridView из Windows Forms Удаление строк из DataTable Теперь добавим к графическому интерфейсу возможность удалять строки из находящегося в памяти объекта DataTable, который привязан к элементу DataGridView. Один из возможных подходов — вызов метода Delete () объекта DataRow, который содержит удаляемую строку. Для этого нужно просто указать индекс (или объект DataRow), представляющий эту строку. Чтобы пользователь мог указать, какую строку необходимо удалить, добавьте в конструктор элементы TextBox (txtRowToRemove) и Button (btnRemoveRow). На рис. 22.7 показан один из возможных вариантов изменения интерфейса (обратите внимание на группирование двух элементов с помощью элемента GroupBox, которое подчеркивает их связь).
Глава 22. ADO.NET, часть II: автономный уровень 823 °&< :*• ID of Car to Delete Рис. 22.7. Измененный интерфейс для удаления строк из DataTable Ниже приведен код обработчика события Click новой кнопки, который удаляет указанную пользователем строку (по идентификатору автомобиля) из находящегося в памяти объекта DataTable. Метод Select () из класса DataTable позволяет указать критерий поиска, который похож на обычный синтаксис SQL. Метод возвращает массив объектов, удовлетворяющих критерию поиска: // Удаление данной строки из DataRowCollection. private void btnP«=moveRow_Click (object sender, EventArgs e) { try { // Поиск строки, которую нужно удалить. DataRow[] rowToDelete = inventoryTable.Select( string.Formиt("ID={0}", int.Parse(txtCarToRemove.Text))); // Удаление. rowToDelete [0] .Delete() ; inventoryTable.AcceptChanges(); } catch(Exception ex) { MessageBox.Show(ex.Message); Теперь можно запустить приложение и указать идентификатор удаляемого автомобиля. При удалении объектов DataRow из DataTable графическая таблица реагирует моментально, т.к. она привязана к состоянию объекта. Выборка строк с помощью фильтра Во многих приложениях обработки данных бывает необходимо просматривать небольшое подмножество данных из DataTable на основании какого-то критерия фильтрации. Например, может понадобиться просмотреть только автомобили BMW, которые хранятся в памяти в объекте DataTable. Вы уже видели, как метод Select () класса DataTable позволяет найти строгсу для удаления, но его можно использовать и для выборки подмножества записей для их отображения.
824 Часть V. Введение в библиотеки базовых классов .NET В качестве иллюстрации еще раз изменим наш пользовательский интерфейс. Теперь пользователи получат возможность задавать модель автомобиля, которую они хотели бы просмотреть (рис. 22.8) с помощью новых элементов Text Box (с именем txtMakeToView) и Button (с именем btnDisplayMakes). MatnForm.cs [Design]* X I "'j Windows Forms Data Binding Here is what we have in stock hi ШЫ Enter ID of Car to Delete °ШегМ Рис. 22.8. Добавление возможности фильтрации строк Метод Select () имеет несколько перегруженных вариантов, предоставляющих различные возможности выборки. На самом простом уровне передаваемый в Select () параметр является строкой, которая содержит какое-то условное выражение. Для начала рассмотрим следующую логику обработки события Click только что добавленной кнопки: private void btnDisplayMakes_Click(object sender, EventArgs e) { // Создание фильтра на основании введенных пользователем данных. string filterStr = string.Format("Make= '{О}'", txtMakeToView.Text); // Поиск всех строк, удовлетворяющих фильтру. DataRow[] makes = inventoryTable.Select(filterStr); if (makes.Length == 0) MessageBox.Show("Sorry, no cars...", "Selection error!"); // ничего не найдено else { string strMake = null; for (int i=0; i < makes.Length; i++) { // Получение значения PetName из текущей строки. strMake += temp["PetName"] + "\n"; } // Вывод названий всех найденных автомобилей указанной марки. MessageBox.Show(strMake, string.Format("We have {0}s named:", txtMakeToView.Text)); Здесь сначала создается простой фильтр на основе значения из соответствующего поля Text Box. Если в этом поле указать BMW, то получится следующий фильтр: Make = 'BMW
Глава 22. ADO.NET, часть II: автономный уровень 825 Если передать этот фильтр методу Select (), он возвратит массив объектов DataRow со строками, удовлетворяющими заданному критерию (рис. 22.9). %* Windows Forms Data Binding Here is what we have in stock Cdor Green Pet Name Tchucky -eBMWsn^I Enter ID erf Car to Delete Списку Fred 81} Sidd Рис. 22.9. Вывод фильтрованных данных Логика фильтрации основана на стандартом синтаксисе SQL. Для примера предположим, что результаты, полученные от предыдущего вызова Select(), нужно выдать упорядоченными по столбцу PetName. К счастью, имеется перегруженный вариант метода Select (), позволяющий указать критерий сортировки: // Сортировка по PetName. makes = mventoryTable.Select(filterStr, "PetName"); При необходимости вывести результаты упорядоченными по убыванию вызов SelectO выглядит так: // Вывод результатов в порядке убывания. makes = mventoryTable.Select(filterStr, "PetName DESC"); В общем случае строка с критерием сортировки должна содержать имя столбца, за которым идет слово ASC ("ascending" — по возрастанию, подразумевается по умолчанию) или DESC ("descending" — по убыванию). При необходимости можно указать несколько столбцов, разделенных запятыми. И, наконец, строку с критерием фильтрации можно составить из любого количества реляционных операторов. Например, вот вспомогательная функция, которая выполняет поиск всех автомобилей с идентификатором, большим 5: private void ShowCarsWithldGreaterThanFive() { // Вывод дружественных имен всех автомобилей с ID, большим 5. DataRow[] properlDs; string newFilterStr = "ID > 5"; properlDs = mventoryTable.Select(newFilterStr); string strlDs = null; for(int i=0; l < properlDs.Length; i++) { DataRow temp = properlDs[1]; strlDs += temp ["PetName"] + " is ID " + temp ["ID"] + "\n"; MessageBox.Show(strlDs, "Pet names of cars where ID > 5");
826 Часть V. Введение в библиотеки базовых классов .NET Изменение строк в DataTable Последний аспект DataTable, с которым следует ознакомиться — это процесс изменения существующих строк, т.е. занесения в них новых значений. Один из способов сделать это состоит в получении строк (или строки), удовлетворяющих заданному критерию, с помощью метода Select (). После получения нужных DataRow остается изменить их содержимое. Допустим, например, что на поверхности, производной от формы, находится новый элемент Button по имени btnChangeMakes, при щелчке на котором выполняет поиск в DataTable всех строк, где столбец Make содержит значение BMW. После получения этих элементов нужно занести в Make значение Yugo: // Поиск с помощью фильтра всех строк, которые нужно изменить. private void btnChangeMakes_Click(object sender, EventArgs e) { // Проверка выбора пользователя. if (DialogResult.Yes == MessageBox.Show("Are you sure?? BMWs are much nicer than Yugos'", "Please Confirm!", MessageBoxButtons . YesIIn) ) { // Создание фильтра. string filterStr = "Make='BMW1"; string strMake = string.Empty; // Поиск всех строк, удовлетворяющих фильтру. DataRow[] makes = inventoryTable.Select(filterStr); // Замена всех Бумеров на Yugo. for (int i = 0; i < makes.Length; i++) { makes[i]["Make"] = "Yugo"; } }' } Работа с типом DataView Объект представления (view object) — это альтернативное представление таблицы (или набора таблиц). Например, с помощью Microsoft SQL Server можно создать представление для таблицы Inventory, которое возвращает новую таблицу, содержащую автомобили только определенного цвета. В ADO.NET тип DataView позволяет программным образом извлекать подмножество данных из DataTable в отдельный объект. Серьезным преимуществом наличия нескольких представлений одной и той же таблицы является то, что все эти представления можно привязать к различным графическим элементам (наподобие DataGridView). Например, один DataGridView может быть привязан к DataView, показывающему все автомобили из таблицы Inventory, а другой можно настроить на вывод лишь автомобилей зеленого цвета. Для примера добавьте в текущий графический интерфейс еще один элемент DataGridView по имени dataGridColtsView и элемент Label с пояснением. Потом определите переменную-член типа DataView по имени coltsOnlyView: public partial class MainForm : Form { // Отображение содержимого DataTable. DataView coltsOnlyView;
Глава 22. ADO.NET, часть II: автономный уровень 827 Затем создайте новую вспомогательную функцию CreateDataView () и поместите ее вызов в конструктор по умолчанию формы сразу за завершением создания DataTable: public MainFormO { // Создание таблицы данных. CreateDataTable(); // Создание представления. CreateDataView(); } Ниже показана реализация этой новой вспомогательной функции. Конструктору каждого DataView передается объект DataTable, который будет использован для создания специального набора строк данных. private void CreateDataView () { // Указание таблицы для создания данного представления. coltsOnlyView = new DataView(inventoryTable); // Настройка представлений с помощью фильтра. coltsOnlyView.RowFilter = "Make = ' Yugo'"; // Привязка к новой графической таблице. dataGridColtsView.DataSource = yugosOnlyView; } Как видно, класс DataView содержит свойство по имени RowFilter, содержащее строку с критерием фильтрации, который используется для извлечения искомых строк. После создания представления измените соответствующим образом свойство DataSource графической таблицы. Завершенное приложение в действии показано на рис. 22.10. Рис. 22.10. Вывод уникального представления данных
828 Часть V. Введение в библиотеки базовых классов .NET Исходный код. Проект WindowsFormsDataTableViewer доступен в подкаталоге Chapter 22. Работа с адаптерами данных Мы уже разобрались со всеми нюансами ручной работы с объектами DataSet в ADO.NET, и теперь можно перейти к рассмотрению объектов адаптеров данных. Класс адаптеров данных применяется для заполнения наборов данных DataSet с помощью объектов DataTable; кроме того, они могут отправлять измененные DataTable назад в базу данных для обработки. В табл. 22.8 перечислены основные члены базового класса DbDataAdapter, от которого порождаются все объекты адаптеров данных (например, SqlDataAdapter и OdbcDataAdapter). Таблица 22.8. Основные члены класса DbDataAdapter Член Назначение Fill 0 Выполняет команду SQL SELECT (указанную в свойстве SelectCommand) для запроса к базе данных и загрузки этих данных в объект DataTable SelectCommand Содержат SQL-команды, отправляемые в хранилище данных при вызовах InsertCommand методов Fill () и Update () UpdateCommand DeleteCommand Update () Выполняет команды SQL INSERT, UPDATE и DELETE (указанных свойствами InsertCommand, UpdateCommand и DeleteCommand) для сохранения в базе данных изменений, выполненных в DataTable Адаптер данных определяет четыре свойства: SelectCommand, InsertCommand, UpdateCommand и DeleteCommand. При создании объекта адаптера данных для конкретного поставщика данных (например, SqlDataAdapter) можно передать строку с текстом команды, используемом объектом команды SelectCommand. После должной настройки каждого из четырех объектов команд можно вызвать метод Fill() и получить объект DataSet (или, при желании, отдельный DataTable). Для этого объект команды выполняет оператор SQL SELECT, заданный с помощью свойства SelectCommand. Аналогично, при необходимости сохранить измененный объект DataSet (или DataTable) в базе данных для обработки можно вызвать метод Update (), который использует какой-то из оставшихся объектов команд в зависимости от состояния каждой строки в DataTable (подробнее чуть ниже). Один из самых странных аспектов работы с объектом адаптера данных состоит в том, что при этом не нужно открывать или закрывать подключение к базе данных. Все это делается автоматически. Однако адаптеру данных нужно передать объект подключения или строку подключения (на основании которой все равно будет создан объект подключения), чтобы сообщить адаптеру данных, с какой базой данных вы хотите взаимодействовать. На заметку! Адаптер данных безразличен по своей природе. Вы можете подключать на ходу разные объекты подключения и объекты команд и выбирать данные из самых различных баз данных. Например, один объект DataSet может содержать табличные данные, полученные от поставщиков данных SQL Server, Oracle и MySQL.
Глава 22. ADO.NET, часть II: автономный уровень 829 Простой пример адаптера данных Теперь добавим новые возможности в библиотеку доступа к данным AutoLotDAL.dll, созданную в главе 21. Вначале рассмотрим простой пример, в котором объект Data Set заполняется одной таблицей с помощью объекта адаптера данных ADO.NET. Создайте новое консольное приложение с именем FillDataSetUsingSqlDataAdapter и импортируйте в первоначальный код С# пространства имен System.Data и System. Data.SqlClient. Теперь измените метод Main() следующим образом (в зависимости от того, как вы создали базу данных AutoLot в предыдущей главе, может понадобиться изменить строку подключения): static void Main(string [ ] args) { Console.WriteLine ("***** Работа с адаптерами данных *~***\п"); // Жестко закодированная строка подключения. string cnStr = "Integrated Security = SSPI;Initial Catalog=AutoLot;" + @"Data Source=(local)\SQLEXPRESS"; // Создание объекта DataSet вызывающим процессом. DataSet ds = new DataSet ("AutoLot"); // Передача адаптеру текста команды Select и подключения. SqlDataAdapter clAdapt = new SqlDataAdapter ("Select * From Inventory", cnStr); // Заполнение DataSet новой таблицей с именем Inventory. dAdapt.Fill(ds, "Inventory"); // Вывод содержимого DataSet. PrintDataSet(ds); Console.ReadLine(); } Для создания адаптера данных используется строковый литерал, который преобразуется в SQL-оператор Select. Из этого значения адаптер создает объект команды, который потом можно получить с помощью свойства SelectCommand. Обратите внимание, что экземпляр класса DataSet должен быть создан вызывающим процессом, и уже затем передан в метод Fill(). В этот метод можно передать второй, не обязательный, аргумент — строку, с помощью которой будет сформировано свойство TableName нового объекта DataTable (если не указать имя таблицы, то адаптер данных назовет таблицу просто Table). Обычно имя, присвоенное DataTable, совпадает с именем физической таблицы в реляционной базе данных, но это не обязательно. На заметку! Метод Fill() возвращает целое число, равное количеству строк, возвращенных SQL-запросом. И, наконец, обратите внимание, что в методе Main () нигде нет явного открытия или закрытия подключения к базе данных. В методе Fill() любого адаптера данных с самого начала заложена возможность открытия и закрытия подключения перед вызовом метода Fill(). Поэтому при передаче объекта DataSet методу PrintDataSet () (реализованному выше в данной главе) вы оперируете с локальной копией автономных данных, для которой не нужны дополнительные выборки данных. Замена имен из базы данных более понятными названиями Как уже было сказано, администраторы баз данных обычно создают такие имена таблиц и столбцов, что они редко бывают удобны для конечных пользователей (например, auid, aufname, aulname и т.д.). Но в жизни есть место и хорошему: объ-
830 Часть V. Введение в библиотеки базовых классов .NET екты адаптеров данных содержат внутреннюю строго типизированную коллекцию DataTableMappingCollection объектов типа System.Data.Common.DataTableMapping. Доступ к этой коллекции возможен через свойство TableMappings объекта адаптера данных. При желании можно поработать с этой коллекцией, чтобы сообщить объекту DataTable, какие отображаемые имена следует использовать при выводе его содержимого. Предположим, например, что при выводе информации вместо имени таблицы Inventory лучше использовать заголовок "В наличии". Кроме того, допустим, мы хотим назвать столбец Car ID как "Номер", a PetName — как "Название". Для этого перед вызовом метода Fill () объекта адаптера данных нужно добавить следующий код (и еще надо импортировать пространство имен System.Data.Common, чтобы иметь определение типа DataTableMapping): static void Main(string [ ] args) { // Соответствие имен столбцов базы данных и понятных названий. DataTableMapping custMap = dAdapt.TableMappings.Add("Inventory", "В наличии"); custMap.ColumnMappings.Add("CarlD", "Номер"); custMap.ColumnMappings.Add("PetName", "Название"); dAdapt.Fill(myDS, "Inventory"); } Если еще раз запустить эту программу, то теперь метод PrintDataSetO будет выводить дружественные имена объектов DataTable и Data Row, а не имена из схемы в базе данных: ***** работа с адаптерами данных ***** Имя DataSet: AutoLot => Таблица В наличии: Номер 83 107 678 904 1000 1001 1992 2003 Make Ford Ford Yugo VW BMW BMW Saab Yugo Color Rust Red Green Black Black Tan Pink Rust Название Rusty Snake Clunker Hank Bimmer Daisy Pinkey Mel Исходный код. Проект FillDataSetUsingSqlDataAdapter доступен в подкаталоге Chapter 22. Добавление BAutoLotDAL.dll возможности отключения Чтобы продемонстрировать применение адаптера данных для внесения модификаций из DataTable обратно в базу данных, мы сейчас изменим сборку AutoLotDAL.dll, созданную в главе 21, чтобы она содержала новое пространство имен (по имени AutoLotDisconnectedLayer). Это пространство имен будет содержать новый класс InventoryDALDisLayer, который применяет адаптер данных для взаимодействия с объектом DataTable.
Глава 22. ADO.NET, часть II: автономный уровень 831 Для начала лучше скопировать всю папку проекта AutoLot, который был создан в главе 21, в новое место на жестком диске и переименовать ее в AutoLot (Version Two). Теперь запустите Visual Studio 2010, выберите пункт меню File^Open Project/Solution... (Файл1^ Открыть проект/решение...) и откройте файл AutoLotDAL. sin из папки AutoLot (Version Two). Определение начального класса С помощью пункта меню Projects Add Class (Проекте Добавить класс) добавьте в новый кодовый файл новый класс InventoryDALDisLayer. Этот новый класс должен иметь тип public. Измените имя пространства имен, в которое упакован этот класс, на AutoLotDisconnectedLayer и импортируйте пространства имен System.Data и System.Data.SqlClient. В отличие от типа InventoryDAL, ориентированного на работу с подключением, этому новому классу не нужны специальные методы открытия и закрытия, т.к. адаптер данных выполнит всю необходимую обработку автоматически. Сначала добавьте собственный конструктор, который заносит значение строки подключения в переменную string. Кроме того, определите приватную переменную-член SqlDataAdapter, которая будет настраиваться с помощью (пока еще не созданного) вспомогательного метода ConfigureAdapter(), который принимает выходной параметр SqlDataAdapter: namespace AutoLotDisconnectedLayer { public class InventoryDALDisLayer { // Значения полей. private string cnString = string.Empty; private SqlDataAdapter dAdapt = null; public InventoryDALDisLayer(string connectionString) { cnString = connectionString; // Настройка SqlDataAdapter. ConfigureAdapter(out dAdapt); } } } Настройка адаптера данных с помощью SqlCommandBuilder При использовании адаптера данных для модификации таблиц в наборе данных DataSet вначале нужно указать в свойствах UpdateCommand, DeleteCommand и InsertCommand допустимые объекты команд (до этого они содержат значения null). Для ручного конфигурирования объектов команд для свойств InsertCommand, UpdateCommand и DeleteCommand может понадобиться серьезный объем кода, особенно если применяются параметризованные запросы. Вспомните: в главе 21 было сказано, что параметризованные запросы позволяют создавать SQL-операторы с помощью объектов параметров. И если мы готовимся к серьезной работе, то можем реализовать метод ConfigureAdapter() для ручного создания трех новых объектов SqlCommand, каждый из которых содержит набор объектов SqlParameter. Потом эти объекты можно указать в свойствах адаптера UpdateCommand, DeleteCommand и InsertCommand. В Visual Studio 2010 имеется целый ряд средств проектирования, которые берут на себя хлопоты по составлению этого утомительного участка кода. Эти средства немного отличаются в зависимости от используемого API (Windows Forms, WPF или ASP.NET), но
832 Часть V. Введение в библиотеки базовых классов .NET их общие возможности очень похожи. Действие некоторых из них неоднократно будет продемонстрировано в этой книге, в том числе и на примере конструкторов Windows Forms ниже в данной главе. Вам не понадобится писать многочисленные операторы кода, чтобы полностью сконфигурировать адаптер данных; вместо этого мы существенно снизим объем работы, реализовав метод ConfigureAdapter () следующим образом: private void ConfigureAdapter (out SqlDataAdapter dAdapt) { // Создание адаптера и заполнение SelectCommand. dAdapt = new SqlDataAdapter("Select * From Inventory", cnString); // Динамическое получение остальных объектов команд //во время выполнения с помощью SqlCommandBuilder. SqlCommandBuilder builder = new SqlCommandBuilder(dAdapt); } Для упрощения создания объектов адаптеров данных в каждом поставщике данных ADO.NET, разработанном Microsoft, имеется тип построителя команд (command builder) — SqlCommandBuilder. Он автоматически генерирует значения в свойствах InsertCommand, UpdateCommand и DeleteCommand объекта SqlDataAdapter на основе первоначального объекта SelectCommand. Очень удобно, что не нужно создавать вручную все типы SqlCommand и SqlParameter. Возникает очевидный вопрос: как может построитель команд создать все эти объекты команд на ходу? Краткий ответ на этот вопрос — метаданные. Когда во время выполнения вызывается метод Update () адаптера данных, соответствующий построитель команд читает информацию схемы из базы данных, чтобы автоматически генерировать нужные объекты команд вставки, удаления и изменения. Конечно, для этого нужны дополнительные обращения к удаленной базе данных, а при неоднократном использовании SqlCommandBuilder в одном приложении его производительность снизится. Здесь мы постараемся минимизировать негативный эффект с помощью вызова метода Configure Ad ар ter() во время создания объекта InventoryDALDisLayer и сохранения настроенного SqlDataAdapter для использования на протяжении всего времени жизни объекта. В приведенном выше коде объект построителя команд (SqlCommandBuilder) служил только для передачи в объект адаптера данных в качестве параметра конструктора. Как ни странно, это все, что нам нужно с ним сделать (в минимальном варианте). "За кулисами" этот тип выполняет конфигурирование адаптера данных остальными объектами команд. Каждому нравится получить что-то, не прикладывая усилий, но надо учитывать, что построители команд накладывают серьезные ограничения. А именно, построитель команд может автоматически генерировать команды SQL для использования их адаптером данных, если выполнены все следующие условия: • SQL-команда Select работает только с одной таблицей (т.е. без объединений): • у этой единственной таблицы имеется первичный ключ; • в таблице должен быть столбец (или столбцы), который представляет первичный ключ, включенный в SQL-оператор Select. Учитывая способ построения базы данных AutoLot, эти ограничения не доставляют никаких проблем. Но в более реальных базах данных нужно подумать, может ли оказаться полезным этот тип вообще (а если нет, учтите, что Visual Studio 2010 автоматически генерирует большую часть необходимого кода — это будет показано в конце настоящей главы).
Глава 22. ADO.NET, часть II: автономный уровень 833 Реализация метода GetAllInventoryQ Теперь наш адаптер данных готов к применению. Первый метод нового класса будет просто вызывать метод Fill() объекта SqlDataAdapter для получения DataTable, представляющего все записи из таблицы Inventory базы данных AutoLot: public DataTable GetAllInventory () { DataTable inv = new DataTable("Inventory"); dAdapu.Fill(inv); return inv; } Реализация метода UpdatelnventoryQ Метод UpdatelnventoryO очень прост: public void Updatelnventory(DataTable modifledTable) { dAdapt.Update(modifledTable); 1 Здесь объект адаптера данных проверяет значение RowState у каждой строки входной таблицы. В зависимости от его значения (RowState.Added, RowState.Deleted или RowState.Modified) автоматически вызывается нужный объект команды. Установка номера версии Замечательно! Логика второй версии нашей библиотеки доступа к данным завершена. Хотя это и необязательно, все же для порядка установите для этой библиотеки номер версии 2.0.0.0. Как описано в главе 14, чтобы изменить версию сборки .NET, дважды щелкните на узле Properties (Свойства) в Solution Explorer, а затем щелкните на кнопке Assembly Information... (Информация о сборке), которая находится на вкладке Application (Приложение). В открывшемся диалоговом окне укажите старший номер версии сборки равным 2 (подробнее см. в главе 14). После этого перекомпилируйте приложение, чтобы обновить информацию о сборке. Исходный код. Проект AutoLot DAL (Version 2) доступен в подкаталоге Chapter 22. Тестирование автономной функциональности Теперь у нас есть все для создания клиентского интерфейса, который позволит проверить работу нового класса Invent or yDALDisLayer. Здесь мы опять воспользуемся Windows Forms API для вывода данных в графическом пользовательском интерфейсе. Создайте новое приложение Windows Forms с именем InventoryDALDisconnectedGUI и измените в Solution Explorer первоначальное имя файла Forml.csHaMainForm.cs. После создания нового проекта вставьте в него ссылку на измененную сборку AutoLotDAL.dll (не забудьте выбрать версию 2.0.0.0!) и импортируйте следующее пространство имен: using AutoLotDisconnectedLayer; Форма приложения содержит элементы Label, DataGridView (inventoryGrid) и Button (btnUpdatelnventory), причем для кнопки должен быть создан обработчик события Click. Ниже приведено определение формы: public partial class MainForm : Form { InventoryDALDisLayer dal = null;
834 Часть V. Введение в библиотеки базовых классов .NET public MainForm() { InitializeComponent(); string cnStr = @"Data Source=(local)\SQLEXPRESS;Initial Catalog=AutoLot;" + "Integrated Security=True;Pooling=False"; // Создание объекта доступа к данным. dal = new InventoryDALDisLayer(cnStr); // Заполнение графической таблицы! inventoryGrid.DataSource = dal.GetAllInventory(); } private void btnUpdateInventory_Click(object sender, EventArgs e) { // Получение измененных данных из графической таблицы. DataTable changedDT = (DataTable)inventoryGrid.DataSource; try { // Внесение изменений. dal.Updatelnventory(changedDT); } catch(Exception ex) { MessageBox.Show(ex.Message) ; } } } После создания объекта InventoryDALDisLayer можно выполнить привязку DataTable, возвращенного вызовом GetAllInventory(), к объекту DataGridView. Когда конечный пользователь щелкнет на кнопке Update (Обновить), из графической таблицы выбирается модифицированный DataTable (с помощью свойства DataSource) и передается в метод Updatelnventory (). Вот и все! Запустите это приложение, добавьте в графическую таблицу несколько новых строк и измените и/или удалите несколько других. После щелчка на кнопке Update все изменения будут сохранены в базе данных AutoLot. Исходный код. Проект WindowsFormsInventoryUI доступен в подкаталоге Chapter 22. Объекты DataSet для нескольких таблиц и взаимосвязь данных Пока все примеры данной главы оперировали с одним объектом DataTable. Но вся мощь автономного уровня проявляется тогда, когда объект DataSet содержит несколько взаимосвязанных DataTable. В этом случае в коллекцию DataRelation данного DataSet можно вставить любое количество объектов DataRelation, которые описывают все взаимосвязи таблиц. Эти объекты позволяют клиентскому уровню выполнять навигацию между данными таблиц без обращения к сети. На заметку! Вместо еще одного изменения сборки AutoLotDAL.dll, позволяющего работать с таблицами Customers и Orders, в этом примере вся логика доступа к данным изолируется в новом проекте Windows Forms. Разумеется, в производственном приложении смешивание пользовательского интерфейса и логики обработки данных не рекомендуется. В последних примерах данной главы применяются различные средства проектирования баз данных, которые позволяют отделить код пользовательского интерфейса от логики обработки данных.
Глава 22. ADO.NET, часть II: автономный уровень 835 Начните этот пример с создания нового приложения Windows Forms по имени MultitabledDataSetApp. Графический интерфейс приложения максимально прост. На рис. 22.11 показаны три элемента DataGridView (dataGridViewInventory, dataGridViewCustomers и dataGridViewOrders) для данных из таблиц Inventory, Orders и Customers базы данных AutoLot. Кроме того, имеется кнопка Button (btnUpdateDatabase) для отправки всех изменений, проведенных в графических таблицах, обратно в базу данных для обработки с помощью объектов адаптеров данных. MewvForm с* [Design]4 X Cinvn! ".-:: wi jmm* 0 ta — Рис. 22.11. В первоначальном пользовательском интерфейсе выводятся данные из всех таблиц базы данных AutoLot Подготовка адаптеров данных Для максимального упрощения кода доступа к данным в типе Main Form будут использоваться объекты построителей команд, которые будут автоматически генерировать команды SQL для каждого из трех объектов SqlDataAdapter (по одному для каждой таблицы). Вот первая итерация нашего типа, производного от Form (не забудьте импортировать пространство имен System.Data.SqlClient): public partial class HainForm : Form // Формирование широкого DataSet. private DataSet autoLotDS = new DataSet("AutoLot"); // Использование построителей команд для упрощения настройки адаптера данных. private SqlCommandBuilder sqlCBInventory; private SqlCommandBuilder sqlCBCustomers; private SqlCommandBuilder sqlCBOrders; // Адаптеры данных (для каждой таблицы). private SqlDataAdapter invTableAdapter; private SqlDataAdapter custTableAdapter; private SqlDataAdapter ordersTableAdapter; // Формирование строки подключения. private string cnStr = string.Empty;
836 Часть V. Введение в библиотеки базовых классов .NET Конструктор выполняет всю утомительную работу по созданию переменных-членов, относящихся к обработке данных, и заполнению DataSet. Здесь предполагается, что вы уже создали файл App.config, содержащий соответствующую строку подключения (и, следовательно, вставили ссылку на System.Configuration.dll и импортировали пространство имен System.Configuration): <configuration> <connectionStrings> <add name ="AutoLotSglProvider11 connectionString = "Data Source=(local)\SQLEXPRESS; Integrated Security=SSPI;Initial Catalog=AutoLot" /> </connectionStrings> </configuration> Кроме того, добавляется вызов приватной вспомогательной функции BuildTable Relationship(): public MainFormO { InitializeComponent(); / / Получение строки подключения из файла *.config. cnStr = ConfiguratlonManager.ConnectlonStrings ["AutoLotSglProvider"].ConnectionString; // Создание адаптеров. invTableAdapter = new SglDataAdapter("Select * from Inventory", cnStr); custTableAdapter = new SglDataAdapter("Select * from Customers", cnStr); ordersTableAdapter = new SglDataAdapter("Select * from Orders", cnStr); // Генерация команд. sglCBInventory = new SqlCommandBuilder(invTableAdapter); sglCBOrders = new SqlCommandBuilder(ordersTableAdapter); sglCBCustomers = new SqlCommandBuilder(custTableAdapter) ; // Добавление таблиц в DataSet. invTableAdapter.Fill(autoLotDS, "Inventory"); custTableAdapter.Fill(autoLotDS, "Customers"); ordersTableAdapter.Fill(autoLotDS, "Orders"); // Создание отношений между таблицами. BuildTableRelationship(); // Привязка к графическим элементам dataGridViewInventory.DataSource = autoLotDS.Tables["Inventory"]; dataGridViewCustomers.DataSource = autoLotDS.Tables["Customers" ] ; dataGridViewOrders.DataSource = autoLotDS.Tables["Orders" ] ; Создание отношений между таблицами Вспомогательная функция BuildTableRelationship () берет на себя рутинные действия по добавлению в объект autoLotDS двух объектов DataRelation. В главе 21 было сказано, что в базе данных AutoLot имеется ряд отношений "родительский-дочерний", и они учтены в следующем коде: private void BuildTableRelationship () { // Создание объекта отношения между данными CustomerOrder. DataRelation dr = new DataRelation("CustomerOrder",
Глава 22. ADO.NET, часть II: автономный уровень 837 autoLotDS.Tables["Customers"].Columns["CustlD"], autoLotDS.Tables["Orders"].Columns["CustlD"]); autoLotDS.Relations.Add(dr); // Создание объекта отношения между данными InventoryOrder. dr = new DataRelation("InventoryOrder", autoLotDS.Tables["Inventory"].Columns["CarID"], autoLotDS.Tables["Orders"].Columns["CarlD"]); autoLotDS.Relations.Add(dr); } Здесь при создании объекта DataRelation в первом параметре указывается более понятный строковый идентификатор (вскоре мы покажем, как это можно использовать). Кроме того, задаются ключи для создания самого отношения. Обратите внимание, что сначала указывается родительская таблица (второй параметр конструктора), а затем дочерняя (третий параметр конструктора). Изменение таблиц базы данных После заполнения объекта DataSet и отключения его от источника данных можно локально работать с каждым DataTable. Для этого нужно запустить приложение и вставлять, изменять или удалять значения в любых элементах DataGridView. Когда понадобится отправить данные в базу, щелкните на кнопке Update Database (Обновить базу данных). Теперь уже должен быть понятен код обработки события Click: private void btnUpdateDatabase_Click (object sender, EventArgs e) { try { invTableAdapter.Update(carsDS, "Inventory"); custTableAdapter.Update(carsDS, "Customers"); ordersTableAdapter.Update(carsDS, "Orders"); } catch (Exception ex) { MessageBox.Show(ex.Message); } } Запустите приложение и внесите различные изменения в данные. При следующем запуске приложения вы увидите, что графические таблицы содержат все последние изменения. Переходы между взаимосвязанными таблицами Теперь посмотрим, как объекты DataRelation позволяют программно переходить между взаимосвязанными таблицами. Добавьте в графический интерфейс новый элемент Button (btnGetOrderlnfo), соответствующий TextBox (txtCustID) и Label с подходящим текстом (для большей понятности эти элементы можно сгруппировать в GroupBox). На рис. 22.12 показан один из возможных вариантов графического интерфейса рассматриваемого приложения. Этот измененный интерфейс позволяет пользователю ввести идентификатор клиента и получить всю информацию о заказе этого клиента (имя, номер заказа, автомобиль). Эта информация форматируется в виде строки string, а затем выводится в окне сообщения.
838 Часть V. Введение в библиотеки базовых классов .NET Рис. 22.12. Измененный интерфейс позволяет пользователю выполнять поиск информации о заказах клиента Вот код обработчика события Click только что добавленной кнопки: private void btnGetOrderlnfo_Click(object sender, System.EventArgs e) { string strOrderlnfo = string.Empty; DataRow[] drsCust = null; DataRow[] drsOrder = null; // Получение идентификатора клиента из текстового поля. int custID = int.Parse(this.txtCustlD.Text); // Поиск строки в таблице Customers для введенного идентификатора клиента. drsCust = autoLotDS.Tables["Customers"] .Select ( string.Format("CustID = {0}", custID)); strOrderlnfo += string.Format("Customer {0}: {1} {2}\n", drsCust[0] ["CustID"] .ToStringO , drsCust [0] ["FirstName"] .ToString (), drsCust [0] ["LastName"] .ToString ()); // Переход из таблицы Customers к таблице Orders. drsOrder = drsCust[0].GetChildRows(autoLotDS.Relations["CustomerOrder"]); // Получение номера заказа. foreach (DataRow r in drsOrder) strOrderlnfo += string.Format("Order Number: {0}\n", r["OrderlD"]); //А теперь переход из таблицы Orders к таблице Inventory. DataRow[] drslnv = drsOrder[0] .GetParentRows(autoLotDS.Relations["InventoryOrder"]) ; // Перебор всех заказов данного клиента. foreach (DataRow order in drslnv) { strOrderlnfo += string.Format(" \nOrder Number: {0}\n", order["OrderlD"]); // Выборка автомобиля, упоминаемого в данном заказе. DataRow[] drslnv = order.GetParentRows(autoLotDS.Relations[ "InventoryOrder"]); // Получение информации для этого (ОДНОГО) автомобиля. DataRow car = drslnv[0]; strOrderlnfo += string.Format("Make: {0}\n", r["Make"]); // Марка
Глава 22. ADO.NET, часть II: автономный уровень 839 strOrderlnfо += string.Format("Color : {0}\n", r["Color"]); //Цвет strOrderlnfo += string.Format("Pet Name: {0}\n", r["PetName"]); / / Дружественное имя } MessageBox.Show(strOrderlnfo, "Order Details"); // Информация о заказе На рис. 22.13 показан один из возможных результатов работы, когда указан идентификатор клиента 3 (в моей базе данных AutoLot это Стив Хаген, у которого имеются два ожидающих обработки заказа). £ Current invent,;I, ' CariO [107 ИУУ> Current Customers j CmtID \2 C Current Orders j OrderlD 'jiooi 11002 Lookup Customer Order Customer ID 3 ( GetO pubitoi Make I Ford i Ford IW7 RrstNdroe ■ Dave Matt | Steve CustiD |l \2 j3 aer Details Color Rust Red P. .ml. Last Name Brenner Walton Hagen CarlD 1000 678 . PetName ] Rusty Customer 3: Steve Hagen j» Order f'4umben 1002 ■ MakeVW Color. Black Pet Name: Hank [ OK ] I J 1 ] Рис. 22.13. Навигация с помощью отношений между данными Надеюсь, последний пример убедил вас в пользе типа DataSet. При полном отсутствии связи DataSet с первоначальным источником данных вы можете работать с находящейся в памяти копией данных, переходить от таблицы к таблице и вносить необходимые изменения, удаления или вставки. После этого можно отправить измененные данные обратно в хранилище. В результате получается очень масштабируемое и надежное приложение. Исходный код. Проект MultitabledDataSetApp доступен в подкаталоге Chapter 22. Средства конструктора баз данных в Windows Forms До сих пор все приведенные примеры содержали ощутимый объем "грязной работы": всю логику доступа к данным мы писали вручную. И хотя значительный объем этого кода был вынесен в библиотеку .NET (AutoLotDAL.dll) для использования в последующих главах книги, нам все равно приходится вручную создавать различные объекты поставщика данных, чтобы можно было взаимодействовать с реляционной базой данных. Теперь мы научимся использовать средства конструктора баз данных Windows Forms, которые могут создать за вас значительный объем кода доступа к данным.
840 Часть V. Введение в библиотеки базовых классов .NET На заметку! Для создания веб-проектов с помощью Windows Presentation Foundation и ASP.NET имеются аналогичные средства, с которыми вы познакомитесь ниже в данной главе. Одним из способов применения этих интегрированных средств является использование конструктор, поддерживаемых элементом DataGridView из Windows Forms. Но тогда средства конструктора баз данных вставят весь код доступа к данным непосредственно в кодовую базу GUI! Лучше всего изолировать весь этот код, сгенерированный конструктором, в специальную кодовую библиотеку .NET, чтобы повторно использовать полученную логику доступа к данным в различных проектах. Однако полезно начать с рассмотрения, как можно с помощью элемента DataGridView сгенерировать код доступа к данным, т.к. такой подход все-таки может быть полезен в небольших проектах и прототипах приложений. А затем мы научимся изолировать этот сгенерированный код в третьей версии библиотеки AutoLot.dll. Визуальное проектирование элементов DataGridView У элемента DataGridView имеется связанный с ним мастер, который может генерировать код доступа к данным. Для начала создайте новый проект приложения Windows Forms по имени Da taGridViewDat a Designer. Переименуйте с помощью Solution Explorer первоначальную форму в MainForm.cs и добавьте на нее экземпляр элемента DataGridView (с именем inventoryDataGridView). При этом справа от элемента открывается текстовый редактор. В раскрывающемся списке Choose Data Source (Выберите источник данных) выберите пункт Add Project Data Source (Добавить источник данных для проекта), как показано на рис. 22.14. MatnForm.es [Design}" X ■кг* Windows Forms Data Wizards Рис. 22.14. Редактор DataGridView После этого запустится мастер конфигурирования источников данных (Data Source Configuration Wizard). Он проведет вас через последовательность шагов, позволяющих выбрать и настроить источник данных, который затем будет привязан к DataGridView. На первом шаге мастер просто спрашивает тип источника данных, с которым вы хотели бы взаимодействовать. Выберите вариант Database (База данных) (рис. 22.15) и щелкните на кнопке Next (Далее). На следующем шаге (который зависит от выбора, сделанного на первом шаге) мастер спрашивает, нужно ли использовать модель базы данных DataSet или модель данных Entity. Выберите модель базы данных DataSet (рис. 22.16), т.к. мы еще не знакомы с технологией Entity Framework (о ней будет рассказано в следующей главе).
Глава 22. ADO.NET, часть II: автономный уровень 841 Choose a Data Source Type .Where will the application get data from? Database j Service Object SharePoint Lets ycu connect tc a database and choose the database objects for your application. Рис. 22.15. Выбор типа источника данных Data Source Configuration Wizard Choose a Database Model What type of database model do you want to use? u k _^ Entity Data "EJT Model The database model you choose determines the types of data objects your application code uses. A dataset file will be added to your project, Рис. 22.16. Выбор модели базы данных Следующий шаг позволяет настроить подключение к базе данных. Если эта база данных уже добавлена в Server Explorer, то она будет присутствовать в раскрывающемся списке. А если ее там нет (например, если она еще не добавлена в Server Explorer), щелкните на кнопке New Connection (Новое подключение). Результат выбора локального экземпляра базы данных AutoLot показан на рис. 22.17 (обратите внимание, что при этом генерируется нужная строка подключения). Последний шаг мастера позволяет выбрать объекты базы данных, которые будут фигурировать в автоматически сгенерированном DataSet и соответствующих адаптерах данных. В принципе можно выбрать все объекты данных из базы AutoLot, но нам здесь понадобится только таблица Inventory. Поэтому измените предлагаемое имя DataSet на InventoryDataSet (рис. 22.18), отметьте таблицу Inventory и щелкните на кнопке Finish (ГЪтово).
842 Часть V. Введение в библиотеки базовых классов .NET ^JgD Choose Your Data Connection Which data connection should your application use to connect to the database? | win-bфЫehfvOv\^qlexpreи.ДutoLotdbo T Jr.. Connection... Gl Connection string Data Sources local) \SQLEXPRESS;Jnitial Cetalog=AutoLot;lntegrated Security=True.Pocling=Faise .: p,e^c Caned Рис. 22.17. Выбор базы данных Data Source Configuration Wizard i Щ2щ Щ Choose Your Database Objects ■ЩР Which database objects do you want in your dataset? f ' ■>£ T*le, _J CreditRisks . _3 Customers ' У _] ln\ enter.- Й23 CarlD [^ BH Make , Й1Э Color SHU PetName f ИЗ Orders Etfjj Vievw | •* L *!> Stored Procedures □ 3 GetPetName 1 Ej14 Functions Enable local database caching DataSet name: InventoryDataSet 1 < Previous i Finish J j Cancel Рис. 22.18. Выбор таблицы Inventory После этого визуальный конструктор изменится сразу во многих отношениях. Наиболее заметно то, что в элементе DataGridVieu отображается схема таблицы Inventory, т.е. появились соответствующие заголовки столбцов. Кроме того, в нижней части конструктора формы (который называется "component tray" — "лоток с компонентами") имеются три компонента: DataSet, BindingSource и TableAdapter (рис. 22.19). Сейчас уже можно запустить приложение — и вы увидите, что графическая таблица сразу заполнена записями из таблицы Inventory. Конечно, тут нет никакого волшебства. Среда разработки создала за вас значительный объем кода и настроила элемент графической таблицы для его использования. Давайте рассмотрим этот сгенерированный код.
Глава 22. ADO.NET, часть II: автономный уровень 843 MatnForm.cs [Design]" Щ Windows Forms Data Wizards Шш&Г[ •ntoryOataSet ™ inventoryBindingSource Щ inventoryTableAdapter Tasks Choose Data Source Edit Columns- Add Column... H Enable Adding H Enable Editing V; Enable Deleting П Enable Column Reordering Dock in Parent Container Add Queiy... Preview Data... ■'? Рис. 22.19. Проект Windows Forms после отработки мастера конфигурирования источников данных Сгенерированный файл app.config Если посмотреть на проект в Solution Explorer, легко заметить, что он уже содержит файл app.config. В нем имеется элемент <connectionStrings> с несколько необычным именем: <?xml version="l .0" encoding="utf-811 ?> <configuration> <configSections> </configSections> <connectionStrings> <add name="DataGridViewDataDesigner. Properties. Settings . AutoLotConnectionString" connectionString= "Data Source=(local)\SQLEXPRESS; Initial Catalog=AutoLot; Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration> В качестве имени объекта адаптера данных (о котором будет рассказано ниже) взято длинное значение "DataGridViewDataDesigner.Properties.Settings. AutoLotConnectionString". Анализ строго типизированного DataSet Кроме конфигурационного файла, IDE генерирует так называемый строго типизированный DataSet. Этим термином обозначается пользовательский класс, который расширяет базовый DataSet и содержит ряд членов, позволяющих взаимодействовать с базой данных с помощью более наглядной объектной модели. Строго типизированные объекты DataSet содержат свойства, непосредственно отображаемые на имена таблиц из базы данных. Например, можно использовать свойство Inventory для прямого обращения к строкам и столбцам базы, не путаясь в коллекции таблиц Tables. Вставьте в проект новый файл диаграммы классов, выбрав в Solution Explorer значок проекта и щелкнув на кнопке View Class Diagram (Просмотр диаграммы классов). Обратите внимание, что на основе введенной информации мастер создал новый тип DataSet по имени InventoryDataSet. В этом классе определен ряд членов, важнейшим из которых является свойство Inventory (рис. 22.20).
844 Часть V. Введение в библиотеки базовых классов .NET Если в Solution Explorer дважды щелкнуть на файле InventoryDataSet.xsd, то загрузится конструктор наборов данных (Dataset Designer) Visual Studio 2010 (подробнее он рассматривается чуть ниже). Если щелкнуть правой кнопкой мыши в любом месте этого конструктора и выбрать в контекстном меню пункт View Code (Просмотреть код), то вы увидите совершенно пустое определение частичного класса: public partial class InventoryDataSet { } При необходимости в это определение частичного класса можно добавить дополнительные члены. Однако все действие происходит в сопровождаемом конструктором файле InventoryDataSet.Designer.cs. Если открыть этот файл в Solution Explorer, то можно увидеть, что тип InventoryDataSet расширяет класс DataSet. Рассмотрим следующий фрагмент кода с добавленными для ясности комментариями: // Весь этот код сгенерирован конструктором! public partial class InventoryDataSet : global::System.Data.DataSet { // Переменная-член типа InventoryDataTable. private InventoryDataTable tablelnventory; // Каждый конструктор вызывает вспомогательный метод InitClass(). public InventoryDataSet() { this.InitClass (); } // InitClass() подготавливает DataSet и добавляет // InventoryDataTable в коллекцию Tables. private void InitClass() { this.DataSetName = "InventoryDataSet"; this.Prefix = ""; this.Namespace = "http://tempuri.org/InventoryDataSet.xsd"; this.EnforceConstraints = true; this.SchemaSerializationMode = global::System.Data.SchemaSerializationMode.IncludeSchema; this.tablelnventory = new InventoryDataTable(); base.Tables.Add(this.tablelnventory); } // Свойство Inventory (только для чтения) возвращает переменную-член InventoryDataTable. public InventoryDataTable Inventory { get { return this.tablelnventory; } } } В этом строго типизированном DataSet имеется переменная-член, которая является строго типизированным DataTable — в данном случае это класс InventoryDataTable. Конструктор строго типизированного класса DataSet вызывает приватный метод инициализации InitClass (), который добавляет экземпляр этого строго типизированного DataTable в коллекцию Tables объекта DataSet. И еще один важный момент: реализация свойства Inventory возвращает переменную-член InventoryDataTable. | InventoryDataSet ® I Class | -fc DataSet * Fields s Properties Wr SchemaSerializationMode Ш Tables Я Methods Э Nested Types Рис. 22.20. Строго типизированный DataSet, созданный мастером конфигурирования источников данных
Глава 22. ADO.NET, часть II: автономный уровень 845 Анализ строго типизированного DataTable Теперь вернитесь к файлу диаграммы классов и откройте узел Nested Types (Вложенные типы) у значка InventoryDataSet. Здесь имеются строго типизированный класс InventoryDataTable и строго типизированный класс DataRow. В классе InventoryDataTable (имеющем тот же тип, что и только что рассмотренная переменная-член строго типизированного DataSet) определен набор свойств, основанных на именах столбцов физической таблицы Inventory (CarlDColumn, ColorColumn, MakeColumn и PetNameColumn), а также специальный индексатор и свойство Count для получения текущего количества записей. Но более интересно то, что в этом строго типизированном классе DataTable определен набор методов, которые позволяют вставлять, находить и удалять строки в таблице с помощью строго типизированных членов (удобная альтернатива ручной навигации по индексаторам Rows и Columns). К примеру, метод AddlnventoryRowO предназначен для добавления новой строки в находящуюся в памяти таблицу, FindByCarlDO — для поиска в таблице по первичному ключу, a RemoveInventoryRow() позволяет удалить строку из строго типизированной таблицы (рис. 22.21). Анализ строго типизированного DataRow Строго типизированный класс DataRow, также вложенный в строго типизированный DataSet, расширяет класс DataRow и содержит свойства, непосредственно соотносимые со схемой таблицы Inventory. Кроме того, конструктор данных создал метод IsPetNameNullO, который проверяет, содержит ли данный столбец значение (рис. 22.22). Анализ строго типизированного адаптера данных InventoryDataTable 1*1 Class •*■ TypedTableBase<Inventorj«ow> a Fields Э Properties CarlDColumn 3^ ColorColumn j? Count ^ MakeColumn Ж PetNameColumn "^ this Q Methods ИИШ^^!|||ЯЯ1!В'1Я111ЯР,|'<Р.1» * Clone f* Createlnstance ■-♦ FindByCarlD $♦ GetRow/Type -♦ GetTypedTableSchema $Ь InitClass a* InitVars -♦ InventoryDataTable !> 2 overlo... j "♦ Ne/vInventoryRow f^ NewRowFromBuilder f* OnRowChanged f* OnRowChangmg f* On Rov* Deleted 4^ On Row Deleting ^ RemovelnventoryRow Я Events 4, J Рис. 22.21. Строго типизированный DataTable, вложенный в строго типизированный DataSet InventoryRow Class * DataRow И Fields |^ tablelnventory a Properties *j* CarlD *§? Color Й? Make Э1 PetName 8 Methods ai* InventoryRow :* IsPetNameNull -'• SetPetNameNull Получение строгой типизации для автономных типов — серьезный аргумент в пользу использования мастера конфигурирования источников данных, поскольку создание этих классов вручную — утомительное (хотя и вполне посильное) занятие. Этот мастер даже генерирует объект специального адаптера данных, который может строго типизированным образом заполнять и обновлять объекты InventoryDataSet и InventoryDataTable. Найдите в окне визуального конструктора классов класс InventoryTableAdapter и просмотрите сгенерированные для него члены (рис. 22.23). Автоматически сгенерированный тип InventoryTableAdapter содержит коллекцию объектов SqlCommand (доступ к ним возможен с помощью свойства CommandCollection), у каждого из которых имеется полностью заполненный набор объектов SqlParameter. Кроме того, этот специальный адаптер данных предоставляет набор свойств для выборки соответствующих объектов подключения, транзакций и адаптеров данных, а также свойство для получения массива, представляющего все типы команд. Рис. 22.22. Строго типизированный DataRow
846 Часть V. Введение в библиотеки базовых классов .NET Inventory Table Adapter Class ■+ Component H Fields & .adapter ф _clearBeforefill jjj^ jrcmmandCollection & .connection $& .transaction ■~ Properties Щ Adapter Ш ClearBeroreFill jfjj| ComrnandCollection щ1 Connection j|* Transaction В Methods * Delete * Fill • GetData J* InitAdapter Д* InitCommandCollection £* InitConnection •♦ Insert '♦ InventoryTableAdapter Ф Update I* 5 overloads) Рис. 22.23. Специальный адаптер данных, работающий со строго типизированными DataSet и DataTable Завершение приложения Windows Forms Если внимательно рассмотреть обработчик события Load в типе, порожденном от формы (то есть если рассмотреть код для MainForm.cs и найти метод MainFormLoadO), то можно увидеть, что метод Fill () специализированного адаптера данных вызывается в самом начале, и специальный DataSet передает ему специальный объект DataTable: private void MainForm_Load(object Gender, EventArgs e) { this.inventoryTableAdapter.Fill(this.mventoryDat ?Set.Inventory) ; Этот объект адаптера данных позволяет отрабатывать изменения, выполненные в графической таблице. Добавьте в пользовательский интерфейс вашей формы еще один элемент Button (по имени btnUpdatelnventory). Затем создайте обработчик события Click и впишите в него следующий код: private void btnUpdateInventory_Click(object sender, EventArgs e) { try { // Отправка всех изменений, выполненных в таблице // Inventory, на обработку в базу данных. this.inventoryTableAdapter.Update(this.inventor/DataSet.Inventory); } catch(Exception ex) { MessageBox.Show(ex.Message); } // Получение свежей копии для графической таблицы. this.inventoryTableAdapter.Fill(this.inventoryDataSet.Inventory) ;
Глава 22. ADO.NET, часть II: автономный уровень 847 Снова запустите приложение; добавляйте, удаляйте или изменяйте записи, отображаемые в графической таблице, а затем щелкните на кнопке Update (Обновить). При следующем запуске программы вы увидите, что все изменения присутствуют и учитываются. Данный пример позволяет увидеть, насколько полезным может быть конструктор элемента DataGridView. Он позволяет работать со строго типизированными данными и генерирует за вас большую часть логики работы с базами данных. Очевидной проблемой является то, что полученный код тесно связан с окном, которое его использует. Конечно, лучше бы такой код находился в сборке AutoLotDAL.dll (или в какой-то другой библиотеке доступа к данным). Можно подумать и о перенесении кода, сгенерированного мастером элемента DataGridView, в проект библиотеки классов, т.к. по умолчанию конструктор форм в нем отсутствует. Исходный код. Проект DataGridViewDataDesigner доступен в подкаталоге Chapter 22. Выделение строго типизированного кода работы с базами данных в библиотеку классов К счастью, активизировать средства проектирования данных Visual Studio 2010 можно в любом проекте (как с пользовательским интерфейсом, так и без), без необходимости копировать и вставлять большие фрагменты кода из одного проекта в другой. Для иллюстрации добавим в библиотеку AutoLot.dll новые возможности. Скопируйте всю папку AutoLot (Version two), созданную ранее в этой главе, в новое место на жестком диске и переименуйте ее в AutoLot (Version Three). Затем выберите в Visual Studio 2010 пункт меню File^Open Project/Solution... (Файл^Открыть проект/решение...) и откройте файл AutoLotDAL.sln из новой папки AutoLot (Version Three). Теперь вставьте в проект новый строго типизированный класс DataSet по имени AutoLotDataSet .xsd с помощью пункта меню Project^Add New Item (Проект^Добавить новый элемент), как показано на рис. 22.24. Add New Item - AutoLotOAL ] | Instated Тел.рЫел | л Visual С* Items Code Web Window Formj WPF Reporting Workflow Name: AutoLotDataSet. Sort by. Default «f. '• ф-. Database Unit Test gJH XML Schema Щ*~* ь •<*} XML File r-^l XSLTFile Ц Local Database Cache JL ADO.NtT Entity Data Model U Local Database j*'jL L1NQ to SQL Classes 1 Service-based Database <sd m Visual C* Items Visual C» Items Visual C» hems Visual C* Herns Visual C» Items Visual C« Hems Visual C* Hems Visual C» Hems Visual C# Hems Visual C» Hems Typer. Visual C* Hems A DataSet for using data application | Add fr»TM' /- • \ Д Cancel^J Рис. 22.24. Вставка нового строго типизированного DataSet
848 Часть V. Введение в библиотеки базовых классов .NET AutoLotDataSetxsd X V& QueriesTabteAdaptei Ш GetPetName (@carID, ©petNa... Рис. 22.25. Специальные строго типизированные объекты — на сей раз в проекте библиотеки классов При этом откроется пустая поверхность конструктора наборов данных. Теперь воспользуйтесь Server Explorer для подключения к нужной базе данных (у вас уже должно быть подключение к Auto Lot) и перетащите на поверхность все таблицы и хранимые процедуры, которые необходимо сгенерировать. На рис. 22.25 показаны все аспекты базы AutoLot, с которыми будет продолжаться работа (таблица Credit Risk не нужна), а также автоматически показанные взаимоотношения между ними. Просмотр сгенерированного кода Конструктор DataSet создал в точности такой же код, как и мастер DataGridView в предыдущем примере Windows Forms. Но на этот раз задействованы таблицы Inventory, Customers и Orders, а также хранимая процедура GetPetName, поэтому получилось значительно больше сгенерированных классов. Для каждой таблицы базы данных, перетащенной на поверхность конструктора, появились строго типизированные классы DataSet, DataTable, DataRow и адаптера данных. Строго типизированные классы DataSet, DataTable, DataRow помещаются в корневое пространство имен проекта AutoLot. Специализированные адаптеры таблиц находятся во вложенном пространстве имен. Есть более легкий способ просмотреть все сгенерированные типы — средство Class View (Просмотр классов), которое открывается из меню View (Просмотр) Visual Studio (рис. 22.26). Чтобы все было по правилам, запус- Рис. 22.26. Строго типизированные классы, сге- тите редактор свойств Visual Studio 2010 нерированные для базы данных AutoLot 1 Class View » П X 1 1 U — [<Scarch> ~~ L^^^l 1 л i^j AutoLotDAL *| C> 03 Project References t> {} AutoLotConnectedLayer ' {>CJ \> *J AutoLotDataSet > ^ AutoLotDataSet.CustomersDataTable !• ^ AutoLotDataSet.CustomersRow t» ^ AutoLotDataSet.CustomersRowChangeEvent t> Л AutoLotDataSet.CustomersRcwChangeEventHandler !> *t$ AutoLotDataSetJnventoryDataTable t> ^ AutoLotDataSet.lnventoryRow l> "tj AutoLotDataSetinventoryRowChangeEvent !> ;jj|| AutoLotDataSetlnventoryRowChangeEventHandler t> fy AutoLotDataSet.OrdersDataTable :> *ty AutoLotDataSet.OrdersRow l> ^$ AutoLotDataSet.OrdersRowChangeEvent i> Л AutoLotDataSet.OrdersRowChangeEventHandler л {) AutoLotDAL AutoLotDataSetTableAdapters t> Щ% CustomersTableAdapter i> ^J InventoryTableAdapter t> 4$ OrdersTableAdapter :> ^ QueriesTableAdapter It! > -AX TableAdapterManager '-■ $t TableAdapterManager.SerfReferenceComparer ■ мг1 TableAdapterManager.UpdateOrderOption „ 1 Ш Class View КИ
Глава 22. ADO.NET, часть II: автономный уровень 849 (подробнее см. главу 14) и измените версию этой последней инкарнации AutoLot.dll на 3.0.0.0. Исходный код. Проект AutoLotDAL (Version 3) доступен в подкаталоге Chapter 22. Выборка данных с помощью сгенерированного кода Теперь полученные строго типизированные данные можно использовать в любом приложении .NET, которому необходимо взаимодействовать с базой данных AutoLot. Чтобы проверить, понимаете ли вы все основные механизмы, создайте консольное приложение с именем StronglyTypedDataSetConsoleClient. Добавьте в него ссылку на самую последнюю (и самую замечательную) версию AutoLot.dll и импортируйте в первоначальный файл с кодом на С# пространства имен AutoLotDAL и AutoLotDAL. AutoLotDataSetTableAdapters. Ниже приведен метод Main(), который использует объект InventoryTableAdapter для выборки всех данных и таблицы Inventory. В нем нет необходимости указывать строку подключения, т.к. эта информация теперь входит в состав модели строго типизированных объектов. После заполнения таблицы можно вывести результаты с помощью вспомогательного метода PrintlnventoryO. При этом со строго типизированным DataTable можно обращаться так же, как и с "обычным" DataTable — с помощью коллекций Rows и Columns: class Program { static void Main(string [ ] args) { Console.WriteLine (••***** Работа со строго типизированными DataSet *****\n"); AutoLotDataSet.InventoryDataTable table = new AutoLotDataSet.InventoryDataTable(); InventoryTableAdapter dAdapt = new InventoryTableAdapter (); dAdapt.Fill(table); Printlnventory(table); Console.ReadLine() ; } static void Printlnventory(AutoLotDataSet.InventoryDataTable dt) { // Вывод имен столбцов. for (int curCol = 0; curCol < dt.Columns.Count; curCol++) { Console.Write(dt.Columns[curCol].ColumnName + "\t"); } Console.WriteLine ("\n " ) ; // Вывод данных. for (int curRow = 0; curRow < dt.Rows.Count; curRow++) { for (int curCol = 0; curCol < dt.Columns.Count; curCol++) { Console.Write(dt.Rows[curRow] [curCol] .ToString () + "\t"); } Console.WriteLine(); } } }
850 Часть V. Введение в библиотеки базовых классов .NET Вставка данных с помощью сгенерированного кода Допустим, что теперь нужно вставить новые записи с помощью модели строго типизированных объектов. Приведенная ниже вспомогательная функция добавляет две новых строки в текущую таблицу InventoryDataTable, а затем обновляет содержимое базы данных с помощью адаптера данных. Первая строка добавляется вручную с помощью конфигурирования строго типизированного DataRow, а вторая — с помощью передачи содержимого столбцов, что позволяет создать DataRow автоматически и незаметно для программиста: public static void AddRecords(AutoLotDataSet.InventoryDataTable tb, InventoryTableAdapter dAdapt) { // Получение из таблицы новой строго типизированной строки. AutoLotDataSet.InventoryRow newRow = tb.NewInventoryRow() ; // Заполнение строки данными. newRow.CarlD = 999; newRow.Color = "Purple"; newRow.Make = "BMW"; newRow.PetName = "Saku"; // Вставка новой строки. tb.AddlnventoryRow(newRow); // Добавление еще одной строки с помощью перегруженного метода Add. tb.AddInventoryRow(888, "Yugo", "Green", "Zippy"); // Обновление базы данных. dAdapt.Update(tb) ; } Этот метод можно вызвать из метода Main (), ив таблице базы данных появятся новые записи: static void Main(string [ ] args) { // Добавление строк, обновление и повторный вывод. AddRecords(table, dAdapt); table.Clear (); dAdapt.Fill(table); Printlnventory(table); Console.ReadLine(); } Удаление данных с помощью сгенерированного кода Удаление записей с помощью модели строго типизированных объектов также не представляет трудностей. Сгенерированный метод FindByXXXXO (где ХХХХ — имя столбца с первичным ключом) строго типизированного DataTable возвращает по первичному ключу нужный (строго типизированный) DataRow. Вот еще один вспомогательный метод, который удаляет две только что созданные записи: private static void RemoveRecords(AutoLotDataSet.InventoryDataTable tb, InventoryTableAdapter dAdapt) { AutoLotDataSet.InventoryRow rowToDelete = tb.FindByCarlD(999); dAdapt.Delete(rowToDelete.CarID, rowToDelete.Make, rowToDelete.Color, rowToDelete.PetName); rowToDelete = tb.FindByCarlD(888); dAdapt.Delete(rowToDelete.CarlD, rowToDelete.Make, rowToDelete.Color, rowToDelete.PetName); }
Глава 22. ADO.NET, часть II: автономный уровень 851 Если вызвать его из метода Main () и снова вывести содержимое таблицы, то вы увидите, что две новые тестовые записи уже отсутствуют. На заметку! Если запустить это приложение второй раз с тем же методом AddRecordO, то возникнет ошибка VIOLATION CONSTRAINT ERROR, т.к. метод AddRecordO оба раза пытается вставить одно и то же значение первичного ключа CardlD. Если нужна большая гибкость, понадобится запрашивать данные у пользователя Вызов хранимой процедуры с помощью сгенерированного кода Рассмотрим еще один пример применения модели строго типизированных объектов. Создадим последний метод, который вызывает хранимую процедуру GetPetName. При создании адаптеров данных для базы данных AutoLot был создан специальный класс QueriesTableAdapter, который как раз и содержит вызов процедур, хранимых в реляционной базе данных. Вот последняя вспомогательная функция, которая выводит название указанного автомобиля при вызове из Main(): public static void CallStoredProc () { QueriesTableAdapter q = new QueriesTableAdapter(); Console.Write("Введите идентификатор автомобиля: "); string carlD = Console.ReadLine(); string carName = ""; q.GetPetName(int.Parse(carlD), ref carName); Console.WriteLine("Имя CarlD {0} - {1}", carlD, carName); Теперь вы умеете использовать строго типизированные объекты базы данных и упаковывать их в специальную библиотеку классов. В этой объектной модели есть и другие аспекты, с которыми можно поэкспериментировать, но вы уже знаете достаточно, чтобы самостоятельно разобраться в них. И в завершение данной главы вы научитесь применять LINQ-запросы к объекту ADO.NET DataSet. Исходный код. Проект StronglyTypedDataSetConsoleClient доступен в подкаталоге Chapter 22. Программирование с помощью LINQ to DataSet В данной главе было показано, что данные, находящиеся в объекте DataSet, можно обрабатывать тремя различными способами: • С помощью коллекций Tables, Rows и Columns • С помощью объектов чтения из таблиц данных • С помощью строго типизированных классов данных Различные индексаторы типов DataSet и DataTable позволяют взаимодействовать с содержащимися данными наглядным, но слабо типизированным способом. Вспомните, что этот запрос требует рассматривать данные как табличный набор ячеек, например: static void PrintDataWithlndxers(DataTable dt) { // Вывод содержимого DataTable. for (int curRow = 0; curRow ^ dt.Rows.Count; curRow++) { for (int curCol = 0; curCol < dt.Columns.Count; curCol++) { Console.Write(dt.Rows[curRow] [curCol] .ToString () + "\t"); }
852 Часть V. Введение в библиотеки базовых классов .NET Console.WriteLine(); } } Метод CreateDataReaderO типа DataTable предлагает другой подход, где данные, содержащиеся в DataSet, трактуются как линейный набор строк, которые можно обрабатывать последовательным образом. Это позволяет применять модель программирования для подключенных объектов чтения данных к автономному DataSet: static void PrintDataWithDataTableReader(DataTable dt) { // Получение объекта DataTableReader. DataTableReader dtReader = dt.CreateDataReader(); while (dtReader.Read()) { for (int i=0; l < dtReader.FieldCount; i++) { Console.Write(M{0}\t", dtReader.GetValue(i)); } Console.WriteLine(); } dtReader.Close(); } И, наконец, можно использовать строго типизированный DataSet для получения кодовой базы, которая позволяет взаимодействовать с данными, содержащимися в объекте, с помощью свойств, которые отображаются на имена столбцов в реляционной базе данных. Строго типизированные объекты позволяют написать, к примеру, следующий код: static void AddRowWithTypedDataSet () { InventoryTableAdapter invDA = new InventoryTableAdapter (); AutoLotDataSet.InventoryDataTable inv = invDA.GetData(); inv.AddInventoryRow(999, "Ford", "Yellow", "Sal"); invDA.Update(inv); } Все эти подходы вполне работоспособны, но есть еще один — LINQ to DataSet API, который предназначен для обработки данных из DataSet с помощью выражений LINQ- запросов. На заметку! LINQ to DataSet используется только для применения LINQ-запросов к объектам DataSet, которые возвращаются адаптерами данных, и это никак не связано с передачей LINQ-запросов непосредственно в механизм базы данных. В главе 23 вы познакомитесь с технологиями LINQ to Entities и ADO.NET Entity Framework, которые предоставляют способ представления SQL-запросов в виде LINQ-запросов. В исходном состоянии объект ADO.NET DataSet (и соответствующие типы наподобие DataTable и DataView) не содержат необходимую инфраструктуру для непосредственного использования в LINQ-запросах. Например, следующий метод (в котором задействовано пространство имен AutoLotDisconnectedLayer) приведет к появлению ошибки времени компиляции: static void LinqOverDataTable () { // Получение DataTable, содержащего данные. InventoryDALDisLayer dal = new InventoryDALDisLayer( @"Data Source=(local)\SQLEXPRESS;" + "Initial Catalog=AutoLot;Integrated Security=True"); DataTable data = dal.GetAllInventory();
Глава 22. ADO.NET, часть II: автономный уровень 853 // Применение LINQ-запроса к DataSet? var moreData = from с in data where (int) с [ "CarlD" ] > 5 select c; } При компиляции метода LinqOverDataTableO компилятор сообщит, что тип DataTable предоставляет реализацию шаблонов запросов. Аналогично применению LINQ-запросов к объектам, которые не реализуют интерфейс IEnumerable<T>, объекты ADO.NET необходимо преобразовать в совместимые типы. Чтобы понять, как это сделать, нужно рассмотреть типы из библиотеки System.Data.DataSetExtensions.dll. Библиотека расширений DataSet Сборка System.Data.DataSetExtensions.dll, ссылка на которую по умолчанию имеется во всех проектах Visual Studio 2010, добавляет в пространство имен System.Data ряд новых типов (рис. 22.27). Browse All Components ■'Щ rsS*" ПТ!оЯ[ л {) System.Data > *fy DataRowComparer > *ty DataRowComparer<TRow> I> *t} DataRowExtensions > ^J DataTableExtensions f> ^ EnumerableRowCollection 1 A% EnumerableRowCollectton<TRow> t> ^ EnumerableRowCollectionExtensions t> ^% OrderedEnumerableRcvvCcllecticn<TRow> t> ^TypedTableBase<T> > ^ TypedTableBaseExtensicns 1 - [ г _)- I* ^^ □ a 1 Assembly System.Data.DataSetExtensions Member of .NET Framework 4 C:\Program Files (xS6)\Reference Assemblies |\Microsoft\Framework\.NETFramework\v4,0 [\System.Data.DataSetExtensions.dll '' Рис. 22.27. Сборка System.Data.DataSetExtensions.dll Из содержащихся в библиотеке типов наиболее полезны DataTableExtensions и DataRowExtensions. Эти классы расширяют возможности классов DataTable и DataRow с помощью набора методов расширения (см. главу 12). Еще один важный класс — TypedTableBaseExtensions, определяющий методы расширения, которые можно применять к строго типизированным объектам DataSet, чтобы внутренние объекты DataTable могли взаимодействовать с LINQ. Все остальные члены сборки System.Data.DataSetExtensions.dll просто обеспечивают инфраструктуру и не предназначены для непосредственного применения в кодовой базе. Получение DataTable, совместимого с LINQ А теперь посмотрим, как можно использовать расширения DataSet. Допустим, у нас имеется новое консольное С#-приложение с именем LinqToDataSetApp. Добавьте в него ссылку на самую последнюю и самую замечательную версию C.0.0.0) сборки AutoLotDAL.dll и измените первоначальный кодовый файл, чтобы он содержал следующую логику: using System; using System.Data; // Местоположение строго типизированных контейнеров данных. using AutoLotDAL;
854 Часть V. Введение в библиотеки базовых классов .NET // Местоположение строго типизированных адаптеров данных. using AutoLotDAL.AutoLotDataSetTableAdapters; namespace LinqToDataSetApp { class Program { static void Main(string[ ] args) { Console.WriteLine ("***** LINQ через DataSet *****\n"); // Получение строго типизированного DataTable, содержащего // текущие данные таблицы Inventory из базы данных AutoLot. AutoLotDataSet dal = new AutoLotDataSet (); InventoryTableAdapter da = new InventoryTableAdapter(); AutoLotDataSet.InventoryDataTable data = da.GetData(); // Здесь будут вызываться описанные ниже методы! Console.ReadLine(); } } } Чтобы преобразовать объект ADO.NET DataTable (в том числе и строго типизированный DataTable) в объект, совместимый с LINQ, необходимо вызвать метод расширения AsEnumerable(), определенный в классе DataTableExtensions. Метод возвращает объект Enumerable RowCollection, который содержит коллекцию объектов Data Row. После этого тип EnumerableRowCollection можно использовать для обработки каждой строки с помощью основного синтаксиса DataRow (т.е. с использованием индексатора). Рассмотрим приведенный ниже новый метод из класса Program, который принимает строго типизированный DataTable, получает перечисляемую копию данных и выводит все значения Car ID: static void PrintAllCarlDs(DataTable data) { // Получение перечисляемой версии DataTable. EnumerableRowCollection enumData = data.AsEnumerable(); // Вывод идентификаторов автомобилей. foreach (DataRow r in enumData) Console.WriteLine("Car ID = {0}", r ["CarlD"]); } LINQ-запросы здесь еще не использовались, а смысл этого фрагмента в том, что к объекту enumData теперь можно применять выражения LINQ-запросов. Объект EnumerableRowCollection содержит коллекцию объектов DataRow, т.к. для вывода значений CarlD индексатор типа применяется к каждому подобъекту. В большинстве случаев нет необходимости объявлять переменную типа EnumerableRowCollection для хранения значения, возвращаемого AsEnumerable(). Этот метод можно вызывать из самого выражения запроса. А вот более интересный метод из класса Program, который получает проекцию CarlD + Makes для всех элементов из DataTable с красным цветом (если в вашей таблице Inventory нет красных автомобилей, измените LINQ-запрос): static void ShowRedCars(DataTable data) { // Проекция нового результирующего набора, содержащего // значения ID/цвет для строк, в которых Color = Red. var cars = from car in data .AsEnumerable ()
Глава 22. ADO.NET, часть II: автономный уровень 855 where (string)car["Color"] == "Red" select new { ID = (int)car["CarlD"], Make = (string)car["Make"] }; Console.WriteLine ("На складе имеются следующие красные автомобили:"); foreach (var item in cars) { Console.WriteLine("-> CarlD = {0} - {1}", item.ID, item.Make); } } Метод расширения Da taRowEx tens ions. Field<T>() Одним из неприятных аспектов текущего выражения LINQ-запроса является то, что для получения результирующего набора приходится применять различные операции приведения и индексаторы DataRow, а это может привести к исключениям времени выполнения, если будет выполнена попытка приведения для несовместимых типов. Для внесения в запрос строгой типизации можно использовать метод расширения Field<T>() типа DataRow. Он позволяет увеличить межтиповую безопасность запроса, т.к. совместимость типов данных проверяется еще на этапе компиляции. Рассмотрим следующее изменение: var cars = from car in data. AsEnumerable () where car.Field<string>("Color") == "Red" select new { ID = car.Field<int>("CarlD"), Make = car.Field<string>("Make") }; В этом случае вызывается метод Field<T>() с указанием типа параметра, который представляет соответствующий тип данных столбца. В качестве аргумента методу передается имя этого столбца. Из-за наличия этой дополнительной проверки на этапе компиляции рекомендуется при обработке элементов EnumerableRowCollection использовать метод Field<T>() (а не индексатор DataRow). Кроме вызова метода AsEnumerable(), общий формат LINQ-запроса остается в точности таким же, как и в главе 13. Поэтому мы не будем здесь повторять и объяснять различные операторы LINQ. При желании дополнительные примеры можно посмотреть в разделе "LINQ to DataSet Examples" документации по .NET Framework 4.0 SDK. Заполнение новых объектов DataTable с помощью LINQ-запросов Заполнение нового DataTable результатами LINQ-запросов выполняется легко, если не используются проекции. При наличии результирующего набора с типом, который можно представить как IEnumerable<T>, можно вызвать для результата метод расширения CopyToDataTable<T>(): static void BuildDataTableFromQuery(DataTable data) { var cars = from car in data .AsEnumerable () where car.Field<int>("CarlD") >5 select car;
856 Часть V. Введение в библиотеки базовых классов .NET // Использование этого результирующего набора для создания нового DataTable. DataTable newTable = cars.CopyToDataTable (); // Вывод содержимого DataTable. for (int curRow = 0; curRow < newTable.Rows.Count; curRow++) { for (int curCol = 0; curCol < newTable.Columns.Count; curCol++) { Console.Write(newTable.Rows[curRow] [curCol] .ToString () .Trim() + "\t"); } Console.WriteLine(); } } На заметку! Можно также преобразовать LINQ-запрос в тип DataView — с помощью метода расширения AsDataView<T>(). Эта техника может оказаться удобной, если нужно использовать результат LINQ- запроса в качестве источника для операции привязки к данным. Вспомните, что у элемента Windows Forms DataGridView (а также ASP.NET и элемента графической таблицы в WPF) имеется свойство DataSource. Привязку результата LINQ-запроса к графической таблице можно выполнить так: // Здесь myDataGrid - объект графической таблицы. myDataGrid.DataSource = (from car in data.AsEnumerable() where car.Field<int>(,,CarID") >5 select car).CopyToDataTable(); На этом рассмотрение автономного уровня ADO.NET завершено. С помощью этого аспекта API можно выбирать данные из реляционной базы данных, обрабатывать эти данные и возвращать в базу, открывая подключение к базе данных лишь на минимальный промежуток времени. Исходный код. Проект LinqOverDataSet доступен в подкаталоге Chapter 22. Резюме В данной главе был подробно рассмотрен автономный уровень ADO.NET. Как вы видели, центром всего автономного уровня является тип DataSet — размещенное в памяти представление любого количества таблиц и (возможно) любого количества отношений, ограничений и выражений. Отношения между локальными таблицами удобны тем, что позволяют осуществлять программную навигацию между ними без подключения к удаленному хранилищу данных. В этой главе была также рассмотрена роль типа адаптера данных. С помощью этого типа (и соответствующих свойств SelectCommand, InsertCommand, UpdateCommand и DeleteCommand) адаптер может приводить изменения в DataSet в соответствие с исходным хранилищем данных. Кроме того, вы научились переходить по объектной модели DataSet прямолинейным ручным способом, а также с помощью строго типизированных объектов, обычно сгенерированных средствами конструктора наборов данных из Visual Studio 2010. В конце главы был рассмотрен один аспект технологии LINQ под названием LINQ to DataSet. Он позволяет получать копию DataSet, к которой можно применять форматированные LINQ-запросы.
ГЛАВА 23 ADO.NET, часть III: Entity Framework В предыдущих двух главах рассматривались фундаментальные программные модели ADO.NET, а именно — подключенный и автономный уровни. Эти подходы позволяли программистам .NET работать с реляционными данными (в относительно прямолинейной манере) с самого первого выпуска платформы. Однако в версии .NET 3.5 Serice Pack 1 был предложен совершенно новый компонент API-интерфейса ADO.NET, который называется Entity Framework (EF). Главная цель EF заключалась в том, чтобы предоставить возможность взаимодействия с реляционными базами данных через объектную модель, которая отображается непосредственно на бизнес-объекты приложения. Например, вместо трактовки пакета данных как коллекции строк и столбцов можно оперировать коллекцией строго типизированных объектов, именуемых сущностями (entities). Эти сущности также естественным образом сочетаются с LINQ, и к ним можно выполнять запросы с использованием той же грамматики LINQ, которая была описана в главе 13. Исполняющая среда EF транслирует запросы LINQ в подходящие запросы SQL. В этой главе вы ознакомитесь с программной моделью EF Будут подробно рассматриваться различные части инфраструктуры, включая службы объектов, клиент сущностей, LINQ to Entities и Entity SQL. Также будет описан формат наиболее важного файла *.edmx и его роль в API-интерфейсе Entity Framework. Вы научитесь генерировать файлы *.edmx с помощью Visual Studio 2010 и командной строки, используя утилиту генератора EDM (EdmGen.exe). К концу главы вы построите финальную версию сборки AutoLotDal.dll и узнаете, как привязать сущностные объекты к настольному приложению Windows Forms. Роль Entity Framework Подключенный и автономный уровни ADO.NET снабжают фабрикой, которая позволяет выбирать, вставлять, обновлять и удалять данные с помощью объектов соединений, команд, чтения данных, адаптеров данных и DataSet. Хотя все это замечательно, эти аспекты ADO.NET заставляют трактовать полученные данные в манере, которая тесно связана с физической схемой данных. Вспомните, например, что при использовании подключенного уровня обычно производится итерация по каждой записи за счет указания имен столбцов объекту чтения данных. С другой стороны, в случае работы с автономным уровнем придется иметь дело с коллекциями строк и столбцов объекта DataTable внутри контейнера DataSet.
858 Часть V. Введение в библиотеки базовых классов .NET Если используется автономный уровень в сочетании со строго типизированными классами DataSet или адаптерами данных, то получаете программную абстракцию, которая предоставляет некоторые важные преимущества. Во-первых, строго типизированный класс DataSet представляет данные таблицы через свойства класса. Во-вторых, строго типизированный адаптер таблицы поддерживает методы, которые инкапсулируют конструирование лежащих в основе операторов SQL. Вспомним метод AddRecords () из главы 22: public static void AddRecords(AutoLotDataSet.InventoryDataTable tb, InventoryTableAdapter dAdapt) { // Получение из таблицы новой строго типизированной строки. AutoLotDataSet.InventoryRow newRow = tb.NewInventoryRow(); // Заполнение строки данными. newRow.CarlD = 999; newRow.Color = "Purple"; newRow.Make = "BMW"; newRow.PetName = "Saku"; // Вставка новой строки. tb.AddlnventoryRow(newRow) ; // Добавление еще одной строки с помощью перегруженного метода Add. tb. AddlnventoryRow (888, "Yugo", "Green", "Zippy"); // Обновление базы данных. dAdapt.Update(tb); } Все становится еще лучше, если скомбинировать автономный уровень с LINQ to DataSet. В этом случае можно применить запросы LINQ к находящимся в памяти данным, получить новый результирующий набор и затем дополнительно отобразить его на автономный объект, такой как DataTable, List<T>, Dictionary<K, V> или массив данных: static void BuildDataTableFromQuery(DataTable data) { var cars = from car in data.AsEnumerable () where car.Field<int>("CarlD") > 5 select car; // Использовать этот результирующий набор для построения нового объекта DataTable. DataTable newTable = cars.CopyToDataTable (); // Работать с объектом DataTable... } Несмотря на удобство интерфейса LINQ to DataSet, следует помнить, что целью запроса LINQ являются данные, возвращенные из базы данных, а не сам механизм базы данных. В идеале хотелось бы строить запрос LINQ, который отправлялся бы на обработку непосредственно базе данных, и возвращал строго типизированные данные (именно это и позволяет достичь ADO.NET Entity Framework). При использовании подключенного и автономного уровней ADO.NET всегда приходится помнить о физической структуре лежащей в основе базы данных. Необходимо знать схему каждой таблицы данных, писать сложные SQL-запросы для взаимодействия с данными таблиц и т.д. Это вынуждает писать довольно громоздкий код С#, поскольку С# существенно отличается от языка самой базы данных. Вдобавок способ конструирования физической базы данных (администратором баз данных) полностью сосредоточен на таких конструкциях базы, как внешние ключи, представления и хранимые процедуры. Сложность баз данных, спроектированных администратором, может еще более возрастать, если администратор при этом заботится о безопасности и масштабируемости. Это также усложняет код С#, который приходится писать для взаимодействия с хранилищем данных.
Глава 23. ADO.NET, часть III: Entity Framework 859 Платформа ADO.NET Entity Framework (EF) — это программная модель, которая пытается заполнить пробел между конструкциями базы данных и объектно-ориентированными конструкциями. Используя EF, можно взаимодействовать с реляционными базами данных, не имея дело с кодом SQL (при желании). Исполняющая среда EF генерирует подходящее операторы SQL, когда вы применяете запросы LINQ к строго типизированным классам. На заметку! LINQ to Entities — это термин, описывающий применение запросов LINQ к сущностным объектам ADO.NET. Другой возможный подход состоит в том, чтобы вместо обновления базы данных посредством нахождения строки, обновления строки и отправки строки обратно на обработку в пакете запросов SQL, просто изменять свойства объекта и сохранять его состояние. И в этом случае исполняющая среда EF обновляет базу данных автоматически. В Microsoft считают ADO.NET Entity Framework новым членом семейства технологий доступа к данным, и не намерены заменять им подключенный и автономный уровни. Однако после недолгого использования EF часто отдается предпочтение этой развитой объектной модели перед относительно примитивным миром SQL-запросов и коллекций строк/столбцов. Тем не менее, иногда в проектах .NET используются все три подхода, поскольку одна только модель EF чрезмерно усложняет код. Например, при построении внутреннего приложения, которому нужно взаимодействовать с единственной таблицей базы данных, подключенный уровень может применяться для запуска пакета хранимых процедур. Существенно выиграть от использования EF могут более крупные приложения, особенно если команда разработчиков уверено работает с LINQ. Как с любой новой технологией, следует знать, как (и когда) имеет смысл применять ADO.NET EF. На заметку! Вспомните, что в .NET 3.5 появился API-интерфейс программирования баз данных под названием LINQ to SQL. Он был построен на основе концепции, близкой (даже очень близкой в смысле конструкций программирования) к ADO.NET ЕЕ Хотя LINQ to SQL формально еще существует, официальное мнение в Microsoft состоит в том, что теперь следует обращать внимание на EF, а не на LINQ to SQL. Роль сущностей Строго типизированные классы, упомянутые ранее, называются сущностями (entities). Сущности — это концептуальная модель физической базы данных, которая отображается на предметную область. Формально говоря, эта модель называется моделью сущностных данных (Entity Data Model — EDM). Модель EDM представляет собой набор классов клиентской стороны, которые отображаются на физическую базу данных. Тем не менее, нужно понимать, р oq i г к что сущности вовсе не обязаны напрямую отображаться на ,„, " " -. ., пп w J^ r J r цЫ inventory базы данных схему базы данных, как может показаться, исходя из на- AUtoLot звания. Сущностные классы можно реструктурировать для соответствия существующим потребностям, и исполняющая среда EF отобразит эти уникальные имена на корректную схему базы данных. Например, вспомним простую таблицу Inventory из базы данных Auto Lot, схема которой показана на рис. 23.1. Inventory ? СагЮ Make Color PetName
860 Часть V. Введение в библиотеки базовых классов .NET Если сгенерировать EDM для таблицы Inventory базы данных AutoLot (ниже будет показано, как это делается), то по умолчанию сущность будет называться Inventory. Тем не менее, сущностный класс можно переименовать в Саг и определить для него уникально именованные свойства по своему выбору, которые будут отображены на столбцы таблицы Inventory. Такая слабая привязка означает возможность формирования сущностей так, чтобы они наиболее точно соответствовали предметной области. На рис. 23.2 показан пример сущностного класса. На заметку! Во многих случаях сущностный класс клиентской стороны называется по имени связанной с ним таблицы базы данных. Однако помните, что вы всегда можете изменить сущность для лучшего соответствия конкретной ситуации. Теперь давайте рассмотрим следующий класс Program, в котором используется сущностный класс Саг (и связанный с ним класс по имени AutoLotEntities) для добавления новой строки к таблице Inventory базы данных AutoLot. Этот класс называется контекстом объектов, и его назначение — обеспечивать взаимодействие с физической базой данных (о деталях речь пойдет ниже). class Program { static void Main(string [ ] args) { // Строка соединения автоматически читается //из сгенерированного конфигурационного файла. using (AutoLotEntities context = new AutoLotEntities ()) { // Добавить новую строку в таблицу Inventory, используя сущность Саг. context.Cars.AddObject(new Car() { AutoIDNumber = 987, CarColor = "Black", MakeOfCar = "Pinto", NicknameOfCar = "Pete" }); context.SaveChanges (); } } } Обязанность исполняющей среды EF — позаботиться о клиентском представлении таблицы Inventory (класс по имени Саг в рассматриваемом случае) и выполнить обратное отображение на корректные столбцы таблицы Inventory. Обратите внимание, что здесь нет никаких следов SQL-оператора INSERT; производится просто добавление нового объекта Саг в коллекцию, поддерживаемую соответственно названным свойством Cars в контексте объектов, после чего изменения сохраняются. Если затем заглянуть в таблицу данных с помощью проводника сервера в Visual Studio 2010, можно увидеть там добавленную новую строку (рис. 23.3). В приведенном примере нет никакой магии. "За кулисами" процесса открывается соединение с базой данных, генерируется подходящий оператор SQL и т.д. Преимущество EF состоит в том, что эти детали обрабатываются без вашего участия. Теперь давайте взглянем на базовые службы EF, которые все это делают. Строительные блоки Entity Framework API-интерфейс EF находится на вершине существующей инфраструктуры ADO.NET, которая рассматривалась в предыдущих двух главах. ^ ..... _j^ 3 Properties ff AutoIDNumber *? MakeOfCar I^JP CarColor ^ NicknameOfCar S Navigation Properties Рис. 23.2. Сущность Car — это клиентское представление схемы Inventory
Глава 23. ADO.NET, часть III: Entity Framework 861 ► 93? Pinto Black Pete 999 BMW Red FocFcc 1000 BMW Black Bimmer ||1яиан11вашнаввманаишишнаи1^1^нв ' I И 4 ;'6 Of 10 ! ► И ► \ф J Рис. 23.3. Результат сохранения контекста Подобно любому взаимодействию ADO.NET, сущностная платформа использует поставщик данных ADO.NET для взаимодействия с хранилищем данных. Однако поставщик данных должен быть обновлен, чтобы поддерживать новый набор служб, прежде чем он сможет взаимодействовать с API-интерфейсом EF. И как можно было ожидать, поставщик данных Microsoft SQL Server уже обновлен соответствующей инфраструктурой, которая полагается на использование сборки System.Data.Entity.dll. На заметку! Многие сторонние базы данных (например, Oracle и MySQL) предлагают EF- совместимые поставщики данных. Детальную информацию можно узнать у поставщика системы управления базами данных или просмотреть список известных поставщиков данных ADO.NET по адресу www.sqlsummit.com/dataprov.htm. В дополнение к добавлению необходимых компонентов к поставщику данных Microsoft SQLServer, сборка System.Data.Entity.dll содержит различные пространства имен, которые сами полагаются на службы ЕЕ Две ключевых части API-интерфейса EF, на которые следует обратить внимание сейчас — это службы объектов (object services) pi клиент сущности (entity client). Роль служб объектов Под службами объектов подразумевается часть EF, которая управляет сущностями клиентской стороны при работе с ними в коде. Службы объектов отслеживают изменения, внесенные в сущность (например, смена цвета автомобиля с зеленого на синий), управляют отношениями между сущностями (скажем, просмотр всех заказов для клиента с заданным именем), а также обеспечивают возможности сохранения изменений в базе данных и сохранение состояния сущности с помощью сериализации (XML и двоичной). С точки зрения программирования, уровень службы объектов управляет любым классом, расширяющим базовый класс EntityObject. Как и ожидалось, EntityObject представляет цепочку наследования для любых сущностных классов в программной модели EF. Например, взглянув на цепочку наследования сущностного класса Саг из предыдущего примера, можно увидеть, что Саг связан с EntityObject отношением "является" (рис. 23.4). Роль клиента сущности Вторым важным аспектом API-интерфейса EF является уровень клиента сущности. Эта часть API-интерфейса EF отвечает за работу с поставщиком данных ADO.NET для установки соединений с базой данных, генерации необходимых SQL-операторов на основе состояния сущностей и запросов LINQ, отображения извлеченных данных на корректные формы сущностей, а также управления прочими деталями, которые обычно приходится делать вручную, если не используется Entity Framework.
862 Часть V. Введение в библиотеки базовых классов .NET >♦ EntityObjectO ft ReportPropertyChanged(string) $♦ ReportPropertyChanging(string) Я1 EntityKey Я1 EntityState Рис. 23.4. Уровень служб объектов EF может управлять любым классом, расширяющим EntityObject Функциональность уровня клиента сущности определена в пространстве имен System.Data.EntityClient. Это пространство имен включает набор классов, которые отображают концепции EF (такие как запросы LINQ to Entity) на лежащий в основе поставщик данных ADO.NET. Эти классы (т.е. EntityCommand и EntityConnection) очень похожи на классы, которые можно найти в составе поставщика данных ADO.NET; например, на рис. 23.5 показано, что классы уровня клиента сущности расширяют те же абстрактные базовые классы любого другого поставщика (например, DbCommand и DbConnection; см. главу 22). л {) System.Data.EntrtyCttent л d Base Types Г> Щ$ DbCommand \ s Щ% EntityConnection л i_j Base Types > *\% DbConnection л <*£ Entit>ConnectionStringBuilder л Q| Base Types U *£ DbConnectionStringBuilder л Щ% EntityDataReader * Гц Base Types i> Щ% DbDataReader -° IDataRecord > ** JExtendedDataReccrd л ^ EntttyParameter л Q| Base Types t> ■*!$ DbParameter I J . Ш - :-№ ,* CrtateDbPaiameterU •'• CreateParameterO ♦ EntityCommendfstring, S>stem.Data.EntityClient.Ej Ф Ent'rtyCommand(stn'ng, System.Data.EntityClient.E| ♦ EntrtyCcmmand(stringj -• EntityCommandO j% ExecuteDbDataRead*r(System.Data.CommandBeh ♦ ExecuteNonQueryO -♦ ExecuteReadertSystem.Deta.CommandBehaviof} ♦ ExecuteReadenj ♦ ExecuteScalarO H public sealed class EntityCommand : Syrtgm Oata.CommonPbCommand Member of $ystem.BiiftJ^Ut^knJ; Summary: -Represents a command to be executed against an Щ Entity Data Model (EDM). 4 Рис. 23.5. Уровень клиента сущности отображает команды сущности на лежащий в основе поставщик данных ADO.NET Уровень клиента сущности обычно работает "за кулисами", но вполне допустимо взаимодействовать с клиентом сущности напрямую, если нужен полный контроль над его действиями (прежде всего, над генерацией запросов SQL и обработкой возвращенных данных из базы). Если требуется более тонкий контроль над тем, как сущностный клиент строит SQL- оператор на основе входящего запроса LINQ, можно использовать Entity SQL. Это независимый от базы данных диалект SQL, который работает непосредственно с сущностями. Построенный запрос Entity SQL может быть отправлен непосредственно службам
Глава 23. ADO.NET, часть III: Entity Framework 863 клиента сущности (или, при желании, объектным службам), где он будет сформатиро- ван в правильный SQL-оператор для лежащего в основе поставщика данных. Далее в этой главе будет приведено несколько примеров применения Entity SQL. Если требуется более высокая степень контроля над манипуляциями извлеченными результатами, можно отказаться от автоматического отображения результатов базы данных на сущностные объекты и вручную обрабатывать записи с помощью класса EntityDataReader. Как и можно было ожидать, EntityDataReader позволяет обрабатывать извлеченные данные с использованием однонаправленного, доступного только для чтения потока данных, как это делает SqlDataReader. Ниже в главе рассматривается работающий пример этого подхода. Роль файла *.edmx Подводя итог сказанному: сущности — это классы клиентской стороны, которые функционируют, как модель сущностных данных (Entity Data Model). Хотя сущности клиентской стороны в конечном итоге отображаются на таблицу базы данных, жесткая связь между именами свойств сущностных классов и именами столбцов таблиц с данными отсутствует. В контексте API-интерфейса Entity Framework, чтобы данные сущностных классов отображались на данные таблиц корректно, требуется правильное определение логики отображения. В любой системе, управляемой моделью данных, уровни сущностей, реальной базы данных и отображения разделены на отдельные части: концептуальная модель, логическая модель и физическая модель. • Концептуальная модель определяет сущности и отношения между ними (если есть). • Логическая модель отображает сущности и отношения на таблицы с любыми необходимыми ограничениями внешних ключей. • Физическая модель представляет возможности конкретного механизма данных, указывая детали хранилища, такие как табличная схема, разбиение на разделы и индексация. В мире EF каждый из этих трех уровней фиксируется в XML-файле. В результате использования интегрированных визуальных конструкторов Entity Framework из Visual Studio 2010 получается файл с расширением *.edmx. Этот файл содержит XML- описания сущностей, физической базы данных и инструкции относительно того, как отображать эту информацию между концептуальной и физической моделями. Формат файла *.edmx рассматривается в первом примере этой главы. При компиляции основанных на EF проектов в Visual Srudio 2010 файл *.edmx используется для генерации трех отдельных файлов XML: один для концептуальной модели данных (*.csdl), один для физической модели (*.ssdl) и один для уровня отображения (*.msl). Данные из этих трех XML-файлов затем объединяются с приложением в виде двоичных ресурсов. После компиляции сборка .NET имеет все необходимые данные для вызовов API-интерфейса EF, имеющихся в коде. Роль классов ObjectContext и ObjectSet<T> Последним фрагментом мозаики EF является класс ObjectContext, определенный в пространстве имен System.Data.Objects. Генерация файла *.edmx дает в результате сущностные классы, которые отображаются на таблицы базы данных, и класс, расширяющий ObjectContext. Обычно этот класс используется для непрямого взаимодействия со службами объектов и функциональностью клиента сущности. Класс ObjectContext предлагает набор базовых служб для дочерних классов, включая возможность сохранения всех изменений (которые в конечном итоге превращаются
864 Часть V. Введение в библиотеки базовых классов .NET в обновление базы данных), настройку строки соединения, удаление объектов, вызов хранимых процедур, а также обработку других фундаментальных деталей. В табл. 23.1 описаны некоторые основные члены класса ObjectContext (не забывайте, что большинство этих членов остаются в памяти, пока не будет произведен вызов SaveChanges ()). Таблица 23.1. Общие члены ObjectContext Член Назначение AcceptAllChanges () Принимает все изменения, проведенные в сущностных объектах внутри контекста объектов AddObj ect () Добавляет объект к контексту объектов DeleteOb j ect () Помечает объект для удаления ExecuteFunction<T> () Выполняет хранимую процедуру в базе данных ExecuteStoreCommandO Позволяет отправлять команду SQL прямо в хранилище данных GetObjectByKey () Находит объект внутри контекста объектов по его ключу SaveChanges () Отправляет все обновления в хранилище данных CommandTimeout Это свойство получает или устанавливает значение таймаута в секундах для всех операций контекста объектов Connection Это свойство возвращает строку соединения, используемую текущим контекстом объектов SavingChanges Это событие инициируется, когда контекст объектов сохраняет изменения в хранилище данных Класс, унаследованный от ObjectContext, служит контейнером, управляющим сущностными объектами, которые сохраняются в коллекции типа ObjectSet<T>. Например, в результате генерации файла *.edmx для таблицы Inventory базы данных AutoLot получается класс с именем (по умолчанию) AutoLotEntities. Этот класс поддерживает свойство по имени Inventories (обратите внимание на множественное число), которое инкапсулирует член данных ObjectSet<Inventory>. В случае создания EDM для таблицы Orders базы данных AutoLot в классе AutoLotEntities определено второе свойство по имени Orders, которое инкапсулирует переменную-член ObjectSet<Order>. В табл. 23.2 определены некоторые общие члены System.Data. Objects.ObjectSet<T>. Таблица 23.2. Общие члены ObjectSet<T> Член AddObjectO CreateOb]ect<T> DeleteObject Назначение Позволяет вставить новый сущностный объект в коллекцию Создает новый экземпляр указанного сущностного типа Помечает объект для удаления Добравшись до нужного свойства контекста объектов, можно вызывать любой член ObjectSet<T>. Еще раз рассмотрим простой код примера, приведенного в начале этой главы: using (AutoLotEntities context = new AutoLotEntities ()) { // Добавить новую строку в таблицу Inventory, используя сущность Саг.
Глава 23. ADO.NET, часть III: Entity Framework 865 context.Cars.AddObject(new Car() { AutoIDNumber = 987, CarColor = "Black", MakeOfCar = "Pinto", NicknameOfCar = "Pete" }); context.SaveChanges(); } Здесь AutoLotEntities "является" ObjectContext. Свойство Cars предоставляет доступ к переменной ObjectSet<Car>. Эта ссылка используется для вставки нового сущностного объекта Саг и выполнения ObjectContext операции сохранения всех изменений в базе данных. Обычной целью запросов LINQ to Entities является экземпляр класса ObjectSet<T>; этот класс поддерживает те же расширяющие методы, о которых говорилось в главе 13. Более того, ObjectSet<T> получает значительную часть своей функциональности от своего прямого предка ObjectQuery<T> — класса, представляющего строго типизированный запрос LINQ (или Entity SQL). Собираем все вместе Прежде чем построить первое приложение, в котором используется Entity Framework, взгляните на рис. 23.6, на котором показана организация API-интерфейса EF. Кодовая база С# Entity SQL Entity SQL || Гос|| fh IEnumerable<T> Объектные службы *.edmx w F\ \z Дерево команд ТГ^ EntityDataReader Поставщик данных клиента сущности Дерево команд ТГ^ DbDataReader Поставщик данных AD0.NET И" Физическая база данных Рис. 23.6. Базовые компоненты AD0.NET Entity Framework Составные части на рис. 23.6 не столь сложны, как могут показаться на первый взгляд. Например, рассмотрим следующий распространенный сценарий. Вы пишете код С#, в котором применяется запрос LINQ к сущности, полученной от контекста. Этот запрос проходит через службы объектов, где преобразует команду LINQ в дерево, которое может понять клиент сущности. В свою очередь, клиент сущности форматирует это дерево в правильный оператор SQL для лежащего в основе поставщика ADO.NET. Этот поставщик вернет читатель данных (т. е. объект-наследник DbDataReader), который клиентские службы используют для направления данных объектным службам, используя EntityDataReader. Кодовая база получает обратно перечисление данных сущностей (IEnumerable<T>).
866 Часть V. Введение в библиотеки базовых классов .NET Теперь рассмотрим другой сценарий. Кодовая база С# желает получить больший контроль над тем, как клиентские службы конструируют конечный оператор SQL для отправки в базу данных. Поэтому код С# пишется с использованием Entity SQL, который может быть передан непосредственно клиенту сущности или объектным службам. Конечный результат возвращается в виде IEnumerable<T>. В любом из этих сценариев XML-данные из файла *.edmx должны быть сделаны известными клиентским службам; это позволит им понять, как следует отображать элементы базы данных на сущности. Наконец, помните, что клиент (т.е. кодовая база С#) также может получить результаты, отправленные сущностным клиентом, применяя EntityDataReader непосредственно. Построение и анализ первой модели EDM Понимая предназначение платформы ADO.NET Entity Framewrok, а также имея общее представление о ее работе, можно приступать к рассмотрению первого примера. Чтобы пока не усложнять картину, построим модель EDM, которая обеспечит доступ только к таблице Inventory базы данных AutoLot. Разобравшись с основами, мы затем построим новую модель EDM, которая будет рассчитана на всю базу данных AutoLot, и отобразим данные в графическом интерфейсе пользователя. Генерация файла *.edmx Начнем с создания консольного приложения по имени InventoryEDMConsoleApp. Когда планируется использование Entity Framework, первый шаг состоит в генерации необходимой концептуальной, логической и физической модели данных, определенной в файле *.edmx. Один из способов предусматривает применение для этого утилиты командной строки EdmGen.exe из комплекта .NET 4.0 SDK. Откройте окно командной строки Visual Studio 2010 и введите следующую команду: EdmGen.exe -? На консоль выводится список опций, которые можно указывать утилите для генерации необходимых файлов, основываясь на существующей базе данных; кроме того, доступны опции для генерации совершенно новой базы данных на основе имеющихся сущностных файлов. В табл. 23.3 описаны некоторые общие опции EdmGen.exe. Таблица 23.3. Часто используемые флаги командной строки утилиты EdmGen.exe Опция Назначение /mode:FullGeneration Генерировать файлы *.ssdl, *.msl, *.csdl и клиентские сущности из указанной базы данных /proj ect: Базовое имя, которое должно использоваться для сгенерированного кода и файлов. Обычно это имя базы данных, из которой извлекается информация (допускается сокращенная форма — /р:) /connectionstring: Строка соединения, используемая для взаимодействия с базой данных (допускается сокращенная форма — /с:) /language: Позволяет указать, какой синтаксис должен использоваться для сгенерированного кода — С# или VB /pluralize Позволяет автоматически выбирать множественное или единственное число для имени набора сущностей, имени типа сущности и имени навигационного свойства, согласно правилам английского языка
Глава 23. ADO.NET, часть III: Entity Framework 867 Как и платформа .NET 4.0 в целом, программная модель EF поддерживает программирование в стиле сначала домен, что позволяет создавать свойства (с применением типичных объектно-ориентированных приемов) и использовать их для генерации новой базы данных. В этом вводном обзоре ADO.NET EF ни подход "сначала модель", ни генерация сущностной модели клиентской стороны с помощью утилиты EdmGen.exe применяться не будут. Вместо этого будут использоваться визуальные конструкторы EDM из среды Visual Studio 2010. Выберите пункт меню Project^Add New Item (ПроектеДобавить новый элемент) и вставьте новый элемент ADO.NET Entity Data Model по имени InventoryEDM.edmx, как показано на рис. 23.7. Add Ne* Item - Inventor/ED InsUlled Templates л Visual C# Items Code Data General Web Windows Forms WPF Reporting Workflow I K|jL LINQ to SQL Classes j Service-based Database Visual C# hems Per user extensions are currently not allowed to load. Name InventoryEDM.edmx Рис. 23.7. Вставка нового элемента проекта AD0.NET EDM Щелчок на кнопке Add (Добавить) приводит к запуску мастера создания модели сущностных данных (Entity Data Model Wizard). На первом шаге мастер позволяет выбрать, нужно генерировать EDM из существующей базы данных либо определить пустую модель (для разработки в стиле "сначала модель"). Выберите опцию Generate from database (Генерировать из базы данных) и щелкните на кнопке Next (Далее), как показано на рис. 23.8. На втором шаге мастера выбирается база данных. Если соединение с базой данных внутри проводника сервера Visual Studio 2010 уже существует, оно будет присутствовать в раскрывающемся списке. Если же нет, щелкните на кнопке New Connection (Создать соединение). В любом случае выберите базу данных AutoLot и отметьте флажок Save entity connection settings in App.config as (Сохранить настройки соединения в файле App.config как), как показано на рис. 23.9. Прежде чем щелкать на кнопке Next, взгляните на формат строки соединения: metadata=res : //VlnventoryEDM. csdl | res: //VlnventoryEDM.ssdl | res : //VlnventoryEDM.msl; provider=System. Data. SqlClient;provider connection stnng= "Data Source=(local)\SQLEXPRESS; Initial Catalog=AutoLot;Integrated Security=True;Pooling=False" Основной интерес в ней представляет флаг metadata, который используется для указания имен встроенных данных XML-ресурсов концептуального, физического и файла отображений (вспомните, что во время компиляции файл *.edmx будет разделен на отдельные файлы, и данные этих файлов примут вид двоичных ресурсов, встраиваемых в сборку).
868 Часть V. Введение в библиотеки базовых классов .NET Entity Data Model Wizard Choose Model Contents Empty model Generates the model from a database. Classes are generated from the model when the project is compiled. This wizard also lets you specify the database connection and database objects to include in the model. J^kJ- Рис. 23.8. Генерация модели EDM из существующей базы данных Choose Your Data Connection Which data connection should your application use to connect to the database? | andrewpc\sqlexpress.AutoLot.dbo ^J [ New Connection... This connection string appears to contain sensitr.e data (fw example a pass*oid) that к required 1o connect to the database. Storing sensitive data in tht connection stnng can be a security nsk. Do you want to include this sensitive data in the connection rtring7 I eta from the connection rtnng 1 will set it i Yes, include the sensitive data in the connection string. Entity connection string: metadata=res://n*/lnventoryEDM.csdl|res://*/InventoryEDM.ssdl| res://*/InventoryEDM.msl;provider=System.Data.SqlClient;provider connection string="Data Source= (locaO\SQLEXPRESS;InitialCatalog=AutoLofIntegratedSecurity=True;Pooling=Farse" } Save entity connection settings in App.Config as; AutoLotEntities previous Next Г Рис. 23.9. Выбор базы данных для генерации EDM На последнем шаге мастера можно выбрать элементы из базы данных, для которой необходимо сгенерировать модель EDM. В рассматриваемом примере ограничимся только таблицей Inventory (рис. 23.10). Щелкните на кнопке Finish (Готово) для генерации модели EDM.
Глава 23. ADO.NET, часть III: Entity Framework 869 s gfg Tables □3 CreditRisks (dbo) LJ3 Customers (dbo) ЭЭ Inventory (dboV B3 Orders (dbo) ■* !_ J3 sysdiagrams (dbo) (_Г)*р Views * (ЛЫ Stored Procedures !7'"_m fn_diagramobjects (dbo) ОЭ GetPetName (dbo) ОПП sp_alterdiagram (dbo) S ]^~] sp_creatediagram (dbo) ■: [/] Pluralrze or jingufarize generated object names у j Include foreign key columns in the model Model Namespace: AutoLotModel <£revious } N.e*t ■ Finish Рис. 23.10. Выбор элементов базы данных Изменение формы сущностных данных После завершения работы с мастером откроется визуальный конструктор EDM в IDE-среде с одной сущностью по имени Inventory. Просмотреть композицию любой сущности в визуальном конструкторе можно в окне Model Browser (Браузер моделей), которое открывается через пункт меню View1^Other Windows (Вид1^ Другие окна). Теперь взгляните на формат концептуальной модели для таблицы базы данных Inventory, представленный в папке Entity Types (Типы сущности), как показано на рис. 23.11. В узле хранилища, имя которого совпадает с именем базы данных (AutoModel.Store), находится физическая модель базы данных. InventoryEDM.edmx xQ - ,...- , ,-... , Inventory E) _n— - Properties *$CarID !*j*Make *? Color _<? PetName Ь Navigation Properties s К 4 '" >.ф [Model Browser 1 Type here to search 1 л ^ InventoryEDM.edmx * [£| AutoLotModel * U Entity Types л ^Inventory tiCarfD . 3* Color *f Make Ч/О? PetName l3 Complex Types _jl Associations ^ EntityContamer AutoLotEntrhes л \J AutoLotModel.Store л d Tables / Views 1 Color I] Make H PetName uj Stored Procedures kjj Constraints ^ ? X Рис. 23.11. Визуальный конструктор EDM и окно браузера моделей
870 Часть V. Введение в библиотеки базовых классов .NET По умолчанию имена сущностей будут основаны на именах исходных объектов баз данных; однако, вспомните, что имена сущностей в концептуальной модели могут быть любыми. Чтобы изменить имя сущности либо имена свойств сущности, необходимо выбрать нужный элемент в визуальном конструкторе и установить соответствующим образом свойство Name в окне свойств (Properties). Переименуйте сущность Inventory в Саг и свойство PetName в CarNickname (рис. 23.12). Концептуальная модель должна выглядеть подобно тому, как показано на рис. 23.13. Теперь выберите сущность Саг в визуальном конструкторе и снова загляните в окно Properties. Вы должны увидеть поле Entity Set Name (Имя набора сущностей), также переименованное из Inventories в Cars (рис. 23.14). Значение Entity Set Name важно, потому что оно соответствует имени свойства в классе контекста данных, который используется для модификации базы данных. Вспомните, что это свойство инкапсулирует переменную-член ObjectSet<T> класса-наследника ObjectContext. Прежде чем двигаться дальше, скомпилируйте приложение; это приведет к обновлению кодовой базы и генерации файлов *.csdl, *.msl и *.ssdl на основе данных файла *.edmx. Properties AutoLotModel.Car.CarNickname :: U\3 t Documentation Entity Key Fixed Length Getter Max Length Nullable Setter Property Fake False Public 50 j CarNickname (None) Public * *x The name of the property. Рис. 23.12. Изменение формы сущностей с помощью окна свойств ** Cat © а Properties ЩСагЮ fit Make ^jjf Color ^ CarNickname 3 Navigation Properties Рис. 23.13. Модель клиентской стороны, измененная в соответствии с предметной областью Properties AutoLotModel.Car Abstract Access Base Type I > Documentation ЕЕЕЕЕППЕ Name EntrtyType - * x] False Public (None) | Can Car entity Set Name The name of the EntitySet that contains instances of the entity. Рис. 23.14. Имя оболочки свойства ObjectSet<T>
Глава 23. ADO.NET, часть III: Entity Framework 871 Просмотр отображений Имея данные в измененной форме, можно просматривать отображения между концептуальным уровнем и физическим уровнем в окне Mapping Details (Сведения об отображениях), которое открывается через пункт меню View=> Other Windows1^ Mapping Details (Вид1^Другие окна1^ Сведения об отображениях). Взгляните на рис. 23.15 и обратите внимания, что узлы в левой части дерева представляют имена данных из физического уровня, в то время как узлы справа представляют имена концептуальной модели. Mapping Details - Car Column * Tables л  Maps to Inventory 3£ <AcldaConditran> л i_j Column Mappings $JDCarID:int 33 Make:varehar ffj Color: varchar J] jPetName: varchar Г*3 <Add a Tdbieor'view> Oper.. ♦-► 4~* «-► Value / Property t^ CarID:lnt32 ^f Make: String *f Color: String ■^вСШНЗИЕЖЯЯГИ Hi ■ ши M.appinqD Рис. 23.15. В окне Mapping Details можно просматривать отображения концептуальной и физической моделей Просмотр данных сгенерированного файла *.edmx Теперь давайте посмотрим, что именно мастер EDM Wizard сгенерировал. Щелкните правой кнопкой мыши на файле InventoryEDM.edmx в проводнике решения и выберите в контекстном меню пункт Open With... (Открыть с помощью). В открывшемся диалоговом окне выберите опцию XML Editor (Редактор XML). Это позволит просмотреть XML- данные, лежащие в основе представления в визуальном конструкторе EDM. Структура этого XML-документа разделена на четыре части: все они находятся в корневом элементе <edms:Edmx>. Подэлемент <edmx:Runtime> определяет XML-данные для концептуальной, физической и модели уровня отображения. Ниже показано определение физической таблицы базы данных Inventory: <!-- Содержимое SSDL --> <edmx:StorageModels> <Schema Namespace="AutoLotModel.Store" Alias="Self" Provider="System.Data.SqlClient" ProviderManifestToken= 008" xmlns:store= "http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="http://schemas.microsoft.com/ado/200 9/02/edm/ssdl"> <EntityContainer IJame="AutoLotModelStoreContainer"> <EntitySet Name="Inventory" EntityType="AutoLotModel.Store.Inventory" store:Type="Tables" Schema="dbo" /> </EntityContainer> <EntityType Name="Inventory"> <Key> <PropertyRef Name="CarID" /> </Key> <Property Name="CarID" Type="int" Nullable="false" /> <Property Name="Make" Type="varchar" Nullable="false" MaxLength=0" />
872 Часть V. Введение в библиотеки базовых классов .NET <Property Name="Color" Type="varchar" Nullable="false" MaxLength=0" /> <Property Name="PetName" Type="varchar" MaxLength=0" /> </EntityType> </Schema> </edmx:StorageModels> Обратите внимание, что узел <Schema> определяет имя поставщика данных ADO.NET, который использует эту информацию при взаимодействии с базой данных (System.Data.SqlClient). Узлами <EntityType> помечается имя физической таблицы базы данных, а также каждый столбец в таблице. Следующая важная часть файла *.edmx — элемент <edmx:ConceptualModels>, который определяет измененные сущности клиентской стороны. Как видно, сущность Cars определяет свойство CarNickname, которое изменяется в визуальном конструкторе: <'-- Содержимое CSDL --> <edmx:ConceptualModels> <Schema Namespace="AutoLotModel" Alias="Self" xmlns:annotation="http://schemas.microsoft.com/ado/200 9/02/edm/annotation" xmlns="http://schemas.microsoft.com/ado/2 00 8/0 9/edm"> <EntityContainer Name="AutoLotEntities" annotation:LazyLoadingEnabled="true"> <EntitySet Name="Cars" EntityType="AutoLotModel.Car" /> </EntityContainer> <EntityType Name="Car"> <Key> <PropertyRef Name="CarID" /> </Key> <Property Name="CarID" Type="Int32" Nullable="false" /> <Property Name="Make" Type="String" Nullable="false" MaxLength=0" Unicode="false" FixedLength="false" /> <Property Name="Color" Type="String" Nullable="false" MaxLength=0" Unicode="false" FixedLength="false" /> <Property Name="CarNickname" Type="String" MaxLength=0" Unicode="false" FixedLength="false" /> </EntityType> </Schema> </edmx:ConceptualModels> Это перемещает на уровень отображения, который окно Mapping Details и исполняющая среда EF используют для подключения имен в концептуальной модели к физической модели: <!-- Содержимое отображения C-S --> <edmx:Mappings> <Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2008/0 9/mapping/cs"> <EntityContainerMapping StorageEntityContainer="AutoLotModelStoreContainer" CdmEntityContainer="AutoLotEntities"> <EntitySetMapping Name="Cars"> <EntityTypeMapping TypeName="AutoLotModel.Car"> <MappingFragment StoreEntitySet="Inventory"> <ScalarProperty Name="CarID" ColumnName="CarID" I> <ScalarProperty Name="Make" ColumnName="Make" /> <ScalarProperty Name="Color" ColumnName="Color" /> <ScalarProperty Name="CarNickname" ColumnName="PetName" /> </MappingFragmentx/EntityTypeMapping> </EntitySetMapping> </EntityContainerMapping> </Mapping> </edmx:Mappings>
Глава 23. ADO.NET, часть III: Entity Framework 873 Последней частью файла *.edmx является элемент <Designer>, который исполняющей средой EF не используется. Он содержит инструкции, используемые Visual Studio для отображения сущностей на поверхности визуального конструктора. Удостоверьтесь, что проект скомпилирован, по крайней мере, однажды, и щелкните на кнопке Show All Files (Показать все файлы) в проводнике решений. Затем зайдите в папку obj\Debug, а после этого — в edmxResourcesToEmbed. Здесь находятся три XML- файла, основанные на содержимом файла *.edmx (рис. 23.16). Данные в этих файлах будут встроены в сборку как двоичные ресурсы. Таким образом, приложение .NET обладает всей информацией, необходимой для понимания концептуального, физического и уровня отображения модели EDM. Просмотр сгенерированного исходного кода Теперь вы почти готовы к тому, чтобы написать некоторый код, использующий построенную модель EDM. Однако прежде чем сделать это, стоит заглянуть в сгенерированный код С#. Откройте окно Class View (Представление класса) и раскройте пространство имен по умолчанию. В дополнение к классу Program там будет присутствовать сгенерированный мастером EDM Wizard сущностный класс (который вы переименовали в Саг) и другой класс по имени AutoLotEntities. Зайдя в Solution Explorer и раскрыв узел InventoryEDM.edmx, вы увидите поддерживаемый IDE-средой файл по имени InventoryEDM.Designer.cs. Как и любой другой файл подобного рода, его не следует редактировать напрямую, потому что IDE-среда пересоздает его при каждой компиляции. Тем не менее, этот файл можно открыть для просмотра двойным щелчком кнопкой мыши, Класс AutoLotEntities расширяет класс ObjectContext, который (как вы, возможно, помните), представляет собой входную точку в программную модель EF. Конструктор предоставляет различные способы заполнения данными строки соединения. Конструктор по умолчанию сконфигурирован на автоматическое чтение данных строки соединения из сгенерированного мастером файла App.conf ig: public partial class AutoLotEntities : ObjectContext { public AutoLotEntities () : base("name=AutoLotEntities" , "AutoLotEntities") { this.ContextOptions.LazyLoadingEnabled = true; OnContextCreated () ; } } Затем обратите внимание, что свойство Cars класса AutoLotEntities инкапсулирует член данных ObjectSet<Car>. Это свойство можно использовать для работы с моделью EDM с целью непрямой модификации физической базы данных заднего плана: public partial class AutoLotEntities : ObjectContext { Solution Explorer w [ ^ Solution 'InventoryEDMConsoleApp' A project) л .JH InventoryEDMConsoleApp P iM Properties 13Й1 References ILJ obj -- :.J/x86 .._>■ Debug i edmxResourcesToEmbed j InventoryEDM.csdl j InventoryEDM. msl J InventoryEDM.ssdl ^ Solution Explorer I Рис. 23.16. Файл *.edmx используется для генерации трех отдельных XML-файлов
874 Часть V. Введение в библиотеки базовых классов .NET public ObjectSet<Car> Cars { get { if ( (_Cars == null) ) { _Cars = base.CreateObjectSetKCar^ ("Cars"); } return _Cars; } } private ObjectSet<Car> _Cars; } На заметку! В классе, унаследованном от ObjectContext, доступно множество методов, имена которых начинаются с AddTo. Хотя их можно использовать для добавления новых сущностей к переменным-членам ObjectSet<T>, предпочтительнее делать это за счет обращения к члену ObjectSet<T>, полученному от строго типизированных свойств. И последним интересным аспектом в файле кода визуального конструктора является сущностный класс Саг. Значительная часть кода каждого сущностного класса представляет собой коллекцию, моделирующую форму концептуальной модели. Каждое из этих свойств реализует свою логику set за счет вызова статического метода StructuralObject.SetValueO из API-интерфейса EF. Кроме того, логика set включает код, который информирует исполняющую среду EF о том, что состояние сущности изменилось; это важно, поскольку ObjectContext должен знать обо всех этих изменениях, чтобы вытолкнуть изменения в физическую базу данных. Вдобавок внутри логики set производятся вызовы двух частичных методов. Вспомните, что частичный метод С# предоставляет простой способ обращения с уведомлениями об изменениях в приложениях. Если частичный метод не реализован, компилятор его игнорирует и полностью отбрасывает. Ниже приведена реализация свойства CarNickname сущностного класса Саг: public partial class Car : EntityObject { public global::System.String CarNickname { get { return _CarNickname; } set { OnCarNicknameChanging(value); ReportPropertyChanging ("CarNickname"); _CarNickname = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("CarNickname") ; OnCarNicknameChanged() ; } } private global::System.String _CarNickname; partial void OnCarNicknameChanging (global: :System.String value); partial void OnCarNicknameChanged (); }
Глава 23. ADO.NET, часть III: Entity Framework 875 Улучшение сгенерированного исходного кода Все классы, сгенерированные визуальным конструктором, объявлены с ключевым словом partial, которое позволяет разнести реализацию класса по нескольким файлам кода С#. Это особенно полезно при работе с программной моделью EF, поскольку означает возможность добавлять "реальные'' методы к сущностным классам, что помогает лучше моделировать предметную область. В этом примере будет переопределен метод ToStringO сущностного класса Саг для возврата состояния сущности в виде хорошо форматированной строки. Также будет завершены определения частичных методов OnCarNicknameChangingO и OnCarNicknameChanged() для обслуживания простых диагностических уведомлений. Определим следующий частичный класс в новом файле Car.cs: public partial class Car { public override string ToStringO { // Поскольку столбец PetName может быть пустым, // укажем в качестве имени по умолчанию **No Name**, return string. Format (" {0} is a {1} {2} with ID {3}.", this.CarNicftname ?? "**No Name**", this.Color, this.Make, this.CarlD); partial void OnCarNicknameChanging(global::System.String value) Console.WriteLine ("\t-> Changing name to: {0}", value); partial void OnCarNicknameChanged () Console.WriteLine ("\t-> Name of car has been changed!"); } Помните, после предоставления реализации этих частичных методов можно получать уведомления, когда свойства сущностных: классов изменены или изменяются, но не при изменении физической базы данных. Если требуется знать, когда изменяется физическая база данных, можно обработать событие SavingChanges класса-наследника ObjectContext. Программирование с использованием концептуальной модели Теперь можно написать некоторый код, взаимодействующий с моделью EDM. Начнем с добавления в метод Main() класса Program вызова одного вспомогательного метода, который выводит каждый элемент из базы данных Inventory с использованием концептуальной модели и еще одного, который вставляет новую запись в таблицу Inventory: class Program { static void Main(string [ ] args) { Console.WriteLine("***** Fun with ADO.NET EF *****"); AddNewRecord(); PrintAllInventory(); Console.ReadLine(); }
876 Часть V. Введение в библиотеки базовых классов .NET private static void AddNewRecord () { // Добавить запись в таблицу Inventory базы данных AutoLot. using (AutoLotEntities context = new AutoLotEntities()) { try { // Жестко закодировать данные новой записи (для целей тестирования). context.Cars.AddObject(new Car () { CarID = 2222, Make = "Yugo", Color = "Brown" }); context.SaveChanges(); } catch(Exception ex) { Console.WriteLine(ex.InnerException.Message); } } } private static void PnntAllInventory () { // Выбрать все элементы из таблицы Inventory базы AutoLot и вывести данные, // используя специальный метод ToStringO сущностного класса Саг. using (AutoLotEntities context = new AutoLotEntities()) { foreach (Car с in context.Cars) Console.WriteLine(c); } } } Подобный показанному выше код уже встречался ранее в этой главе, но сейчас вы уже должны лучше представлять, как он работает. Каждый вспомогательный метод создает экземпляр класса-наследника ObjectContext (AutoLotEntities) и использует строго типизированное свойство Cars для взаимодействия с полем ObjectSet<Car>. Перечисление каждого элемента, представленного свойством Cars, позволяет передать неявно SQL-оператор SELECT лежащему в основе поставщику данных ADO.NET. За счет вставки нового объекта Саг методом AddObjectO класса ObjectSet<Car> и последующего вызова SaveChanges () на контексте выполняете SQL-оператор INSERT. Удаление записи Для того чтобы удалить запись из базы данных, сначала необходимо найти корректный элемент в ObjectSet<T>. Это можно сделать, передав объект EntityKey (член пространства имен System.Data) методу GetObjectByKeyO. Предполагая, что это пространство имен импортировано в файл кода С#, теперь можно написать следующий вспомогательный метод: private static void RemoveRecord () { // Найти автомобиль для удаления по первичному ключу. using (AutoLotEntities context = new AutoLotEntities()) { // Определить ключ для искомой сущности. EntityKey key = new EntityKey("AutoLotEntities.Cars", "CarlD", 2222); // Проверить ее существование, и если да - удалить. Car carToDelete = (Car) context.GetObjectByKey (key);
Глава 23. ADO.NET, часть III: Entity Framework 877 if (carToDelete != null) { context.DeleteObject(carToDelete) ; context.SaveChanges() ; } } } На заметку! Хорошо это или плохо, но вызов GetObjectByKey () требует обращения к базе данных, прежде чем можно будет удалить объект. Обратите внимание, что при создании объекта EntityKey в первом аргументе передается объект string, информирующий о том, какой объект ObjectSet<T> должен обрабатываться в заданном классе-наследнике ObjectContext. Второй аргумент (еще один объект string) представляет имя свойства сущностного класса, которое служит ключом, а последний аргумент — это значение первичного ключа. Как только нужный объект найден, можно вызвать DeleteObjectO контекста и сохранить изменения. Обновление записи Обновление записи также делается просто: найдите объект, который хотите изменить, установите новые значения свойств возвращенной сущности и сохраните изменения: private static void UpdateRecord () { // Найти автомобиль для обновления по первичному ключу. using (AutoLotEntities context = new AutoLotEntities ()) { // Определить ключ для сущности, которую мы ищем. EntityKey key = new EntityKey("AutoLotEntities.Cars", "CarlD", 2222); // Извлечь объект автомобиля, изменить его и сохранить. Car carToUpdate = (Car)context.GetObjectByKey(key); if (carToUpdate '= null) { carToUpdate.Color = "Blue"; context.SaveChanges (); } } } Приведенный выше метод может показаться немного странным, по крайней мере, до тех пор, пока вы не вспомните, что сущностный объект, возвращенный из GetObjectByKey () — это ссылка на существующий объект в поле Object Set<T>. Таким образом, установка свойств для изменения состояния приводит к изменению того же объекта в памяти. На заметку! Во многом подобно объекту DataRow из AD0.NET (см. главу 22), любой наследник EntityObject (т.е. все сущностные классы) имеет свойство по имени EntityState, используемое контекстом объектов для определения того, был ли элемент модифицирован, удален, отсоединен и т.п. Оно устанавливается без вашего участия при работе с программной моделью; тем не менее, при необходимости его можно изменять вручную.
878 Часть V. Введение в библиотеки базовых классов .NET Запросы с помощью LINQ to Entities До сих пор использовались несколько простых методов на контексте объектов и сущностные объекты для выполнения выборки, вставки, обновления и удаления. Это удобно и само по себе; однако гораздо больший эффект от EF можно получить, добавив запросы LINQ. Чтобы использовать LINQ для обновления или удаления записей, создавать объект EntityKey вручную не понадобится. Взгляните на следующее изменение в методе RemoveRecordO, которое пока не работает должным образом: private static void RemoveRecordO { // Найти автомобиль для удаления по первичному ключу, using (AutoLotEntities context = new AutoLotEntities ()) { // Проверить его наличие. var carToDelete = from с in context.Cars where c.CarlD == 2222 select c; if (carToDelete != null) { context.DeleteObject(carToDelete); context.SaveChanges (); } } } Этот код скомпилируется, но при попытке вызвать метод Delete Ob ject() возникнет исключение времени выполнения. Причина в том, что этот конкретный запрос LINQ возвращает объект Ob]ectQuery<T>, а не объект Саг. Помните, что запрос LINQ для поиска единственной сущности дает в результате объект ObjectQuery<T>, который представляет запрос, доставляющий необходимые данные. Чтобы выполнить запрос (и вернуть сущность Саг), потребуется выполнить такой метод, как FirstOrDefaultO, на объекте запроса, как показано в следующем примере: var carToDelete = (from с in context.Cars where c.CarlD == 2222 select c).FirstOrDefault(); Вызов FirstOrDefaultO на ObjectQuery<T> позволяет искать нужный элемент; если не окажется экземпляра Саг с идентификатором 2222, будет получено значение по умолчанию — null. Рассмотрим несколько дополнительных примеров запросов LINQ: private static void FunWithLINQQueries () { using (AutoLotEntities context = new AutoLotEntities ()) { // Получить проекцию новых данных. var colorsMakes = from item in context.Cars select new { item.Color, item.Make }; foreach (var item in colorsMakes) { Console.WriteLine(item); } // Получить только элементы с CarID < 1000 var idsLessThanlOOO = from item in context.Cars where item.CarlD < 1000 select item; foreach (var item in idsLessThanlOOO) { Console.WriteLine(item); } } }
Глава 23. ADO.NET, часть III: Entity Framework 879 Хотя синтаксис этих запросов достаточно прост, помните, что при каждом применении запроса LINQ к контексту объектов происходит обращение к базе данных! Когда нужно получить независимую копию данных, которая может быть целью новых запросов LINQ, пригодятся (среди прочих) расширяющие методы ToList<T>(), ToArray<T>() или ToDictionary<K, V>(). Ниже показан измененный предыдущий метод, который выполняет эквивалент SELECT *, кэширует сущности в виде массива и манипулирует данными массива с помощью LINQ to Objects. using (AutoLotEntities context = new AutoLotEntities () ) { // Чтобы получить все данные из таблицы Inventory, // можно было бы также написать следующий код: // var allData = (from item in context.Cars select item) .ToArrayO; var allData = context.Cars.ToArray(); // Получить проекцию новых данных. var colorsMakes = from item in allData select new { item.Color, item.Make }; // Получить только элементы с CarlD < 1000 var idsLessThanlOOO = from item in allData where item.CarlD < 1000 select item; } Работа с LINQ to Entities намного интереснее, когда модель EDM содержит несколько взаимосвязанных таблиц. Ниже будут продемонстрированы некоторые примеры, иллюстрирующие сказанное; а сейчас давайте завершим текущий пример рассмотрением двух других способов взаимодействия с контекстом объектов. Запросы с помощью Entity SQL В большинстве случаев запросы к ObjectSet<T> производятся с помощью LINQ. Сущностный клиент преобразует запрос LINQ в соответствующий SQL-оператор и передает его на обработку базе данных. В случаях, когда необходим больший контроль над формированием запроса, можно воспользоваться Entity SQL. Entity SQL — это SQL-подобный язык, который может применяться к сущностям. Хотя формат операторов Entity SQL подобен традиционному SQL, все же они не идентичны. Entity SQL обладает уникальным синтаксисом, так как имеет дело с запросом, а не с физической базой данных. Подобно запросу LINQ to Entities, запрос Entity SQL используется для передачи "реального" SQL-запроса базе данных. Подробные сведения о командах Entity SQL можно найти в документации .NET Framework 4.0 SDK, а ниже представлен один пример. Взгляните на следующий метод, где строится запрос Entity SQL, который находит все черные автомобили в коллекции ObjectSet<Car>: private static void FunWithEntitySQL () { using (AutoLotEntities context = new AutoLotEntities()) { // Построить строку, содержащую синтаксис Entity SQL. string query = "SELECT VALUE car FROM AutoLotEntities.Cars " + "AS car WHERE car.Color='black'"; // Теперь построить ObjectQuery<T> на основе строки, var blackCars = context.CreateQuery<Car>(query); foreach (var item in blackCars) { Console.WriteLine(item); } } }
880 Часть V. Введение в библиотеки базовых классов .NET С форматированный оператор Entity SQL передается в качестве аргумента методу CreateQuery<T> контекста объектов. Работа с объектом EntityDataReader При использовании LINQ to Entities или Entity SQL извлеченные данные отображаются на сущностные классы автоматически, благодаря службе клиента сущности. Обычно именно это и нужно; но при желании можно перехватить результирующий набор, прежде чем он превратится в сущностные объекты, и вручную обработать с использованием EntityDataReader. Ниже приведен последний вспомогательный метод для этого примера, в котором используются несколько членов пространства имен System.Data.EntityClient для построения соединения вручную, через объект команды и чтения данных. Этот код должен показаться знакомым по главе 21; основное отличие состоит в том, что применяется Entity SQL, а не "нормальный" SQL. private static void FunWithEntityDataReader () { // Создать объект соединения на основе файла *.config. using (EntityConnection en = new EntityConnection (llname=AutoLotEntities11) ) { en.Open (); // Построить запрос Entity SQL. string query = "SELECT VALUE car FROM AutoLotEntities.Cars AS car"; // Создать командный объект. using (EntityCommand cmd = en.CreateCommand()) { cmd.CommandText = query; // Получить объект для чтения данных и обработать записи. using (EntityDataReader dr = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { while (dr.ReadO ) { Console.WriteLine ("***** RECORD *****"); Console.WriteLine ("ID: {0}", dr["CarlD"]); Console.WriteLine ("Make: {0}", dr["Make"]); Console.WriteLine("Color : {0}", dr["Color"]); Console.WriteLine ("Pet Name: {0}", dr["CarNickname"]); Console.WriteLine (); } } } } } Этот начальный пример должен открыть вам дорогу к пониманию деталей работы с Entity Framework. Как упоминалось ранее, все становится намного интереснее, когда модель EDM содержит взаимосвязанные таблицы, о чем речь пойдет ниже. Исходный код. Проект InventoryEDMConsoleApp доступен в подкаталоге Chapter 23.
Глава 23. ADO.NET, часть III: Entity Framework 881 Проект AutoLotDAL версии 4.0, теперь с сущностями Далее будет показано, как строить модель EDM, которая охватывает значительную часть базы данных AutoLot, включая хранимую процедуру GetPetName. Рекомендуется скопировать проект AutoLotDAL (Version 3), созданный в главе 22, и переименовать копию в AutoLotDAL (Version 4). Откройте проект AutoLotDAL (Version 4) в Visual Studio 2010 и добавьте новый элемент ADO.NET Entity Data Model (Модель сущностных данных ADO.NET) по имени AutoLotDALEF. edmx. На третьем шаге мастера понадобится выбрать таблицы Inventory, Orders и Customers (таблица CreditRisks пока не нужна), а также специальную хранимую процедуру (рис. 23.17). Which database objects do you want to include in your model? ГТЩр Tables HG3 CreditRisks (dbo) SO Customers (dbo) ШШ1 Inventory (dbo) ШШ Orders (dbo) LJZ3 sysdiagrams (dbo) | jj Views л 2Ш Stored Procedures 1 \~~\ fn_diagramobjects (dbo) [7[~1 GetPetName (dbo) I 1 \l\ sp_alterdiagram (dbo) 1'ТП sp^creatediagram (dbo) I ■iiff", r u,v—-.. \J\ Pluralize or singulartze generated object names \/\ Include foreign key columns in the model Model Namespace: j AutoLotModel < Previous J j Next > Finish Cancel Рис. 23.17. Построение файла *.edmx для большей части базы данных AutoLot В отличие от первого примера с EDM, на этот раз переименование сущностных классов и их свойств не требуется. Отображение хранимой процедуры Теперь рассмотрим один не совсем очевидный аспект мастера EDM Wizard: отметкой имени хранимой процедуры с целью ее включения в концептуальную модель работа не завершается. В этот момент все, что вы сделали — это сообщили IDE-среде о существовании физической хранимой процедуры; чтобы она материализовалась в коде, потребуется импортировать функцию на концептуальный уровень. Откройте браузер модели (выбрав пункт меню View1^Other Windows1^Entity Data Model Browser (Вид^ Другие окна1^ Браузер модели сущностных данных)) и обратите внимание, что хранимая процедура GetPetName видна в физической базе данных (в узле AutoLotModel.Store), но папка Function Imports (Импорты функций), выделенная на рис. 23.18, пока пуста.
882 Часть V. Введение в библиотеки базовых классов .NET Model Browser Type here to search 4 AutoLotDAL_EF.edmx л [£) AutolotModd ' Ш Entity Types t "^J Customer I ^ Inventory f ^J Order lJ Complex Types л LJ Associations 03 FK_Orders_Customers SO FK Orders Inventory EntityContainen AutoLotEntrbes _l Entity Sets :'.J Association Sets ,_J Function Imports -< Qj AutoLotModeLStore ^ Si Tables / Views 1*3 Customers 3 Inventory GJ Orders ^ J Stored Procedures л ^GetPetName фсагТО <§3 petName Li Constraints Начните с отображения этого на концептуальный уровень, щелкнув правой кнопкой мыши на папке Function Imports и выбрав в контекстном меню пункт Add Function Import (Добавить импорт функции), как показано на рис. 23.19. В открывшемся диалоговом окне необходимо выбрать имя физической хранимой процедуры (в раскрывающемся списке Stored Procedure Name (Имя хранимой процедуры)) и указать имя метода для отображения на концептуальную модель. В этом случае имена будут отображаться напрямую. Вспомните, что хранимая процедура на самом деле не возвращает набор записей, однако у нее есть выходной параметр. Поэтому отметьте переключатель None (Нет), как показано на рис. 23.20. Роль навигационных свойств I Model Bro.. Если теперь посмотреть на визуальный конструктор EDM, можно увидеть, что все таблицы учтены, включая новые сущности в разделе Navigation Properties (Навигационные свойства) данного сущностного класса (рис. 23.21). Как следует из названия, навигационные свойства позволяют выражать операции JOIN в программной модели Entity Framework (без необходимости написания сложных SQL-операторов). Чтобы учесть эти отношения внешних ключей, каждая сущность в файле *.edmx теперь содержит некоторые новые данные XML, которые показывают, как сущности соединены между собой через данные ключей. Чтобы просмотреть разметку, откройте файл *.edmx в редакторе XML; тем не менее, некоторая информация видна и в папке Associations (Ассоциации) браузера моделей (рис. 23.22). Рис. 23.18. Хранимые процедуры должны быть импортированы, прежде чем их можно будет вызывать Model Browser Type here to search 4s AutolotDAL_EF.edmx л [£ AutoLotModel * _J Entity Types *^J Customer ■$$ Inventory *% Order L3 Complex Types •* LJ Associations OS FK_Orders_Customers ЙУ FK_Orders_Inventory * 33 EntityContainen AutoLotEntrbes Li Entity Sets 12 Association Sets LJ Function Imports л \J AutoLotModeLStore * Ш Tables /Views 13 Customers - j ■ Ш Inventory C3 Orders ^ t_J Stored Procedures i * S^GetPetName \ jjSjcariD Ф petName _J Constraints Add Function Import... Update Model from Database... Generate Database from Model... Add Code Generation hem... Q Рис. 23.19. Выбор импорта хранимой процедуры в концептуальную модель
Глава 23. ADO.NET, часть III: Entity Framework 883 Function Import Name: I GetPetName ] Stored Procedure Name. GetPetName Returns a Collection Of о None " Scalers: О Complex: Stored Procedure Column Information Get Column Information ! Click on "Get Column Information" above to retrieve the stored procedure's schema. Once the schema is available, click on "Create Ne Complex Type" below to create a compatible complex type. You can also always update an existing complex type to match the returned schema. The changes will be applied to the model once you click on OK. j Uwte Ne* Comple < Tyj <? Рис. 23.20. Отображение физической хранимой процедуры на концептуальную модель AutoLotDALJF.edmx* х| t§CusHD 2Jf FirstName !"lf LastName S Navigation Properties Щ Orders |£ ^OrderTO ^CustTO ■SPCarTO a Navigation Properties Щ Customer Щ Inventory' - a Properties ?§CarID -fMake ^ Color ■•fPetName 3 Navigation Properties ^Orders Рис. 23.21. Навигационные свойства
884 Часть V. Введение в библиотеки базовых классов .NET Model Browser - П X Type here to search p 4 AutaLotDAL_EF.edmx - _5_ AutoLotModet - L_i Entity Types * ^J Customer ^Inventory SJ FK_Orders_Customers 1_Й FK_Ofders_Invefrtory Э EntityContainer: AutoLotEntities I {J AutoLotModet.Store Рис. 23.22. Просмотр отношений между сущностями При желании номер версии этой новой библиотеки можно изменить на 4.0.0.0 (используя кнопку Assembly Information (Информация о сборке) на вкладке Applications (Приложения) окна Properties (Свойства)). Прежде чем перейти к созданию первого клиентского приложения, скомпилируйте модифицированную сборку AutoLot.dll. Исходный код. Проект AutoLotDAL (Version 4) доступен в подкаталоге Chapter 23. Использование навигационных свойств внутри запросов LINQ to Entity Теперь давайте посмотрим, как использовать навигационные свойства внутри контекста запросов LINQ to Entities (их можно также использовать и с Entity SQL, однако в этой книге данная тема не рассматривается). Прежде чем выполнить привязку данных к графическому интерфейсу Windows Forms, потребуется создать еще одно консольное приложение по имени AutoLotEDMClient. Создав проект, установите ссылку на System. Data.Entity.dll и на последнюю версию AutoLotDAL.dll. Добавьте файл App.Config из проекта AutoLotDAL (используя пункт меню Project^Add Existing Item (ПроектеДобавить существующий элемент)) и импортируйте пространство имен AutoLotDAL. Вспомните, что в файле App.Config, сгенерированном с помощью утилиты EdmGen.exe, определена корректная строка соединения, которой требуется для Entity Framework. Давайте обновим физическую таблицу Orders несколькими новыми записями. В частности, необходимо обеспечить, чтобы один заказчик имел несколько заказов. Используя проводник сервера Visual Studio 2010, добавьте в таблицу Orders одну или две новых записи, чтобы гарантировать наличие у одного заказчика двух иди более заказов. Например, на рис. 23.23 заказчик с CustID, равным 4, имеет два отложенных заказа на автомобили с CarlD, равными 1992 и 83. Orders: Query (and ! OrderlD 1 • jiooo 1001 1002 [ 1003 ► 1005 1* \NULL H i 5 ...sqlexpressAutoLot) of 5 CustID 2 3 4 4 NULL ► И ► ■ x И CadD 1000 678 904 1992 83 NULL Рис. 23.23. Один заказчик с несколькими заказами
Глава 23. ADO.NET, часть III: Entity Framework 885 Теперь модифицируем класс Program, добавив новый вспомогательный метод (вызываемый в Main ()). Этот метод использует навигационные свойства для выбора каждого объекта Inventory по заказу для заданного заказчика: private static void PrintCustomerOrders(string custID) { int id = int.Parse(custID); using (AutoLotEntities context = new AutoLotEntities ()) { var carsOnOrder = from о in context .Orders where o.CustID == id select o.Inventory; Console.WriteLine("\nCustomer has {0} orders pending:", carsOnOrder.Count()) ; foreach (var item in carsOnOrder) { Console.WriteLine("-> {0} {1} named {2}.", item.Color, item.Make, item.PetName); } } } Ниже показан вывод, полученный в результате запуска этого приложения (при вызове PrintCustomerOrders() в Main() указан идентификатор заказчика, равный 4): ••••• Navigation Properties ***** Please enter customer ID: 4 Customer has 2 orders pending: -> Pink Saab named Pinky. — -> Rust Ford named Rusty. Здесь в контексте обнаруживается единственная сущность Customer с указанным значением CustID. Найдя заказчика, можно перейти к таблице Inventory для выбора каждого заказанного им автомобиля. Возвращаемым значением запроса LINQ будет перечисление объектов Inventory, которые выводятся на консоль с помощью стандартного цикла foreach. Вызов хранимой процедуры Теперь в EMD-модели AutoLotDAL имеется вся необходимая информация для вызова хранимой процедуры GetPetName. Это можно сделать одним из двух способов: private static void CallStoredProc () { using (AutoLotEntities context = new AutoLotEntities ()) { ObjectParameter input = new ObjectParameter("carlD", 83); ObjectParameter output = new ObjectParameter("petName", typeof(string)); // Вызвать ExecuteFunction на контексте... context.ExecuteFunction("GetPetName", input, output); II... либо использовать строго типизированный метод контекста, context.GetPetName(83, output); Console.WriteLine("Car #83 is named {0}", output.Value); } } Первый подход предусматривает вызов метода ExecuteFunction() на контексте объектов. В этом случае хранимая процедура идентифицируется строковым именем, а каждый параметр представлен объектом типа ObjectParameter, который находится в пространстве имен System.Data.Objects (не забудьте импортировать его в код С#).
886 Часть V. Введение в библиотеки базовых классов .NET Второй подход состоит в использовании строго типизированного имени в контексте объектов. Такой подход значительно проще, потому что входные параметры (такие как идентификатор автомобиля) можно передавать как типизированные данные, а не как объект ObjectParameter. Исходный код. Проект AutoLotEDMClient доступен в подкаталоге Chapter 23. Привязка данных сущностей к графическим пользовательским интерфейсам Windows Forms В завершения знакомства с ADO.NET Entity Framework давайте рассмотрим простой пример, в котором производится привязка сущностных объектов к графическому интерфейсу Windows Forms. Как упоминалось ранее в этой главе, операции привязки данных будут демонстрироваться в проектах WPF и ASP.NET. Создайте новое приложение Windows Forms по имени AutoLotEDMGUI и переименуйте его начальную форму на MainForm. После создания проекта (подобно ранее созданному клиентскому приложению) установите ссылки на сборку System.Data.Entity.dll и на последнюю версию AutoLotDAL.dll. Добавьте файл App.config из проекта AutoLotDAL (используя пункт меню Project^Add Existing Item) и импортируйте пространство имен AutoLotDAL в файл кода главной формы. В визуальном конструкторе форм добавьте элемент управления DataGridView и переименуйте его на grid Invent or у. После помещения этого элемента управления на поверхность визуального конструктора выберите встроенный редактор сетки (щелкнув на маленькой стрелке в верхнем правом углу элемента DataGridView). В раскрывающемся списке Choose Data Source (Выберите источник данных) укажите источник данных проекта (рис. 23.24). В этом случае необходима привязка не напрямую к базе данных, а к сущностному классу, поэтому выберите в качестве типа источника данных Object (рис. 23.25). На завершающем шаге мастера отметьте таблицу Inventory из AutoLotDAL.dll, как показано на рис. 23.26 (если она не видна, значит, не была установлена ссылка на эту библиотеку). MainFarm.cs [Design]* X | Z ■■ вТ а J | Click the 'Add Projed connect to data. 'ate Source...' link to Рис. 23.24. Элемент управления DataGridView в приложении Windows Forms
Глава 23. ADO.NET, часть III: Entity Framework 887 ■ СЬоом a Data Source Type Where will the application get data from? Database Service Object SharePcmt Lets you choose objects that can later be used to generate data-bound controls. j [ Naa> Г ^_*"*» Сави» Рис. 23.25. Привязка к строго типизированному объекту (^М— Data Objects Expand the referenced assemblies and namespaces to select your objects. If an object is missing from a referenced assembly, cancel the wizard and rebuild the project that contains the object. What objects do you want to bind to? Ё1Э AutoLotEDM.GUI i H'OAutoLotDAL Q{> AutoLotConnectedLayer л Щ{) AutoLotOAL O^t AutoLotDataSet Q*^ AutoLotEntities И^> Customer •; Inventory ПЩ Order "W И {} AutolotDAL.AutoLotDataSetTableAdapters И {} AutoLotDisconnectedLayer [ Add Ref степса J] \J\ Hide system assemblies Рис. 23.26. Выбор таблицы Inventory После щелчка на кнопке Finish (Пэтово) сетка DataGridView отобразит все свойства сущностного класса Inventory, включая и навигационные. Чтобы удалить их из сетки, снова активизируйте встроенный редактор и щелкните на ссылке Edit Columns... (Редактировать столбцы). В диалоговом окне Edit Columns (Редактирование столбцов) выберите столбец Orders и удалите его из представления (рис. 23.27). В завершение добавьте на форму элемент управления Button и переименуйте его на btnUpdate. После этого поверхность визуального конструктора должен выглядеть примерно так, как показано на рис. 23.28.
888 Часть V. Введение в библиотеки базовых классов .NET Bound Column Properties [ЩВВН CcntextMenuStrip MaxInputLength ReadOnfy Resizable SortMode л Data DataPropertyName л Design (Name) ordersDataGridViewTextBox< , DataGndViewTextBoxColum » J 1 K*""** (Name) Indicates the name used in code to identify the object. Рис. 23.27. Настройка внешнего вида DataGridView Рис. 23.28. Окончательный вид пользовательского интерфейса Добавление кода привязки данных К этому моменту имеется экранная сетка, которая может отображать любое количество объектов Inventory; однако для этого нужно еще написать соответствующий код. Благодаря исполняющей среде EF это очень просто. Начнем с обработки событий FormClosed и Load класса MainForm (используя окно Properties (Свойства)) и события Click элемента управления Button. Модифицируем файл кода следующим образом: public partial class MainForm : Form { AutoLotEntities context = new AutoLotEntities (); public MainForm() InitializeComponent ();
Глава 23. ADO.NET, часть III: Entity Framework 889 private void MainForm_Load(object sender, EventArgs e) { // Привязать коллекцию ObjectSet<Inventory> к сетке. gridlnventory.DataSource = context.Inventories; } private void btnUpdate_Click(object sender, EventArgs e) { context.SaveChanges (); MessageBox.Show("Data saved!"); } private void MainForm_FormClosed(object sender, FormClosedEventArgs e) { context.Dispose () ; } } Вот и все, что потребуется сделать. Запустив приложение, можно добавлять новые записи в сетку, выбирать строки и удалять их, а также модифицировать существующие строки. Щелчок на кнопке Update (Обновить) приводит к автоматическому обновлению таблицы Inventory, потому что контекст объектов достаточно интеллектуален, чтобы генерировать необходимые SQL-операторы для автоматический выборки, обновления, удаления и вставки. На рис. 23.29 показано готовое приложение Windows Forms. г iJ Entity Data Binding A ; CarlD !°Г ...... ■ ^— [904 1ССЮ < Upd-! | Make Ford Ford Yugo VW BMW III Gator Red Yellow Green Back Black m 'itUm Pet Name Snake Buzz Clunker Hank Bimmer i» = Рис. 23.29. Готовое приложение Windows Forms Ниже перечислено несколько важных моментов, связанных с предыдущим приложением. • Контекст остается в памяти на протяжении работы приложения. • Вызов context .Inventories выполняет SQL-код для извлечения всех строк из таблицы Inventory в память. • Контекст отслеживает все грязные сущности, так что знает, какой SQL-код следует выполнить при вызове SaveChanges(). • После вызова SaveChanges () сущности снова считаются чистыми. На этом рассмотрение ADO.NET Entity Framework завершено. Хотя об этой программной модели можно рассказать много больше, чем уместилось в настоящей главе, вы получили достаточное представление о задачах, которые EF пытается решить, и об общей программной модели. За дополнительной информацией по API-интерфейсу EF обращайтесь в документацию .NET Framework 4.0 SDK.
890 Часть V. Введение в библиотеки базовых классов .NET Резюме В этой главе рассмотрением роли Entity Framerowk завершены формальные исследования программирования баз данных с использованием ADO.NET. Технология EF позволяет программировать на уровне концептуальной модели, которая близко отражает предметную область. Хотя можно изменять форму сущностей как угодно, исполняющая среда EF гарантирует, что измененные данные модели будут отображены на корректные данные физической таблицы. В главе рассказывалось о роли (и составе) файлов *.edmx, а также о том, как их генерировать с помощью IDE-среды Visual Studio 2010. Кроме того, было показано, как отображать хранимые процедуры на функции концептуального уровня, как применять запросы LINQ к объектной модели и как потреблять извлеченные данные на самом низком уровне, используя EntityDataReader. Рассматривалась также и роль Entity SQL. Завершил главу простой пример привязки сущностных классов к графическому пользовательскому интерфейсу с использованием Windows Forms. Когда речь пойдет о Windows Presentation Foundation и ASP.NET, будут продемонстрированы и другие примеры привязки сущностей к приложениям с графическим интерфейсом.
ГЛАВА 24 Введение в LINQ to XML Разработчики .NET сталкиваются с данными в формате XML повсеместно. Конфигурационные файлы для обычных и веб-приложений хранят информацию в виде XML. Объекты DataSet из ADO.NET могут легко сохранять (или загружать) данные в формате XML. Технологии Windows Presentation Foundation, Silverlight и Windows Workflow Foundation — все они используют основанную на XML грамматику (XAML) для представления настольных пользовательских интерфейсов, браузерных пользовательских интерфейсов и рабочих потоков, соответственно. В библиотеке Windows Communication Foundation (а также в первоначальных API-интерфейсах удаленного взаимодействия .NET) многочисленные установки хранятся в формате строк XML. Хотя XML распространен повсюду, исторически сложилось так, что программирование с использованием XML утомительно, громоздко и очень сложно, если вы не знакомы с большим количеством XML-технологий (XPath, XQuery, XSLT, DOM, SAX и т.п.). Еще в самом первом выпуске .NET появилась специфическая сборка System.Xml.dll, ориентированная на программирование для документов XML. В ней находятся множество пространств имен и типов для различных технологий программирования XML, а также несколько специфичных для .NET API-интерфейсов XML, таких как классы XmlReader/ XmlWriter. В наши дни большинство программистов предпочитают взаимодействовать с XML- данными посредством API-интерфейса LINQ to XML. В этой главе будет показано, что программная модель LINQ to XML позволяет выражать структуру XML-данных в коде и предлагает намного более простой способ создания, манипулирования, загрузки и сохранения XML-данных. Помимо применения LINQ to XML для простого создания XML- документов, с помощью выражений запросов LINQ можно также быстро извлекать информацию из этих документов. История о двух API-интерфейсах XML Когда впервые появилась платформа .NET, программисты могли манипулировать XML-документами, используя типы из сборки System.Xml.dll. Содержащиеся в ней пространства имен и типы позволяли генерировать XML-данные в памяти и сохранять их на диске. Кроме того, сборка System.Xml.dll предоставляла типы, с помощью которых производится загрузка XML-документа в память, поиск в нем специфических узлов, проверка документа на соответствие заданной схеме и решение других распространенных задач. Хотя эта исходная библиотека успешно применялась во многих проектах .NET, работа с ее типами была несколько запутанной, поскольку программная модель не имела отношения к структуре самого XML-документа. Например, предположим, что нужно создать файл XML в памяти и сохранить его в файловой системе. В случае использования типов
892 Часть V. Введение в библиотеки базовых классов .NET System.Xml.dll можно написать код, подобный показанному ниже (предварительно понадобится создать новое консольное приложение по имени LinqToXmlFirstLook и импортировать пространство имен System.Xml): private static void BuildXmlDocWithDOM() { // Создать новый документ XML в памяти. XmlDocument doc = new XmlDocument (); // Заполнить документ корневым элементом по имени <Inventory>. XmlElement inventory = doc.CreateElement("Inventory"); // Теперь создать подэлемент по имени <Саг> с атрибутом ID. XmlElement car = doc.CreateElement("Car"); car.SetAttribute("ID", 000") ; // Построить данные внутри элемента <Саг>. XmlElement name = doc.CreateElement("PetName"); name.InnerText = "Jimbo"; XmlElement color = doc.CreateElement("Color"); color.InnerText = "Red"; XmlElement make = doc.CreateElement("Make"); make.InnerText = "Ford"; // Добавить элементы <PetName>, <Color> и <Make> в элемент <Саг>. car.AppendChild(name) ; car.AppendChild(color) ; car.AppendChild(make); // Добавить элемент <Саг> к элементу <Inventory>. inventory.AppendChild(car) ; // Вставить полный XML в объект XmlDocument и сохранить в файле, doc.AppendChild(inventory) ; doc.Save ( "Inventory.xml"); } После вызова этого метода созданный им файл Inventory.xml (находящийся в папке bin\Debug) будет содержать следующие данные: <Inventory> <Car ID=000"> <PetName>Jimbo</PetName> <Color>Red</Color> <Make>Ford</Make> </Car> </Inventory> Хотя этот метод работает, как ожидается, нужно сделать несколько замечаний. Программная модель System.Xml.dll — это реализация Microsoft спецификации объектной модели документа W3C Document Object Model (DOM). Согласно этой модели, документ XML строится "вверх дном". Сначала создается документ, затем подэлементы и, наконец, элементы добавляются в документ. Чтобы выразить это в коде, потребуется написать довольно много вызовов функций классов из XmlDocument и XmlElement (помимо прочих). В данном примере для построения очень простого XML-документа понадобилось 16 строк кода (без учета комментариев). Создание более сложных документов с помощью сборки System.Xml.dll требует написания гораздо большего объема кода. Хотя этот код определенно можно упростить, выполняя построение узлов посредством циклических и условных конструкций, факт остается фактом — тело кода имеет минимум визуального отражения финального дерева XML.
Глава 24. Введение в LINQ to XML 893 Интерфейс LINQ to XML как лучшая модель DOM API-интерфейс LINQ to XML предлагает альтернативный способ построения, манипулирования и опроса XML-документов, который использует намного более функциональный подход, чем модель DOM из System.Xml. Вместо построения XML-документа за счет индивидуальной сборки элементов и обновления дерева XML через набор вызовов функций, код пишется сверху вниз: private static void BuildXmlDocWithLINQToXml() 1 // Создание XML-документа в более 'функциональной' манере. XElement doc = new XElement("Inventory", new XElement("Car", new XAttribute ("ID", 000"), new XElement("PetName", "Jimbo"), new XElement("Color", "Red"), new XElement("Make", "Ford") ) ) ; // Сохранить в файл. doc.Save("InventoryWithLINQ.xml"); } Здесь используется новый набор типов из пространства имен System.Xml.Linq, a именно — XElement и XAttribute. В результате вызова метода BuildXmlDocWithLINQToXml() получаются те же данные XML, но на этот раз с гораздо меньшими усилиями. Обратите внимание, что благодаря тщательно выстроенным отступам, исходный код теперь имеет ту же структуру, что и результирующий XML-документ. Это очень удобно и само по себе, но к тому же заметьте, насколько компактнее этот код по сравнению с предыдущим примером (сэкономлено около 10 строк). Здесь не применяются какие-либо выражения запросов LINQ, а просто с помощью типов из пространства имен System.Xml.Linq генерируется находящийся в памяти XML-документ, который затем сохраняется в файл. Фактически API-интерфейс LINQ to XML использовался как лучшая модель DOM, Далее в этой главе будет показано, что классы из System.Xml.Linq поддерживают LINQ и могут быть целью для той же разновидности запросов LINQ, которые рассматривались в главе 13. По мере изучения LINQ to XML, скорее всего, вы начнете отдавать предпочтение этому API-интерфейсу перед начальными библиотеками XML в .NET. Тем не менее, это не значит, что вы вообще перестанете пользоваться пространством имен из System.Xml.dll. Просто шансы выбора System.Xml.dll в новых проектах значительно уменьшатся. Синтаксис литералов Visual Basic как наилучший интерфейс LINQ to XML Прежде чем приступить к формальному ознакомлению с LINQ to XML с точки зрения С#, имеет смысл кратко упомянуть о том, что язык Visual Basic переносит функциональный подход этого API-интерфейса на следующий уровень. В Visual Basic можно определять литералы XML, которые позволяют присваивать XElement потоку встроенной разметки XML прямо в коде. Предполагая наличие проекта VB, можно создать следующий метод: Public Class XmlLiteralExample Public Sub MakeXmlFileUsingLiterals () ' Обратите внимание на возможность встраивания данных XML в XElement.
894 Часть V. Введение в библиотеки базовых классов .NET Dim doc As XElement = _ <Inventory> <Car ID=000"> <PetName>Jimbo</PetName> <Color>Red</Color> <Make>Ford</Make> </Car> </Inventory> ' Сохранить в файл. doc.Save("InventoryVBStyle.xml") End Sub End Class Когда компилятор VB обрабатывает литерал XML, он отображает данные XML на лежащую в основе корректную объектную модель LINQ to XML. Фактически, при работе с LINQ to XML в проекте VB среда IDE уже понимает, что литеральный синтаксис XML — это сокращенная нотация соответствующего кода. Обратите внимание, что на рис. 24.1 применяется операция точки к конечному дескриптору </ Invent or y> и видны те же самые члены, которые отображаются после применения операции точки к строго типизированному XElement. XmlLrteraJExampltvb* X I ;XmlLJtmExampte ■ ^Public Class X*lliteralE*aeple Public Sub MakeX«lFileUsingLiterals() * Motice that we can inline XML data ' to an XEleeent. Di* doc As XFleawnt - _ <Inventory> <Car ID«'*ieee-> <PetMa»e>3inbo< /PetNawe > <Color> Red</Color > <Hake>Ford</Hake> </Car> </ Inventory >.j ; ЫЛЛааи .- WUaafUfanfc End Sub End Class SaveClnvento;^^ ф i«# Add ♦ AddAfterSdf ♦ AddAnnotation ♦ AddBef oreSeif V AddFirrt ♦ Ancestors ♦ AncestorsAndSelf ♦ Annotation ♦ Annotations i * Attribute ■ V Att r i b utes Common All Рис. 24.1. Литеральный синтаксис VB XML — сокращенная нотация работы с объектной моделью LINQ to XML Хотя книга посвящена языку программирования С#, некоторые разработчики считают, что поддержка XML в VB непревзойденна. Даже если вы из тех, кто не может представить работу с языком из семейства BASIC, все равно рекомендуется изучить литеральный синтаксис VB в документации .NET Framework 4.0 SDK. Все операции манипуляций с данными XML можно вынести в отдельную сборку *.dll, так что вполне допускается применять для этого VB!
Глава 24. Введение в UNO to XML 895 Члены пространства имен System. Xml. Linq Несколько неожиданно, но в основной сборке LINQ to XML (System.Xml.Linq.dll) определено весьма небольшое количество типов в трех разных пространствах имен: System.Xml.Linq, System.Xml.Schema и System.Xml. XPath (рис. 24.2). Основное пространство имен System.Xml.Linq содержит легко управляемый набор классов, представляющих различные аспекты документа XML (элементы и атрибуты, пространства имен XML, комментарии XML, инструкции обработки и т.п.). В табл. 24.1 описаны избранные члены System. Xml. Linq. На рис. 24.3 показана цепочка наследования ключевых типов классов. Таблица 24.1. Избранные члены пространства имен System. Xml. Linq A jgj * 1 л О System.Xml.Linq !> ^ Extensions t> J/1 LcadOptions . и* ReaderOptions ' лГ* SaveOptions D -% XAttribute \> -fJXCData 0 ^% XComment > Щ$ XContainer '• ^Ц XDeclaration > *\t XDccument > -ij XDocumentType ! *tf XEIement > ij XName i- Щ% XNamespace > ^$ XNode > *\£ XNcdeDocumentOrderCcmparer £> ^$ XNodeEqualit) Comparer t> % XObject t> a#* XObjectChange 1> *i XObjectChangeEventArgs l> *ft XProcessinglnstruction t> ^ XStreamingElement :• «IjXText > {} System.Xml.Schema 1 £> {} System.Xml.xPath Рис. 24.2. Пространства имен System.Xml.Linq.dll Член Назначение XAttribute XCData XComment XDeclaration XDocument XEIement XName XNamespace XNode XProcessinglnstruction XStreamingElement Представляет XML-атрибут конкретного элемента XML Представляет раздел CDATA в документе XML. Информация раздела CDATA представляет данные в документе XML, которые должны быть включены, но не отвечают правилам грамматики XML (например, код сценария) Представляет комментарий XML Представляет открывающее объявление документа XML Представляет целиком весь документ XML Представляет определенный элемент внутри документа XML, включая корневой Представляет имя XML-элемента или XML-атрибута Представляет пространство имен XML Представляет абстрактную концепцию узла (элемент, комментарий, тип документа, инструкция обработки или текстовый узел) в дереве XML Представляет инструкцию обработки XML Представляет элементы в дереве XML, которые поддерживают отложенный потоковый вывод Осевые методы LINQ to XML В дополнение к классам X* в пространстве имен System.Xml.Linq определен класс по имени Extensions. В этом классе определен набор расширяющих методов, которые обычно расширяют IEnumerable<T>, где Т — некоторый наследник XNode или XContainer. В табл. 24.2 описаны важные расширяющие методы, о которых следует знать (они очень удобны при работе с запросами LINQ).
896 Часть V. Введение в библиотеки базовых классов .NET XText Class •¥ XNode XCData Class -»XText J b : XOfrjerf HLinsIn'o * i Abstract Class , с , L... j XNode i Abstract Class i -»XC*ject : -J ' ST ... * fy J XProcessinginst Class ruction g ■♦XNode 1-е > XAttribute Class -frXObject XCommenl Class • -►XNode ®> ^ | JfToflfainer i Abstract Class 1 -» XNode T XDocument (D | Class -» XContainer i 1 XDocument Type Class 4 XNode , D-'mlSenaliiable XEJement A Class •♦ XContainer . . IV"'' Рис. 24.3. Иерархия базовых классов LINQ to XML Таблица 24.2. Избранные члены класса LINQ to XML Член Назначение Ancestor<T>() Возвращает отфильтрованную коллекцию элементов, содержащих наследников каждого узла в исходной коллекции Attributes () Возвращает отфильтрованную коллекцию атрибутов каждого элемента исходной коллекции DescendantNodes<T> Возвращает коллекцию узлов-наследников каждого документа и элемента исходной коллекции Descendants<T> Возвращает отфильтрованную коллекцию элементов, которые содержат элементы-наследники каждого элемента и документа в исходной коллекции Elements<T> Возвращает коллекцию дочерних элементов каждого элемента и документа исходной коллекции Nodes<T> Возвращает коллекцию дочерних узлов каждого документа и элемента в исходной коллекции Remove () Удаляет каждый атрибут исходной коллекции из родительского элемента Remove<Т> () Удаляет все вхождения заданного узла в исходной коллекции Как можно понять из названий, эти методы позволяют опрашивать загруженное дерево XML в поисках элементов, атрибутов и их значений. Все вместе такие методы называются осевыми методами или просто осями. Эти методы можно применить непосредственно к частям дерева или узлам либо использовать их для построения более сложных запросов LINQ. На заметку! Абстрактный класс XContainer поддерживает множество методов, которые имеют имена, идентичные членам класса Extensions. Класс XContainer является родительским как для XElement, так и для XDocument, и потому они оба поддерживают общую функциональность.
Глава 24. Введение в LINQ to XML 897 Примеры использования этих осевых методов будут встречаться на протяжении оставшейся части главы. А сейчас рассмотрим краткий пример: private static void DeleteNodeFromDoc() { XElement doc = new XElement("Inventory", new XElement ("Car", new XAttnbute ("ID", 000"), new XElement("PetName", "Jimbo"), new XElement("Color", "Red"), new XElement("Make", "Ford") ) ) ; // Удалить элемент PetName из дерева. doc.Descendants("PetName").Remove(); Console.WriteLine(doc); } В результате вызова этого метода получается следующее дерево XML: <Inventory> <Car ID=000"> <Color>Red</Color> <Make>Ford</Make> </Car-> </Inventory^ Избыточность XName (и XNamespace) Если посмотреть на сигнатуру осевых методов LINQ to XML (или идентично именованных членов XContainer), можно заметить, что обычно они требуют указания того, что определяется как объект XName. Взгляните на сигнатуру метода Descendants (), определенного в XContainer: public IEnumerable<XElement> Descendants(XName name) Метод XName является "избыточным" в том смысле, что он никогда не будет использоваться в коде. Фактически, поскольку этот класс не имеет общедоступного конструктора, объект типа XName создавать нельзя: // Ошибка! Нельзя создавать объекты XName! doc.Descendants(new Xname("PetName")) .Remove (); Посмотрев на формальное определение XName, вы обнаружите, что этот класс определяет специальную операцию неявного преобразования (специальные операции преобразования рассматриваются в главе 12), которая отобразит простой тип System.String на объект XName: // На самом деле "за кулисами" создается XName! doc.Descendants("PetName").Remove(); На заметку! Класс XName space также поддерживает подобного рода неявное преобразование строк. Важно отметить, что при работе с осевыми методами можно применять текстовые значения для представления имен элементов или атрибутов и позволять API-интерфейсу LINQ to XML отображать строковые данные на необходимые типы объектов. Исходный код. Проект LinqToXmlFirstLook доступен в подкаталоге Chapter 24.
898 Часть V. Введение в библиотеки базовых классов .NET Работа с XElement и XDocument Продолжим исследования LINQ to XML на примере нового консольного приложения по имени ConstructingXmlDocs. После его создания импортируйте пространство имен System.Xml.Linq в начальный файл кода. Как уже было показано, XDocument представляет полный XML-документ в программной модели LINQ to XML, поскольку он может использоваться для определения корневого элемента, всех содержащихся в нем элементов, инструкций обработки и объявлений XML. Ниже показан еще один пример построения данных XML с применением XDocument: static void CreateFullXDocument () { XDocument inventoryDoc = new XDocument ( new XDeclaration (. 0", "utf-8", "yes"), new XComment("Current Inventory of cars1"), new XProcessinglnstruction("xml-stylesheet", "href='MyStyles.ess' title='Compact' type='text/ess'"), new XElement("Inventory", new XElement("Car", new XAttribute ( "ID" , "), new XElement("Color", "Green"), new XElement("Make", "BMW"), new XElement("PetName", "Stan") ) , new XElement("Car", new XAttribute ( "ID", "), new XElement("Color", "Pink"), new XElement("Make", "Yugo"), new XElement("PetName", "Melvin") ) ) ) ; // Сохранить на диск. inventoryDoc.Save("Simplelnventory.xml"); } Обратите внимание, что конструктор объекта XDocument на самом деле представляет собой дерево дополнительных объектов LINQ to XML. Вызываемый здесь конструктор принимает в качестве первого параметра XDeclaration, за которым следует параметр- массив объектов (вспомните, что параметры-массивы позволяют передавать разделенные запятыми списки аргументов, которые "за кулисами" упаковываются в массив): public XDocument(System.Xml.Linq.XDeclaration declaration, params object[] content) После вызова этого метода в Main() в файл Simplelnventory.xml будут записаны следующие данные: <?xml version="l.0" encoding="utf-8" standalone="yes"?> <!—Current Inventory of cars'--> <?xml-stylesheet href='MyStyles.ess' title='Compact' type='text/ess'?> <Inventory> <Car ID="> <Color>Green</Color> <Make>BMW</Make> <PetName>Stan</PetName> </Car> <Car ID="> <Color>Pink</Color> <Make>Yugo</Make> <PetName>Melvin</PetName> </Car> </Inventory>
Глава 24. Введение в LINQ to XML 899 Как выясняется, объявление XML по умолчанию для любого XDocument предусматривает использование кодировки utf-8, версии XML 1.0 и автономного документа. Таким образом, можно полностью удалить создание объекта XDeclaration и получить те же данные; учитывая, что почти любой документ требует одного и того же объявления, применять XDeclaration приходится нечасто. Если определять инструкции обработки или специальное объявление XML не нужно, можно вообще избежать использования XDocument и просто применять XElement. Помните, что XElement может использоваться для представления корневого элемента документа XML и всех подобъектов. Итак, сгенерировать прокомментированный список складских запасов можно следующим образом: static void CreateRootAndChildren() { XElement inventoryDoc = new XElement("Inventory", new XComment("Current Inventory of cars1"), new XElement("Car", new XAttribute("ID", "), new XElement("Color", "Green"), new XElement("Make", "BMW"), new XElement("PetName", "Stan") ), new XElement("Car", new XAttribute ( "ID", "), new XElement("Color", "Pink"), new XElement("Make", "Yugo"), new XElement("PetName", "Melvin") ) ) ; // Сохранить на диск. inventoryDoc.Save("Simplelnventory.xml"); } Вывод будет практически идентичным, исключая инструкцию обработки для гипотетической таблицы стиля: <?xml version="l.О" encoding="utf-8"?> <Inventory> <'—Current Inventory of cars!--> <Car ID="> <Color>Green</Color> <Make^BMW</Make> <PetName>Stan</PetName> </Car> <Car ID="> <Color>Pink</Color> <Make>Yugo</Make> <PetName>Melvin</PetName> </Car> </Inventory> Генерация документов из массивов и контейнеров До сих пор XML-документы строились с использованием жестко закодированных значений для конструктора. Но гораздо чаще придется генерировать XElement (или XDocument), читая данные из массивов, объектов ADO.NET, файлов данных и т.п. Один из способов отображения данных из памяти на новый XElement предусматривает применение стандартных циклов for для перемещения данных в объектную модель LINQ to XML. Хотя это определенно возможно, проще будет встроить запрос LINQ в конструкцию XElement непосредственно.
900 Часть V. Введение в библиотеки базовых классов .NET Предположим, что имеется анонимный массив анонимных классов (просто чтобы сократить объем кода в этом примере; подойдет также любой массив, List<T> или другой контейнер). Отобразить эти данные на XElement можно было бы следующим образом: static void MakeXElementFromArray () { // Создать анонимный массив анонимных типов, var people = new[] { new { FirstName = "Mandy", Age =32}, new { FirstName = "Andrew", Age = 40 }, new { FirstName = "Dave", Age = 41 }, new { FirstName = "Sara", Age = 31} }; XElement peopleDoc = new Xelement("People", from с in people select new XElement("Person", new XAttribute("Age", с Age), new XElement("FirstName", с.FirstName)) ) ; Console.WriteLine(peopleDoc); } Здесь объект peopleDoc определяет корневой элемент <Реор1е> с результатами запроса LINQ. Этот запрос LINQ создает новый XElement на основе каждого элемента массива people. Если встроенный запрос покажется трудно читабельным, можете разделить этот процесс на отдельные шаги, как показано ниже: static void MakeXElementFromArray () { // Создать анонимный массив анонимных типов, var people = new[] { new { FirstName = "Mandy", Age =32}, new { FirstName = "Andrew", Age =40 }, new { FirstName = "Dave", Age = 41 }, new { FirstName = "Sara", Age = 31} }; var arrayDataAsXElements = from с in people select new XElement("Person", new XAttribute("Age", c.Age), new XElement("FirstName", с.FirstName)); XElement peopleDoc = new XElement("People", arrayDataAsXElements); Console.WriteLine(peopleDoc); } В любом случае вывод будет выглядеть следующим образом: <Реор1е> <Person Age=2"> <FirstName>Mandy</FirstName> </Person> <Person Age=0"> <FirstName>Andrew</FirstName> </Person> <Person Age=1"> <FirstName>Dave</FirstName> </Person> <Person Age=1"> <FirstName>Sara</FirstName> </Person> </People>
Глава 24. Введение в LINQ to XML 901 Загрузка и разбор XML-содержимого Оба типа — XElement и XDocument — поддерживают методы Load() и Parse(), которые позволяют наполнить объект XML содержимым из объекта-строки, содержащего данные XML или внешние файлы XML. Рассмотрим следующий метод, в котором иллюстрируются оба подхода: static void ParseAndLoadExistingXml() { // Построить XElement из строки. string myElement = @"<Car ID ='3'> <Color>Yellow</Color> <Make>Yugo</Make> </Car>"; XElement newElement = XElement.Parse(myElement); Console.WriteLine(newElement); Console.WriteLine(); // Загрузить файл Simplelnventory.xml. XDocument myDoc = XDocument.Load("Simplelnventory.xml"); Console.WriteLine(myDoc); } Исходный код. Проект ConstructingXmlDocs доступен в подкаталоге Chapter 24. Манипулирование XML-документом в памяти К настоящему моменту были продемонстрированы разные способы использования LINQ to XML для создания, сохранения, разора и загрузки данных XML. Следующий аспект LINQ to XML, с которым нужно познакомиться — это навигация по документу для нахождения и изменения определенных элементов дерева на основе использования запросов LINQ и осевых методов LINQ to XML. Для этого построим приложение Windows Forms, которое отобразит данные из документа XML, сохраненного на жестком диске. Графический интерфейс позволит пользователю вводить данные для нового узла, который будет добавлен к тому же XML- документу. Наконец, пользователю будет предоставлено несколько способов выполнения поиска в документе посредством нескольких запросов LINQ. На заметку! Учитывая, что некоторые запросы LINQ уже строились в главе 13, здесь они повторяться не будут. В разделе "Querying XML Trees" ("Выдача запросов к деревьям XML") документации .NET Framework 4.0 SDK можно посмотреть ряд дополнительных специфических примеров LINQ to XML Построение пользовательского интерфейса приложения LINQ to XML Создайте приложение Windows Forms под названием LinqToXmlWinApp и измените имя начального файла For ml. с s на MainForm.cs (в проводнике решения). Графический пользовательский интерфейс этого приложения довольно прост. В левой части окна находится элемент управления Text Box (по имени txt Inventory), у которого свойство Multiline установлено в true, а свойство ScrollBars — в Both. Вдобавок также есть группа простых элементов TextBox (txtMake, txtColor и txtPetName) и элемент Button по имени btnAddNewItem, щелчок на котором приводит к добавлению нового элемента к документу XML. Наконец, есть еще одна группа элементов управления
902 Часть V. Введение в библиотеки базовых классов .NET (TextBox по имени txtMakeToLookUp и еще один Button по имени btnLookUpColors), который позволяет запросить из документа XML подмножество определенных узлов. Возможная компоновка представлена на рис. 24.4. ■1шш „у М1ш^иммшям м,,пю„п.~ ш^пГ^тшшш^я^^шшшт Чв FunwithUNQtoXML В бзД Current Inventory I * Add inventory Item Make Color Pet Name Г ш 1 Look up Colors for Make Make to Look Up BMW | LookUpCotor» I I , __ о ! ! Рис. 24.4. Графический пользовательский интерфейс приложения LINQ to XML Потребуется обработать событие Click каждой кнопки для генерации методов обработки событий, а также обработать событие Load самой формы. Чуть ниже мы займемся этим. Импорт файла Inventory, xml В состав кода примеров для этой главы включен файл Inventory.xml, в котором имеется набор элементов внутри корневого элемента <Inventory>. Импортируйте этот файл в проект, выбрав пункт меню Project^pAdd Existing Item (ПроектерДобавить существующий элемент). Если вы посмотрите на данные, то увидите, что корневой элемент определяет набор элементов <Саг>, каждый из которых определен примерно так: <Car carlD =,,0"> <Make>Ford</Make> <Color>Blue</Color> <PetName>Chuck</PetName> </Car> Прежде чем продолжить, выберите этот файл в Solution Explorer и затем в окне Properties (Свойства) установите свойство Copy to Output Directory (Копировать в выходной каталог) в Copy Always (Копировать всегда). Это гарантирует, что данные будут размещены в папке bin\Debug после компиляции приложения. Определение вспомогательного класса LINQ to XML Чтобы изолировать данные LINQ to XML, добавьте в проект новый класс по имени LinqToXmlObjectModel. В этом классе будет определен набор статических методов, инкапсулирующих некоторую логику LINQ to XML. Для начала определите метод, возвращающий заполненный XDocument на основе содержимого файла Inventory.xml (не забудьте импортировать пространства имен System.Xml.Linq и System.Windows. Forms в этот новый файл): р
Глава 24. Введение в LINQ to XML 903 public static XDocument GetXmllnventory () { try { XDocument inventoryDoc = XDocument.Load("Inventory.xml"); return inventoryDoc; } catch (System.10.FileNotFoundException ex) { MessageBox.Show(ex.Message); return null; } } Метод InsertNewElementO, показанный ниже, принимает значения элементов управления TextBox раздела Add Inventory Item (Добавить элемент на склад), чтобы поместить новый узел внутри элемента < Invent or y>, используя осевой метод Descendants (). После этого можно сохранить документ. public static void InsertNewElement(string make, string color, string petName) { // Загрузить текущий документ. XDocument inventoryDoc = XDocument.Load("Inventory.xml"); // Сгенерировать случайное число для идентификатора. Random r = new Random () ; // Создать новый XElement из входных параметров. XElement newElement = new XElement("Car", new XAttribute("ID", r.Next E0000)), new XElement ("Color", color), new XElement("Make", make), new XElement("PetName", petName)); // Добавить в объект в памяти. inventoryDoc.Descendants("Inventory") .First () .Add(newElement); // Сохранить на диске. inventoryDoc.Save("Inventory.xml"); } Последний метод — LookUpColorsForMakeO — принимает данные из последнего элемента TextBox для построения строки, которая содержит цвета, используемые определенным изготовителем, с помощью запроса LINQ. Взгляните на следующую реализацию: public static void LookUpColorsForMake(string make) { // Загрузить текущий документ. XDocument inventoryDoc = XDocument.Load("Inventory.xml"); // Найти цвета заданного изготовителя. var makelnfo = from car in inventoryDoc.Descendants("Car") where (string)car.Element("Make") == make select car.Element("Color").Value; // Построить строку, представляющую каждый цвет, string data = string.Empty; foreach (var item in makelnfo.Distinct()) { data += string.Format("- {0}\n", item); } // Показать цвета. MessageBox.Show(data, string.Format("{0} colors:", make)); }
904 Часть V. Введение в библиотеки базовых классов .NET Оснащение пользовательского интерфейса вспомогательными методами Теперь все, что осталось сделать — наполнить кодом обработчики событий. Задача решается лишь вызовами статических вспомогательных методов: public partial class MainForm : Form { public MainForm () { InitializeComponent (); } private void MainForm_Load(object sender, EventArgs e) { // Отобразить текущий XML-документ склада в элементе управления TextBox. txtlnventory.Text = LinqToXmlObjectModel.GetXmllnventory() .ToString (); } private void btnAddNewItem_Click(object sender, EventArgs e) { // Добавить новый элемент к документу. LinqToXmlObjectModel.InsertNewElement(txtMake.Text, txtColor.Text, txtPetName.Text) / // Отобразить текущий XML-документ склада в элементе управления TextBox. txtlnventory.Text = LinqToXmlObjectModel.GetXmllnventory().ToString(); } private void btnLookUpColors_Click(object sender, EventArgs e) { LinqToXmlObjectModel.LookUpColorsForMake(txtMakeToLookUp.Text); } } Конечный результат показан на рис. 24.5. *J Fun with LJNQ to XML Current Inventory <CarcarlD»"Q"> < Make > Ford c/Make> <Cokr>BKj«</Color> <PetName>Chuck</PetName> </Car> <СагсатШ»"Т> <Make>VW</Make> cCotor>Slver</C< cPetName>Maryc/Pi BMW colors; </Car> <CarcarlD-"> <Make>Yugoo <Со1ог>Ртк</Сок)гз||| -Black - Green Add Inventory lem Color Green PetName Green* I *" Look up Colors for Make Make to Look Up BMW Look Up Colon I 1 Рис. 24.5. Готовое приложение LINQ to XML На этом начальное знакомство с LINQ to XML и изучение LINQ завершено. Впервые вы встретились с технологией LINQ в главе 13, где шла речь о LINQ to Objects. В главе 19 приводились различные примеры использования PLINQ, а в главе 23 рассматривалось применение LINQ к объектам ADO.NET Entity. Теперь вы готовы и должны двигаться дальше. В Microsoft ясно дали понять, что LINQ будет развиваться вместе с развитием платформы .NET.
Глава 24. Введение в LINQ to XML 905 Исходный код. Проект LinqToXmlWinApp доступен в подкаталоге Chapter 24. Резюме В этой главе рассматривалась роль LINQ to XML. Как можно было увидеть, этот API- интерфейс является альтернативой начальной библиотеке для манипуляций XML, которая поставлялась в составе платформы .NET, а именно — System.Xml.dll. Используя System.Xml.Linq.dll, можно генерировать новые документы XML, используя подход "сверху вниз", при котором структура кода очень напоминает конечные данные XML. В этом свете LINQ to XML — лучшая объектная модель документа (DOM). Также было показано, как строить объекты XDocument и XElement различными путями (разбором, загрузкой из файла, отображением объектов в памяти) и как выполнять навигацию и манипуляции данными с применением запросов LINQ.
ГЛАВА 25 Введение в Windows Communication Foundation В версии .NET 3.0 был представлен API-интерфейс, специально предназначенный для построения распределенных систем — Windows Communication Foundation (WCF). В отличие от других распределенных API-интерфейсов, которые, возможно, приходилось применять в прошлом (например, DCOM, .NET Remoting, веб-службы XML, очереди сообщений), WCF предлагает единую, унифицированную и расширяемую программную объектную модель, которая может использоваться для взаимодействия с множеством ранее разрозненных технологий. Эта глава начинается с определения потребностей, которые призван удовлетворить WCF, и рассмотрения задач, которые данный интерфейс должен решить, с приведением краткого обзора предшествующих API-интерфейсов распределенных вычислений. После этого рассматриваются средства, предлагаемые WCF, а также ключевые сборки, пространства имен и типы, которые представляет эта программная модель. На протяжении главы будет построено несколько служб, хостов и клиентов WCF с использованием различных инструментов разработки WCF. Также будет отработан один простой пример для .NET 4.0, который продемонстрирует, насколько упростилась задача построения конфигурационных файлов хостинга (множество примеров конфигурационных файлов представлено далее в главе). API-интерфейсы распределенных вычислений Операционная система Windows предоставляет множество API-интерфейсов для построения распределенных систем. Хотя и верно, что под "распределенными системами" большинство понимает системы, состоящие минимум из двух сетевых компьютеров, этот термин в широком смысле может охватывать два исполняемых модуля, которые нуждаются в обмене данными, даже если они с успехом работают на одной и той же физической машине. Используя приведенное определение, выбор распределенных API- интерфейсов для решения конкретной программистской задачи обычно подразумевает ответ на следующий основополагающий вопрос: Будет ли эта система использоваться исключительно "дома", или лее внешним пользователям понадобится доступ к функциональности приложения?
Глава 25. Введение в Windows Communication Foundation 907 Если вы строите распределенную систему для "домашнего" пользования, то есть шанс гарантировать, что каждый подключенный компьютер будет работать под управлением одной и той же операционной системы и использовать одну и ту же программную платформу (.NET, COM или Java). Выполнение "домашних" систем также означает возможность применения для аутентификации, авторизации и тому подобного существующей системы безопасности. В такой ситуации можно выбрать конкретный распределенный API-интерфейс, который, исходя из соображений производительности, наилучшим образом подходит для существующей операционной системы и программной платформы. В отличие от этого, при построении системы, которая должна быть доступна другим за пределами ваших стен, вы сталкиваетесь с целым букетом проблем, подлежащих решению. Прежде всего, вряд ли удастся диктовать внешним пользователям, какую операционную систему они должны использовать, какую программную платформу применять для построения программного обеспечения и как настраивать параметры безопасности. Если вы работаете в большой компании или в университете, где используется множество операционных систем и технологий программирования, то в этом случае даже "домашние" приложения неожиданно сталкиваются с теми же сложностями, что и приложения, ориентированные на внешний мир. В любом из этих случаев следует ограничиться наиболее гибким API-интерфейсом распределенных вычислений, чтобы обеспечить максимальную доступность результирующего приложения. В зависимости от ответа на этот ключевой вопрос распределенных вычислений, следующей задачей будет выбор конкретного API-интерфейса (либо их набора). Ниже представлен краткий перечень некоторых основных распределенных API-интерфейсов, исторически используемых разработчиками программного обеспечения Windows. После этого краткого экскурса вы легко сможете оценить удобство Windows Communication Foundation. На заметку! Чтобы исключить недоразумения, следует указать, что WCF (и включаемые технологии) не имеет ничего общего с построением веб-сайтов на основе HTML. Хотя и верно считать веб-приложения "распределенными", в том смысле, что в обмене обычно участвуют две машины, API-интерфейс WCF нацелен на установку соединений с машинами для распределения функциональности удаленных компонентов, а не для отображения HTML-разметки в веб-браузере. Построение веб-сайтов на платформе .NET будет рассматриваться в главе 32. Роль DCOM До появления платформы .NET основным API-интерфейсом удаленных вычислений на платформе Microsoft была распределенная модель компонентных объектов (Distributed Component Object Model — DCOM). С помощью DCOM можно было строить распределенные системы, используя при этом объекты СОМ, системный реестр и определенную долю старания. Одно из преимуществ DCOM состояло в том, что она обеспечивала прозрачность расположения компонентов. Просто говоря, это позволяло разрабатывать клиентское программное обеспечение так, что физическое расположение удаленных объектов не должно было указываться жестко. Независимо от того, располагался удаленный объект на той же или на другой сетевой машине, код мог оставаться нейтральным, поскольку действительное местоположение записывалось в системный реестр. Хотя модель DCOM имела определенный успех, для всех практических нужд это был API-интерфейс, ориентированный на Windows. Сама модель DCOM не представляла собой основы для построения полноценных решений, включающих различные операционные системы (Windows, Unix, Mac) или разделяющих данные между разными архитектурами (т.е. COM, Java или CORBA).
908 Часть V. Введение в библиотеки базовых классов .NET На заметку! Предпринимались некоторые попытки переноса DCOM для запуска на различных версиях Unix/Linux, но результат не был блестящим и в конечном итоге стал технологическим тупиком. В общем и целом, модель DCOM была наилучшим образом приспособлена для разработки "домашних" приложений, поскольку передача типов СОМ за пределы компании влекла за собой дополнительные сложности (брандмауэры и т.п.). С появлением платформы .NET модель DCOM быстро стала устаревшей, и если вам не приходится сопровождать унаследованные системы DCOM, то можете смело поставить крест на этой технологии. Роль служб COM+/Enterprise Services Модель DCOM представляла собой лишь немногим более чем способ установки канала взаимодействия между двумя частями программного обеспечения на основе СОМ. Чтобы заполнить брешь недостающих компонентов для построения полноценных распределенных вычислительных решений, в Microsoft в конечном итоге выпустили сервер транзакций (Microsoft Transaction Server — MTS), который позже был переименован в СОМ+. Несмотря на название, СОМ+ использовали не только программисты СОМ; эта технология полностью доступна и профессиональным разработчикам приложений для .NET. Co времен выхода первого выпуска платформы .NET библиотеки базовых классов предоставляют пространство имен System.EnterpriseServices. С его помощью программисты .NET могут строить управляемые библиотеки, которые устанавливаются в исполняющую среду СОМ+, получая доступ к тому же набору служб, что и традиционный СОМ-сервер, поддерживающий СОМ+. В любом случае, как только поддерживающая СОМ+ библиотека устанавливается в исполняющей среде СОМ+, она становится служебным компонентом. СОМ+ предлагает множество средств, которые может использовать служебный компонент, включая управление транзакциями, управление временем жизни объектов, службу поддержки пулов, систему безопасности на основе ролей, модель слабо связанных событий и т.д. Это было главным преимуществом на то время, учитывая, что большинство распределенных систем требовали одинакового набора служб. Вместо того чтобы заставлять разработчиков кодировать их вручную, технология СОМ+ предложила готовое решение. Одним из наиболее неотразимых аспектов СОМ+ был тот факт, что все эти настройки можно было конфигурировать в декларативной манере, используя административные инструменты. Таким образом, если вы хотели обеспечить мониторинг объекта в транзакционном контексте или отнести его к определенной роли безопасности, то просто должны были правильно отметить нужные флажки. Хотя службы COM+/Enterprise Services все еще используются и сегодня, данная технология предлагает решение только для Windows Это решение больше подходит для разработки "домашних" приложений либо в качестве службы заднего плана, опосредованно управляемой более совершенными интерфейсными средствами (например, публичными веб-сайтами, вызывающими служебные компоненты — объекты СОМ+ — в фоновом режиме). На заметку! В WCF в настоящее время не предусмотрено способа построения служебных компонентов. Однако службы WCF имеют возможность взаимодействовать с существующими объектами СОМ+. Если необходимо строить служебные компоненты на С#, придется непосредственно использовать пространство имен System.EnterpriseServices. Подробные сведения ищите в документации .NET Framework 4.0 SDK.
Глава 25. Введение в Windows Communication Foundation 909 Роль MSMQ API-интерфейс MSMQ (Microsoft Message Queuing — очередь сообщений Microsoft) позволяет разработчикам строить распределенные системы, которые нуждаются в надежной доставке сообщений по сети. Хорошо известно, что в любой распределенной системе существует риск отказа сетевого сервера, отключения базы данных или потери соединений по каким-то необъяснимым причинам. Более того, множество приложений должны быть сконструированы так, чтобы данные сообщений, которые невозможно доставить немедленно, сохранялись для последующей доставки (это называется очере- дизацией данных). Изначально в Microsoft представили MSMQ как упакованный набор низкоуровневых API-интерфейсов и СОМ-объектов на основе С. После выхода платформы .NET программисты на С# могут использовать пространство имен System.Messaging для привязки к MSMQ и построения программного обеспечения, взаимодействующего с периодическими подключающимися приложениями в зависимом режиме. Кстати, уровень СОМ+, включающий функциональность MSMQ в исполняющую среду (в упрощенном формате), использует технологию, именуемую Queued Components (QC). Этот способ взаимодействия с MSMQ был оформлен в пространство имен System.EnterpriseServices, которое упоминалось в предыдущем разделе. Независимо от того, какая программная модель используется для взаимодействия с исполняющей средой MSMQ, в конечном итоге гарантируется надежная и своевременная доставка сообщений. Подобно СОМ+, интерфейс MSMQ определенно можно считать частью фабрики по построению распределенного программного обеспечения на платформе операционной системы Windows. Роль .NET Remoting Как ранее упоминалось, с появлением платформы .NET технология DCOM быстро стала устаревшим API-интерфейсом распределенных систем. Библиотеки базовых классов, обслуживающие уровень .NET Remoting, представлены пространством имен System.Runtime.Remoting. Этот API-интерфейс позволяет множеству компьютеров распределять объекты, если только они работают в составе приложений на платформе .NET. API-интерфейсы .NET Remoting предоставили множество очень полезных средств. Наиболее важным было применение конфигурационных файлов на основе XML для декларативного определения внутренних механизмов, используемых клиентским и серверным программным обеспечением. Используя файлы *. con fig, очень просто радикально изменить функциональность распределенной системы, изменив содержимое конфигурационных файлов и перезапустив приложение. К тому же, учитывая факт, что этот API-интерфейс используется только приложениями .NET, можно получить значительный выигрыш в производительности, если данные будут закодированы в компактном двоичном формате, и при определении параметров и возвращаемых значений можно будет использовать систему общих типов (Common Type System — CTS). Хотя .NET Remoting можно было применять для построения распределенных систем, охватывающих несколько операционных систем (через платформу Mono, кратко упомянутую в главе 1 и подробно описанную в приложении Б), взаимодействие с другими программными архитектурами (такими как Java) еще не была возможным.
910 Часть V. Введение в библиотеки базовых классов .NET Роль веб-служб XML Каждый из предшествующих API-интерфейсов распределенных вычислений обеспечивал минимальную (или вовсе никакую) поддержку доступа удаленных пользователей к функциональности в независимой манере. Когда нужно было предоставить услуги удаленных объектов любой операционной системе и любой программной модели, веб- службы XML предлагали наиболее простой способ сделать это. В отличие от традиционных браузерных веб-приложений, веб-служба обеспечивает способ представления функциональности удаленных компонентов через стандартные веб-протоколы. С момента появления начального выпуска .NET программистам была предложена превосходная поддержка построения и использования веб-служб XML через пространство имен System.Web.Services. Фактически во многих случаях построение полноценной веб-службы сводится просто к применению атрибута [WebMethod] к каждому общедоступному методу, к которому планируется открыть доступ. Более того, Visual Studio 2010 позволяет подключаться к удаленным веб-службам парой щелчков на кнопках. Веб-службы позволяют разработчикам строить сборки .NET, включающие типы, которые могут быть доступны по простому протоколу HTTP. Кроме того, веб-служба кодирует свои данные в простой XML. Учитывая тот факт, что веб-службы базируются на открытых промышленных стандартах (таких как HTTP, XML и SOAP), а не на патентованной системе типов и сетевых форматов (как в случае DC ОМ или .NET Remoting), они допускают высокую степень взаимодействия и возможности обмена данными. На рис. 25.1 иллюстрируется независимая природа веб-служб XML. Приложение .NET на платформе Win32 Прокси веб- служб ы. Приложение Java на платформе Unix ' прокси ' веб- службы^ Приложение Java (или .NET) на платформе Macintosh /Прокси4 ( веб- V службы. Веб-браузер (на любой платформе) HTTPhXML HTTPhXML НИР и XML Веб-сервер А Веб-служба XML НИР и XML НПРиХМ1_ Рис. 25.1. Веб-службы XML обеспечивают очень высокую степень взаимодействия
Глава 25. Введение в Windows Communication Foundation 911 Разумеется, ни один распределенный API-интерфейс не является безупречным. Один из потенциальных недостатков веб-служб состоит в том, что они могут страдать от некоторых проблем с производительностью (учитывая использование HTTP и XML для представления данных). Другой недостаток связан с тем, что они могут оказаться не идеальным решением для "домашних" приложений, где беспрепятственно можно применять протоколы на основе TCP и двоичное форматирование данных. Пример веб-службы .NET В течение многих лет программисты .NET создавали веб-службы, используя шаблон проекта ASP.NET Web Service в среде Visual Studio, к которому можно добраться через меню File^New^Web Site (Файл ^ Создать1^Веб-сайт). Этот конкретный шаблон проекта создает общепринятую структуру каталогов и несколько начальных файлов для представления самой веб-службы. Хотя упомянутый шаблон проектов очень полезен в качестве основы, веб-службы XML можно также строить на основе .NET, применяя простой текстовый редактор, и немедленно тестировать их, используя веб-сервер разработчика (подробности ищите в главе 31). Например, предположим, что в новый файл по имени HelloWebService. asmx (*.asmx — расширение по умолчанию для файла веб-служб .NET XML) помещена следующая программная логика. Сохраните этот файл в каталоге C:\HelloWebService. <и@ WebService Language="C#11 Class="HelloWebService11 %> using System; using System.Web.Services; public class HelloWebService { [WebMethod] public string HelloWorld() { return "Hello World"; } } Хотя эту веб-службу вряд ли можно считать сколько-нибудь полезной, обратите внимание, что файл открывается директивой WebService, которая предназначена для указания языка программирования .NET, применяемого в данном файле, и имени типа класса, представляющего службу. Помимо этого, единственный интересный момент — это оснащение метода HelloWorld() атрибутом [WebMethod]. Во многих случаях это и все, что требуется сделать, чтобы предоставить метод внешним потребителям через HTTP. И, наконец, заметьте, что для кодирования возвращаемого значения в формате XML не нужно предпринимать никаких специальных действий, поскольку это делается автоматически во время выполнения. Если хотите протестировать веб-службу, просто нажмите клавишу <F5> для запуска отладочного сеанса или <Ctrl+F5> для запуска проекта на выполнение. При этом должен открыться браузер и отобразить каждый веб-метод, представленный этой конечной точкой. Здесь же можно щелкнуть на ссылке HelloWorld для вызова метода через HTTP. После этого браузер отобразит возвращаемое значение, закодированное в XML: <?xml version="l. О" encoding="utf-8" ?> <string xmlns="http://tempuri.org/"> Hello World </string> Пара пустяков, не так ли? Еще лучше то, что когда необходимо построить реальный клиент для взаимодействия со службой, можно сгенерировать файл прокси клиентской стороны, который будет применяться для вызова веб-методов. Как известно, прокси
912 Часть V. Введение в библиотеки базовых классов .NET (proxy) — это класс, инкапсулирующий низкоуровневые детали взаимодействия с другим объектом, в данном случае — с самой службой. Генерация прокси для классических веб-служб XML осуществляется двумя способами. Во-первых, можно использовать инструмент командной строки wsdl.exe, что удобно, когда нужен полный контроль над тем, как будет генерироваться прокси. Во- вторых, можно применить опцию Visual Studio под названием Add Service Reference (Добавить ссылку на службу), доступную в меню Project (Проект). Оба подхода также обеспечивают генерацию необходимого файла *.config клиентской стороны, который содержит различные конфигурационные установки для прокси (такие как расположение веб-службы). Предполагая, что прокси для рассматриваемой простой веб-службы сгенерирован (хотя в данном случае это необязательно), клиентское приложение сможет вызывать веб-метод следующим образом: static void Main(string [ ] args) { // Тип прокси содержит код для чтения файла *.config, // который позволяет определить расположение веб-службы. HelloWebServiceProxy proxy = new HelloWebServiceProxy() ; Console.WriteLine(proxy.HelloWorId() ) ; } Хотя в .NET 4.0 осталась возможность строить такого рода "традиционный" вариант веб-службы XML, большинство новых проектов служб выиграют от применения вместо этого шаблонов WCF. Фактически, начиная с .NET 3.0, технология WCF стала предпочтительным способом построения систем, ориентированных на службы. Исходный код. Проект HelloWorldWebService доступен в подкаталоге Chapter 25. Стандарты веб-служб Главная проблема состоит в том, что реализации веб-служб, с которыми мы имели дело ранее, созданные крупными компаниями (Microsoft, IBM и Sun Microsystems), были не на 100% совместимы между собой. Вполне очевидно, что это было проблемой, учитывая, что основной целью веб-служб является достижение высокой степени взаимодействия между платформами и операционными системами! Чтобы гарантировать возможность взаимодействия веб-служб, группа под названием World Wide Web Consortium (W3C; http://www.w3.org) и организация Web Services Interoperability Organization (WS-I; http://www.ws-i.org) приступили к разработке спецификаций. Эти спецификации призваны определить, каким образом поставщики программного обеспечения (т.е. IBM, Microsoft и Sun Microsystems) должны строить библиотеки программного обеспечения для разработки веб-служб, чтобы обеспечивать совместимость. Все эти спецификации получили общее имя WS-* и охватили вопросы безопасности, вложений, описание веб-служб (через язык описания веб-служб WSDL (Web Description Language)), политики, форматы SOAP и массу прочих деталей. Как известно, реализация Microsoft, учитывающая большую часть этих стандартов (как для управляемого, так и неуправляемого кода), встроена в набор инструментов Web Services Enhancements (WSE), который может быть свободно загружен с веб-сайта поддержки: http://msdn2.microsoft.com/en-us/webservices. При построении приложения службы WCF не приходится напрямую использовать сборки, которые являются частью набора инструментов WSE. Вместо этого, если создается служба WCF, использующая привязку на основе HTTP (рассматривается далее в
Глава 25. Введение в Windows Communication Foundation 913 главе), то все эти спецификации WS-* предоставляются в готовом виде (в точности те, что основаны на выбранной привязке). Именованные каналы, сокеты и Р2Р Если выбора между DCOM, .NET Remoting, веб-службами, СОМ+ и MSMQ оказывается недостаточно, список распределенных API-интерфейсов можно продолжить. Программисты могут также использовать дополнительные API-интерфейсы межпроцессного взаимодействия, такие как именованные каналы (pipe), сокеты и одноранговые (peer-to-peer — Р2Р) службы. Эти низкоуровневые API-интерфейсы обычно обеспечивают более высокую производительность (особенно для машин, находящихся в одной локальной сети); однако применение этих API-интерфейсов намного усложняется (если вообще возможно) при разработке приложений, открытых внешнему миру. При построении распределенной системы, которая включает множество приложений, работающих на одной физической машине, можно применить API-интерфейс именованных каналов через пространство имен System. 10.Pipes. Этот подход обеспечит самый быстрый способ пересылки данных между приложениями на одной машине. Также если строится приложение, которое требует абсолютного контроля над установкой и поддержанием сетевого соединения, то сокеты и функциональность Р2Р доступны на платформе .NET с использованием пространств имен System.Net.Sockets и System.Net.PeerToPeer. Роль WCF Широкий массив распределенных технологий весьма затрудняет выбор правильного инструмента для работы. Ситуация еще более усложняется ввиду того факта, что некоторые из этих технологий имеют перекрывающуюся функциональность (например, в области транзакций и безопасности). Даже когда разработчик .NET выбрал технологию, которая кажется правильной для текущей задачи, построение, сопровождение и конфигурирование такого приложения будет, по меньшей мере, сложным. Каждый API-интерфейс имеет собственную программную модель, собственный уникальный набор инструментов конфигурирования и т.д. До появления .NET 3.0 было очень трудно подключить распределенные API- интерфейсы без написания существенного объема специальной инфраструктуры. Например, если вы строите систему с использованием API-интерфейсов .NET Remoting, а позднее решите применить веб-службы XML как более подходящее решение, придется полностью перепроектировать кодовую базу. WCF — это инструментальный набор распределенных вычислений, появившийся в .NET 3.0, который интегрирует все эти ранее независимые технологии распределенной обработки в один стройный API-интерфейс, представленный, прежде всего, пространством имен System.ServiceModel. С помощью WCF можно предоставлять эти службы вызывающему коду, применяя для этого широкое разнообразие приемов. Например, при создании "домашнего" приложения, где все подключенные машины работают под управлением Windows, можно использовать различные протоколы TCP для достижения максимально возможной производительности. Те же самые службы также могут быть представлены с применением протоколов на основе веб-служб XML, чтобы позволить внешним клиентам пользоваться их функциональностью, независимо от языка программирования или операционной системы. Учитывая тот факт, что WCF позволяет выбрать правильный протокол для выполнения работы (используя общую программную модель), вы обнаружите, что подключить и запустить низкоуровневые механизмы распределенного приложения достаточно про-
914 Часть V. Введение в библиотеки базовых классов .NET сто. В большинстве случаев это можно делать без перекомпиляции или повторного развертывания клиентского программного обеспечения и службы, поскольку низкоуровневые детали часто задаются в конфигурационных файлах приложения (подобно старым API-интерфейсам .NET Remoting). Обзор средств WCF Возможность взаимодействия и интеграция различных API-интерфейсов — это только два важных аспекта WCF. В дополнение WCF предлагает развитую фабрику программного обеспечения, которая дополняет технологии удаленной разработки, представленные WCF. Ниже приведен список главных средств WCF. • Поддержка как строго типизированных, так и не типизированных сообщений. Этот подход позволяет приложениям .NET эффективно разделять типы, в то время как программное обеспечение, созданное с использованием других платформ (вроде Java) может потреблять потоки слабо типизированного XML. • Поддержка нескольких привязок (низкоуровневый HTTP, TCP, MSMQ и именованные каналы), позволяющая выбирать наиболее подходящий механизм для транспортировки сообщений туда и обратно. • Поддержка последних спецификаций веб-служб (WS-*). • Полностью интегрированная модель безопасности, охватывающая как встроенные протоколы безопасности Windows/.NET, так и многочисленные нейтральные технологии защиты, построенные на стандартах веб-служб. • Поддержка технологий хранения состояния сеансов, а также поддержка однонаправленных сообщений без состояния. Каким бы впечатляющим ни был этот список, на самом деле он лишь поверхностно касается функциональности, предлагаемой WCF Технология WCF также предоставляет средства трассировки и протоколирования, счетчики производительности, модель публикации и подписки на события, поддержку транзакций и многое другое. Обзор архитектуры, ориентированной на службы Еще одно преимущество WCF состоит в том, что он базируется на принципах дизайна, установленном архитектурой, ориентированной на службы (service-oriented architecture — SOA). Чтобы быть точным, SOA стало модным словечком в индустрии, и подобно многим таким словечкам, может быть определено различными способами. Попросту говоря, SOA — это способ проектирования распределенных систем, где несколько автономных служб работают совместно, передавая сообщения через границы (либо сетевых машин, либо двух процессов на одной и той же машине) с использованием четко определенных интерфейсов. В мире WCF эти четко определенные интерфейсы обычно создаются с применением действительных интерфейсных типов CLR (см. главу 9). Однако в более общем смысле интерфейс службы просто описывает набор членов, которые могут быть вызваны внешними клиентами. Команда разработчиков WCF пользовалась четырьмя принципами проектирования SOA. Хотя данные принципы реализуются автоматически, просто при построении приложения WCF. понимание этих четырех кардинальных правил дизайна SOA может помочь применять WCF в дальнейшей перспективе. В последующих разделах приведен краткий обзор каждого принципа.
Глава 25. Введение в Windows Communication Foundation 915 Принцип 1: границы установлены явно Этот принцип подчеркивает тот факт, что функциональность службы WCF выражается через четко определенные интерфейсы (т.е. описания каждого члена, его параметров и возвращаемых значений). Единственный способ, которым внешний клиент может связаться со службой WCF, — через интерфейс, при этом оставаясь в блаженном неведении о деталях ее внутренней реализации. Принцип 2: службы автономны Говоря о службах, как об автономных сущностях, имеется в виду тот факт, что каждая служба WCF является (насколько возможно) отдельным "островом". Автономная служба должна быть независимой от проблем с версиями, развертыванием и установкой. Чтобы помочь в продвижении этого принципа, мы опять возвращаемся к ключевому аспекту программирования на основе интерфейсов. Как только интерфейс внедрен, он никогда не должен изменяться (или вы рискуете разрушить существующие клиенты). Когда требуется расширить функциональность службы WCF, просто напишите новый интерфейс, который моделирует необходимую функциональность. Принцип 3: службы взаимодействуют через контракт, а не реализацию Третий принцип — еще один побочный продукт программирования на основе интерфейсов — состоит в том, что реализация деталей службы WCF (на каком языке она написана, как именно выполняет свою работу, и т.п.) не касается вызывающего ее внешнего клиента. Клиенты WCF взаимодействуют со службами исключительно через их открытые интерфейсы. Более того, если члены службы представляют сложные специальные типы, они должны быть полностью детализированы в виде контракта данных, гарантируя, что клиенты смогут отобразить содержимое на определенную структуру данных. Принцип 4: совместимость служб базируется на политике Поскольку интерфейсы CLR предоставляют строго типизированные контракты всем клиентам WCF (и также могут быть использованы для генерации соответствующего документа WSDL на основе выбранной привязки), важно понимать на то, что интерфейсы и WSDL сами по себе недостаточно выразительны, чтобы детализировать аспекты того, что способна делать служба. Учитывая это, SOA позволяет определять политики, которые еще более проясняют семантику службы (например, ожидаемые требования безопасности, применяемые для общения со службой). Используя эти политики, можно отделять низкоуровневые синтаксические описания службы (предоставляемые интерфейсы) от семантических деталей их работы и способов их вызова. WCF: итоги Этот небольшой экскурс в историю проиллюстрировал, что WCF является предпочтительным подходом для построения распределенных приложений в .NET 3.0 и последующих версиях Пытаетесь ли вы построить "домашнее" приложение, применяя протоколы TCP, перемещаете данные между программами на одной и той же машине с использованием именованных каналов, или же в основном предоставляете данные внешнему миру через веб-службы — для всего этого имеет смысл применять API-интерфейс WCF Это не означает невозможность применения в новых разработках первоначальных пространств имен, связанных с распределенными вычислениями (т.е. System.Runtime. Remotmg, System.Messaging, Syster-.Enterprisedervices и System.Web.Services). Фактически в некоторых случаях (в частности, когда нужно строить объекты СОМ+), это просто придется делать. Но, так или иначе, если вы использовали эти API-интерфейсы в прошлых проектах, изучение WCF не представит особой сложности. Подобно техно-
916 Часть V. Введение в библиотеки базовых классов .NET логиям, которые предшествовали ему, WCF интенсивно использует конфигурационные файлы на основе XML, атрибуты .NET и утилиты генерации прокси. Вооружившись всеми этими знаниями, теперь можно сконцентрировать внимание на построении приложения WCF. Опять-таки, имейте в виду, что полное описание WCF потребовало бы целой книги, поскольку описание каждой из поддерживающих служб (те. MSMQ, COM+, Р2Р и именованные каналы) заняло бы отдельную главу. Здесь будет показан общий процесс построения программ WCF с использованием протоколов TCP и HTTP (веб-службы). Это должно подготовить почву для дальнейшего углубления знаний в данной области. Исследование основных сборок WCF Как и можно было ожидать, фабрика программного обеспечения WCF представлена набором сборок .NET, установленных в GAC. В табл. 25.1 описаны основные сборки WCF, которые понадобится применять почти в любом приложении WCF Таблица 25.1. Основные сборки WCF Сборка Назначение System.Runtime.Serialization.dll Определяет пространства имен и типы, используемые для сериализации и десериализации объектов в WCF System.ServiceModel.dll Основная сборка, содержащая типы! используемые для построения приложений WCF любого рода В этих двух сборках определен ряд пространств имен и типов. Детальные сведения о них доступны в документации .NET Framework 4.0 SDK, а в табл. 25.2 описаны важнейшие пространства имен, о которых следует знать. Таблица 25.2. Основные пространства имен WCF Сборка Назначение System.Runtime.Serialization System.ServiceModel System.ServiceModel.Configuration System.ServiceModel.Description System.ServiceModel.Msmqlntegration System.ServiceModel.Security Определяет многочисленные типы, используемые для управления сериализацией и десериализаци- ей данных в WCF Первичное пространство имен WCF, определяющее типы привязки и хостинга, а также базовые типы безопасности и транзакций Определяет типы, обеспечивающие объектную модель адресов, привязок и контрактов, определенных внутри конфигурационных файлов WCF Определяет типы для интеграции со службами MSMQ Определяет многочисленные типы для управления аспектами уровней безопасности WCF Определяет многочисленные типы, предоставляющие программный доступ к конфигурационным файлам WCF
Глава 25. Введение в Windows Communication Foundation 917 Несколько слов о CardSpace В дополнение к System.ServiceModel.dll и System.Runtime.Serialization.dll, в WCF имеется и третья сборка, называемая System.IdentityModel.dll. В ней определены многочисленные дополнительные пространства имен и типы, которые поддерживают API-интерфейс CardSpace. Эта технология позволяет устанавливать и управлять цифровыми идентификаторами внутри приложения WCF. По сути, API-интерфейс CardSpace предлагает унифицированную программную модель для доступа к различным деталям приложений WCF, связанным с безопасностью, таким как идентичность вызывающего кода, службы авторизации/аутентификации и т.д. Полное описание API-интерфейса CardSpace API можно найти в документации .NET Framework 4.0 SDK. Шаблоны проектов WCF в Visual Studio Как будет более подробно объясняться позже в этой главе, приложение WCF обычно представлено тремя сборками, одной из которых является *.dll, содержащая типы, с которыми может взаимодействовать внешний клиент (другими словами, сама служба WCF). Когда вы хотите построить службу WCF, совершенно разумно выбрать стандартный шаблон проекта Class Library (см. главу 14) в качестве начальной точки и вручную добавить ссылки на сборки WCF. В качестве альтернативы новую службу WCF можно создать, выбрав в Visual Studio 2010 шаблон проекта WCF Service Library (Библиотека служб WCF), как показано на рис. 25.2. Этот тип проекта автоматически устанавливает ссылки на необходимые сборки WCF, однако он также генерирует и приличный объем начального кода, который, скорее всего, будет удален. New Project ^^з^^^я Imtaled Templates л Visual С* Windows Web Office Cloud Service Reporting Sirverttght Test 1Езшашшшш .NET Fra Щ Щ Щ mework 4 » ! Sort by: Default WCF Service Library Visual C« WCF Workflow Service Applic... Visual C* Syndication Service Library visual C* C S »| : 1 (Tj | Type: Visual C» A project for creating a WCF service class library (.dll) Per user extensions are currently not allowed to load. Name: WcfServiceLibraryl Location: С ■ М/С ode Solution name: -.-.-,- t Create directory for solution Add to source control Рис. 25.2. Шаблон проекта WCF Service Library в Visual Studio 2010 Одно из преимуществ выбора этого шаблона проекта состоит в том, что он также снабжает файлом App.config, что может показаться странным, так как строится .NET *.dll, а не .NET *.exe. Однако этот файл очень полезен тем, что при отладке или запуске проекта WCF Service Library интегрированная среда разработки Visual Studio 2010 автоматически запустит приложение WCF Test Client (Тестовый клиент WCF). Программа WcfTestClient.exe читает настройки из файла App.config, поэтому может использо-
918 Часть V. Введение в библиотеки базовых классов .NET ваться для тестирования службы. Далее в этой главе WCF Test Client рассматривается более подробно. На заметку! Файл App.conf ig из проекта WCF Service Library также полезен тем, что демонстрирует начальные установки для конфигурации хост-приложения WCF. Фактически большую часть этого кода можно копировать и вставлять в конфигурационный файл реального хост- приложения. В дополнение к базовому шаблону WCF Service Library в категории проектов WCF диалогового окна New Project (Новый проект) определены еще два библиотечных проекта WCF, которые интегрируют функциональность Windows Workflow Foundation (WF) в службу WCF, а также шаблон для построения библиотеки RSS (см. рис. 25.2). Технология Windows Workflow Foundation рассматривается в следующей главе, поэтому данные шаблоны проектов WCF можно пока проигнорировать. Шаблон проекта WCF Service Доступен еще один шаблон проектов Visual Studio 2010, связанный с WCF, который находится в диалоговом окне New Web Site (Новый веб-сайт), открываемом через пункт меню File^New^Web Site (рис. 25.3). New Web Site Recent Templates Installed Templates Visual Basic Visual C* 1 Online Templates Per user extensions art Web location: сигтепйу not ark file System ^TlTamework 4 ' ) Swt by: "Default ^ Empty Web Site Visual C# *W Sirverlight 1.0 Web Site Visual C# ,cO WCF Service Visual C# jff\ ASP.NET Reports Web Site Visual C* Яу Dynamic Data Linq to SQL W... Visual C* I Ш^ Dynamic Data Entities Web Si...Visual C* ijjfe ASP.NET Crystal Reports We... Visual C* т*4*ьшл.шъшлт Ышш mi —■■■■! •j СЛМуСоое ZZIi С I- Type: Visual C* A Web site for cresting WCF services Browse... OK Cancel \ Рис. 25.3. Веб-ориентированный шаблон проектов WCF Service в Visual Studio 2010 Шаблон проекта WCF Service (Служба WCF) полезен, когда заранее известно, что служба WCF будет использовать протоколы веб-служб, а не, например, именованные каналы. Эта опция позволит автоматически создать новый виртуальный каталог IIS для хранения программных файлов WCF, создаст корректный файл Web.config для представления службы через HTTP и сгенерирует необходимый файл *.svc (подробнее о файлах *.svc рассказывается далее в этой главе). Таким образом, веб-ориентированный проект WCF Service просто экономит время, поскольку IDE-среда автоматически настроит всю необходимую инфраструктуру IIS. Напротив, если новая служба WCF создается с применением опции WCF Service Library, появляется возможность разместить службу несколькими различными способами (например, специальный хост, служба Windows или вручную созданный виртуальный каталог IIS). Эта опция больше подходит, когда планируется построить специальный хост для службы WCF, которая может работать с любым количеством привязок WCF
Глава 25. Введение в Windows Communication Foundation 919 Базовая композиция приложения WCF При построении распределенной системы WCF обычно создаются три взаимосвязанных сборки. • Сборка службы WCF. Эта библиотека *.dll содержит классы и интерфейсы, представляющие обитую функциональность, которая предлагается внешним клиентам. • Хост службы WCF. Этот программный модуль — сущность, которая принимает в себе сборку службы WCF. • Клиент WCF. Это приложение, которое обращается к функциональности службы через промежуточный прокси. Как уже упоминалось, сборка службы WCF — это библиотека классов .NET, содержащая в себе множество контрактов WCF и их реализаций. Ключевое отличие состоит в том, что контрактные интерфейсы оснащены разнообразными атрибутами, которые управляют представлением типа данных, тем, как исполняющая среда WCF взаимодействует с представленными типами, и т.д. Вторая сборка — хост WCF — может быть любой исполняемой программой .NET. Как будет показано в этой главе, WCF настраивается таким образом, что службы могут быть легко представлены приложениями любого типа (т.е. Windows Forms, служба Windows, приложения WPF). При построении специального хоста применяется тип ServiceHost и связанный с ним файл *. con fig, который содержит детали, касающиеся механизмов серверной стороны, которые вы хотите использовать. Однако если в качестве хоста для службы WCF применяется IIS, то нет необходимости в программном построении специального хоста, поскольку IIS использует "за кулисами" тип ServiceHost. На заметку! Развернуть службу WCF также можно с применением службы Windows Activation Service (WAS). За подробными сведениями обращайтесь в документацию .NET Framework 4.0 SDK. Последняя сборка представляет клиент, который осуществляет вызовы службы WCF. Как и можно было ожидать, этим клиентом может быть приложение .NET любого типа. Подобно хосту, клиентское приложение также обычно использует файл *. con fig клиентской стороны, определяющий все клиентские механизмы. Следует также помнить, что если служба WCF строится с использованием привязок, основанных на HTTP, то клиентское приложение может быть реализовано на другой платформе (например, Java). На рис. 25.4 показаны высокоуровневые отношения между этими тремя взаимосвязанными сборками WCF. "За кулисами" скрыто несколько низкоуровневых деталей, реализующих все необходимые внутренние механизмы (фабрики, каналы, слушатели и т.п.). Эти низкоуровневые детали чаще всего скрыты от глаз; однако при необходимости они могут быть расширены или настроены. В большинстве случаев вполне подходят настройки по умолчанию. Также следует упомянуть, что применение файла *. con fig серверной или клиентской стороны не является обязательным. При желании можно жестко закодировать хост (а также клиент), указав необходимые детали (т.е. конечные точки, привязку, адреса). Очевидная проблема такого подхода состоит в том, что если нужно изменить детали настройки, понадобится вносить изменения в код, перекомпилировать и заново развертывать множество сборок. Использование файла *. con fig делает кодовую базу намного более гибкой, поскольку все изменения настроек производятся редактированием конфигурационных файлов и последующим перезапуском. С другой стороны, программные конфигурации обеспечивают приложению более динамичную гибкость — оно может выбирать конфигурацию внутренних механизмов, например, в зависимости от результатов проверки условий.
920 Часть V. Введение в библиотеки базовых классов .NET ( 1 Клиентское приложение ( "^ Прокси 1 ^ 4 Ч * | Т Конфигурационный файл XoctWCF с— "^ 1 \\ 1 Конфигурационный файл у Рис. 25.4. Высокоуровневое представление типичного приложения WCF Понятие ABC в WCF Хосты и клиенты взаимодействуют друг с другом, согласовывая так называемые ABC — условное наименование для запоминания основных строительных блоков приложения WCF, таких как адрес, привязка и контракт (address, binding, contract — ABC). • Адрес. Описывает местоположение службы. В коде представлен типом System.Uri, однако значение обычно хранится в файлах *. с on fig. • Привязка. WCF поставляется с множеством различных привязок, которые указывают сетевые протоколы, механизмы кодирования и транспортный уровень. • Контракт. Предоставляет описание каждого метода, представленного службой WCF. Имейте в виду, что аббревиатура ABC не подразумевает, что разработчик обязан определять сначала адрес, за ним привязку и только потом — контракт. Во многих случаях разработчик WCF начинает с определения контракта службы, за которым следует адрес и привязки (допускается любой порядок, если только указаны все аспекты). Прежде чем строить первое приложение WCF, давайте более детально рассмотрим ABC. Понятие контрактов WCF Понятие контракта является ключевым при построении службы WCF Хотя это и не обязательно, подавляющее большинство приложений WCF будут начинаться с определения типов интерфейсов .NET, используемых для представления набора членов, которые поддерживаются данной службой WCF В частности, интерфейсы, которые представляют контракт WCF, называются контрактами служб. Классы (или структуры), которые реализуют их, носят название типов служб. Контракты служб WCF оснащаются различными атрибутами, наиболее часто используемые из которых определены в пространстве имен System.ServiceModel. Когда члены контракта службы (методы в интерфейсе) содержат только простые типы данных (такие как числовые, булевские и строковые), полную службу WCF можно построить, используя одни только атрибуты [ServiceContract] и [OperationContract]. Однако если члены представляют специальные типы, придется обратиться к пространству имен System.Runtime.Serialization (рис. 25.5) из сборки System.Runtime. Serialization.dll. Здесь доступны дополнительные атрибуты (вроде [DataMember] и [DataContract]) для тонкой настройки процесса определения того, как составные типы будут сериализоваться и десериализоваться из XML при передаче в и из операций службы. Строго говоря, использовать интерфейсы CLR для определения контракта WCF не обязательно. Многие из этих атрибутов могут применяться к общедоступным членам
Глава 25. Введение в Windows Communication Foundation 921 общедоступного класса (или структуры). Однако, учитывая множество преимуществ программирования на основе интерфейсов (полиморфизм, элегантная поддержка множества версий и т.п.), применение интерфейсов CLR для описания контракта WCF лучше считать рекомендуемым приемом. л ^ S stem Puntime Serialization [4.0 0.0] ' пЕшсшЕзга Vv CcllecticnDataContr3ct-.ttnbLite \% ContiactfJamejpacer-.ttnbute T$ DataCcntract^ttribute 1$ DataContractPeiol er r$ DataContractSenahzer -; Dataf lember^ttnbute L^ EnumMemberMttribute Ti E»portOptionn ,; E'tenzicnDataObject "-' IDataCGntractSurrcgate - IE.ten:ibleDataObjert ^ IgncieDataf.lember-ttnbute % ImpcrtQptions Ц In ahdDataCcntractE^ception X$ Knc .nT^pe-tttribute tt NetDataCcntractSerializer Lj mlObjectSenalizer Ц ^.mlSenalizableSer. ices l^ 'PathOuer, Generator {£ :clDataCcntractEjporter T# 'sdDataContiactlmporter Рис. 25.5. В сборке System.Runtime.Serialization определен ряд атрибутов, используемых при построении контрактов данных WCF Понятие привязок WCF Как только контракт (или набор контрактов) определен и реализован внутри библиотеки службы, следующий логический шаг состоит в построении агента хостинга для самой службы WCF. Как уже упоминалось, на выбор доступно множество возможных вариантов хостов, и все они должны указывать привязки, используемые удаленными клиентами для получения доступа к функциональности типа службы. Выбор из набора привязок — это область, которая отличает разработку WCF от .NET Remoting и/или разработки веб-служб XML. На выбор в WCF доступно множество возможных привязок, каждая из которых ориентирована на определенные потребности. Если ни одна из готовых привязок не удовлетворяет существующим требованиям, можно создать собственную привязку, расширив тип CustomBinding (это в главе делаться не будет). Привязка WCF может описывать следующие характеристики: • транспортный уровень, используемый для передачи данных (HTTP, MSMQ, именованные каналы и TCP); • каналы, используемые транспортом (однонаправленные, запрос-ответ и дуплексные); • механизм кодирования, используемый для работы с данными (XML и двоичный); • любые поддерживаемые протоколы веб-служб (если разрешены привязкой), такие как WS-Security, WS-Transactions, WS-Reliability и т.д. Давайте рассмотрим возможные варианты.
922 Часть V. Введение в библиотеки базовых классов .NET Привязки на основе HTTP Классы BasicHttpBinding, WSHttpBinding, WSDualHttpBinding и WSFederation HttpBinding предназначены для представления контрактных типов через протоколы веб-служб XML. Ясно, что если для разрабатываемой службы потребуются дополнительные возможности (множество операционных систем или множество программных архитектур), эти привязки подойдут наилучшим образом, потому что все они кодируют данные на основе представления XML и используют в сети протокол HTTP. В табл. 25.3 показано, что привязка WCF может быть представлена в коде (с использованием классов из пространства имен System.ServiceModel) или в виде атрибутов XML, определенных внутри файлов *. con fig. Таблица 25.3. Привязки WCF на основе HTTP Класс привязки Элемент привязки Назначение BasicHttpBinding <basicHttpBinding"" WSHttpBinding <wsHttpBinding> WSDualHttpBinding CwsDualHttpBinding> WSFederationHttpBinding <wsFederationHttpBinding> Используется для построения службы WCF, совместимой с профилем WS-Basic Profile (WS-I Basic Profile 1.1). Эта привязка использует HTTP в качестве транспорта и Text/XML в качестве метода кодирования сообщений по умолчанию Подобен классу BasicHttpBinding, НО предоставляет больше средств веб-служб. Эта привязка добавляет поддержку транзакций, надежной доставки сообщений и протокола WS-Addressing Подобен классу WSHttpBinding, но предназначен для применения с дуплексными контрактами (например, когда служба и клиент могут посылать сообщения туда и обратно). Эта привязка поддерживает только безопасность SOAP и требует надежного обмена сообщениями Безопасная привязка с возможностью взаимодействия, которая поддерживает протокол WS-Federation, позволяя организациям, объединенным в федерацию, эффективно проводить аутентификацию и авторизацию пользователей BasicHttpBinding является простейшим из всех протоколов на основе веб-служб. В частности, эта привязка гарантирует соответствие службы WCF спецификациям WS-I Basic Profile 1.1, определенным WS-I. Основная причина применения этой привязки со-
Глава 25. Введение в Windows Communication Foundation 923 стоит в поддержке обратной совместимости с приложениями, ранее построенными для взаимодействия с веб-службами ASP.NET (которые были частью библиотек .NET, начиная с версии 1.0). Протокол WSHttpBinding не только включает поддержку подмножества спецификаций WS-* (транзакции, безопасность и надежные сеансы), но также обеспечивает возможность обработки двоичного кодирования данных с использованием механизма МТОМ (Message Transmission Optimization Mechanism — механизм оптимизации передачи сообщений). Основное преимущество привязки WSDualHttpBinding в том, что она добавляет возможность двустороннего (дуплексного) обмена сообщениями между отправителем и получателем. При выборе WSDualHttpBinding можно применять модель событий издания/подписки. И, наконец, WSFederationHttpBinding — это протокол на основе веб-служб, который стоит применять, когда наиболее важна безопасность. Эта привязка поддерживает спецификации WS-Trust, WS-Security и WS-SecureConversation, которые представлены API-интерфейсами WCF CardSpace. Привязки на основе TCP При построении распределенного приложения, работающего на машинах, которые сконфигурированы с библиотеками .NET 4.0 (другими словами, все машины работают под управлением операционной системы Windows), можно получить выигрыш в производительности, минуя привязки веб-служб и работая непосредственно с TCP, что гарантирует кодирование данных в компактном двоичном формате вместо XML. При использовании привязок, перечисленных в табл. 25.4, клиент и хост должны быть приложениями .NET. Таблица 25.4. Привязки WCF на основе TCP Класс привязки Элемент привязки Назначение NetNamedPipeBinding <netNamedPipeBinding> Безопасная, надежная, оптимизированная привязка для коммуникаций между приложениями .NET на одной и той же машине NetPeerTcpBinding <netPeerTcpBinding> Предоставляет безопасную привязку для сетевых приложений Р2Р NetTcpBinding <netTcpBinding> Безопасная и оптимизированная привязка, подходящая для межмашинных коммуникаций между приложениями .NET Класс NetTcpBinding использует протокол TCP для перемещения двоичных данных между клиентом и службой WCF. Как упоминалось ранее, это дает выигрыш в производительности по сравнению с протоколами веб-служб, но при этом решения ограничены ОС Windows. Положительной стороной является поддержка в NetTcpBinding транзакций, надежных сеансов и безопасных коммуникаций. Подобно NetTcpBinding, класс NetNamedPipeBinding поддерживает транзакции, надежные сеансы и безопасные коммуникации, но при этом обладает способностью межмашинных вызовов. Если вы ищете самый быстрый способ передачи данных между приложениями WCF на одной машине, привязке NetNamedPipeBinding нет равных. Более подробную информацию о NetPeerTcpBinding можно найти в документации NET Framework 4.0 SDK.
924 Часть V. Введение в библиотеки базовых классов .NET Привязки на основе MSMQ И, наконец, если цель заключается в интеграции с сервером MSMQ, то непосредственный интерес представляют NetMsmqBinding и MsmqlntegrationBinding. Детали применения привязок MSMQ в этой главе не рассматриваются, но в табл. 25.5 описано основное назначение каждой из них. Таблица 25.5. Привязки WCF на основе MSMQ Класс привязки Элемент привязки Назначение MsmqlntegrationBinding <msmqIntegrationBinding> Эта привязка может применяться для того, чтобы позволить приложениям WCF отправлять и принимать сообщения от существующих приложений MSMQ, которые используют СОМ, родной C++ или типы, определенные в пространстве имен System.Messaging NetMsmqBinding <netMsmqBinding> Привязка на основе очередей, подходящая для межмашинных коммуникаций между приложениями .NET. Это предпочтительный подход среди привязок, основанных на MSMQ Понятие адресов WCF Как только контракты и привязки установлены, финальный элемент мозаики состоит в указании адреса для службы WCF. Это важно, поскольку удаленные клиенты не смогут взаимодействовать с удаленными типами, если им не удастся найти их. Подобно большинству аспектов WCF, адрес может быть жестко закодирован в сборке (с использованием типа System.Uri) или вынесен в файл *.config. В любом случае точный формат адреса WCF отличается в зависимости от выбранной привязки (на основе HTTP, именованных каналов, TCP или MSMQ). На самом высоком уровне адреса WCF могут указывать перечисленные ниже единицы информации. • Scheme. Транспортный протокол (HTTP и т.п.). • MachineName. Полностью квалифицированное доменное имя машины. • Port. Во многих случаях не обязательный параметр. Например, привязка HTTP по умолчанию использует порт 80. • Path. Путь к службе WCF Эта информация может быть представлена следующим обобщенным шаблоном (значение Port необязательно, поскольку некоторые привязки его не используют): Scheme : //<MachineName-> [ : Port] /Path При использовании привязки на базе веб-службы (basicHttpBinding, wsHttpBinding, wsDualHttpBinding или wsFederationHttpBinding) адрес разбивается следующим образом (вспомните, что если номер порта не указан, протоколы на основе HTTP по умолчанию выбирают порт 80): http://localhost:8080/MyWCFService
Глава 25. Введение в Windows Communication Foundation 925 Если применяется привязка на основе TCP (такая как NetTcpBinding или NetPeerTcpBinding), то URI принимает следующий формат: net.tcp://localhost: 8 080/MyWCFService Привязки на основе MSMQ (NetMsmqBinding и MsmqlntegrationBinding) уникальны в части их формата URI, учитывая, что MSMQ может использовать общедоступные или приватные очереди (доступные только на локальной машине), а номера портов не имеют смысла в URI, связанных с MSMQ. Взгляните на следующий URI, который описывает приватную очередь по имени MyPrivate(): net.msmq://localhost/private$/MyPrivateQ И последнее: формат адреса, используемый привязкой для именованных каналов NetNamedPipeBinding, выглядит так, как показано ниже (вспомните, что именованные каналы позволяют межпроцессные взаимодействия приложений на одной и той же физической машине): net.pipe://localhost/MyWCFService Хотя одиночная служба WCF может представлять только один адрес (основанный на единственной привязке), можно сконфигурировать коллекцию уникальных адресов (с разными привязками). Это делается внутри файла *.config за счет определения множества элементов <endpoint>. Для одной и той же службы можно указывать любое количество ABC. Такой подход полезен, когда необходимо позволить клиентам выбирать протокол, которые они хотят использовать для взаимодействия со службой. Построение службы WCF Теперь, когда вы получили представление о построении блоков приложения WCF, давайте создадим первое простое приложение, чтобы посмотреть, как ABC можно описывать в коде и в конфигурационном файле. В первом примере шаблоны проектов WCF из Visual Studio 2010 использоваться не будут, что позволит сосредоточиться на специфических шагах по созданию службы WCF. Для начала создайте новый проект С# Class Library по имени MagicEightBallServiceLib. Переименуйте начальный файл Classl.cs на MagicEightBallService.cs и добавьте ссылку на сборку System.ServiceModel.dll. В начальном файле кода укажите использование пространства имен System.ServiceModel. К этому моменту файл С# должен выглядеть так: using System; using System.Collections .Generic- using System.Linq; using System.Text; // Основное пространство имен WCF. using System.ServiceModel; namespace MagicEightBallServiceLib { public class MagicEightBallService { } } В этом классе реализован единственный контракт службы WCF, представленный строго типизированным интерфейсом CLR по имени IEightBall. Как известно, магический шар Magic 8-Ball — это игрушка, позволяющая получить шуточное предсказание будущего. В интерфейсе определен единственный метод, который позволит клиенту задать вопрос магическому шару, чтобы получить случайный ответ.
926 Часть V. Введение в библиотеки базовых классов .NET Интерфейсы службы WCF оснащены атрибутом [ServiceContract], причем каждый член интерфейса оснащен атрибутом [OperationContract] (подробнее об этих двух атрибутах будет сказано чуть позже). Ниже показано определение интерфейса IEightBall: [ServiceContract] public interface IEightBall { // Задайте вопрос, получите ответ! [OperationContract] string ObtainAnswerToQuestion(string userQuEGtion); } На заметку! Допускается определять интерфейс контракта службы, который содержит методы, не оснащенные атрибутом [OperationContract]. Однако такие члены не будут видны через исполняющую среду WCF. Как известно из главы 9, интерфейс — довольно бесполезная вещь, пока он не реализован классом или структурой с целью пополнения его функциональности. Подобно реальному магическому шару, реализация типа MaqicEightBallService будет возвращать случайно выбранный ответ из массипл стрик Конструктор по умолчанию будет отображать информационное сообщение, которое будет (в конечном итоге) отображено в окне консоли окна (для диагностических целей): public class MagicEightBallService : ILijhtb_ill { // Для отображения на хосте. public MagicEightBallService() { Console.WriteLine ("The 8-Ball awaits your question..."); } public string ObtainAnswerToQuestion (string userQuestion) { string[] answers = { "Future Uncertain", "Yes", "No", "Hazy", "Ask again later", "Definitely" }; // Вернуть случайный ответ. Random г = new Random () ; return answers[r.Next(answers.Length)]; } } Библиотека службы WCF готова. Однако перед конструированием хоста для этой службы давайте ознакомимся с некоторыми подробностями атрибутов [ServiceContract] и [OperationContract]. Атрибут [ServiceContract] Чтобы интерфейс CLR участвовал в службах, предоставленных WCF, он должен быть оснащен атрибутом [ServiceContract]. Подобно многим другим атрибутам .NET, тип ServiceContract At tribute поддерживает набор свойств для дальнейшего прояснения его назначения. Два свойства — Name uNameSpace — могут быть установлены для управления именем типа службы и именем пространства имен XML, определяющим тип службы. Если используется привязка, специфичная для веб-служб, эти значения применяются для определения элементов <portType> связанного документа WSDL. Здесь мы не заботимся о присваивании значения Name, учитывая, что имя типа службы по умолчанию основано на имени класса С#. Однако именем по умолчанию
Глава 25. Введение в Windows Communication Foundation 927 для лежащего в основе пространства имен XML будет просто http://tempuri.org (оно должно быть изменено для всех создаваемых служб WCF). При построении службы WCF, которая будет посылать и принимать специальные типы данных (чего мы пока не делаем), важно установить осмысленное значение для лежащего в основе пространства имен XML, чтобы гарантировать уникальность специального типа. Как вам должно быть известно из опыта построения веб-служб XML, пространства имен XML предоставляют способ помещения типов в уникальный контейнер, гарантируя отсутствие конфликтов с типами из других организаций. По этой причине можно обновить определение интерфейса, сделав его более подходящим — почти так же, как это делается при определении пространства имен XML в проекте .NET Web Service, когда в качестве пространства имен указывается URI издателя службы. Например: [ServiceContract (Namespace = "http://MyCompany.com11)] public interface IEightBall { } Помимо Namespace и Name, атрибут [ServiceContract] может быть сконфигурирован с помощью дополнительных свойств, которые перечислены в табл. 25.6. Имейте в виду, что в зависимости от выбранной привязки, некоторые из этих настроек будут игнорироваться. Таблица 25.6. Различные именованные свойства атрибута [ServiceContract] Свойство Назначение CallbackContract Устанавливает функциональность обратного вызова для двустороннего обмена сообщениями Conf igurationName Это имя используется для нахождения элемента службы в конфигурационном файле приложения. По умолчанию представляет собой имя класса, реализующего службу ProtectionLevel Позволяет указать степень, до которой привязка контракта требует шифрования, цифровых подписей или того и другого для конечных точек, представленных контрактом SessionMode Используется для установки разрешения сеанса, запрета сеанса или обязательности сеанса для данного контракта службы Атрибут [OperationContract] Методы, которые планируется использовать внутри WCF, должны быть оснащены атрибутом [OperationContract], который также может быть сконфигурирован с помощью различных именованных свойств. Используя свойства, перечисленные в табл. 25.7, можно указать, что данный метод предназначен для однонаправленной работы, поддерживает асинхронные вызовы, требует шифрования данных сообщений и т.д. (в зависимости от выбранной привязки, многие из этих значений могут быть проигнорированы). В этом начальном примере дополнительного конфигурирования метода ObtainAnswerToQuestionO не требуется, поэтому атрибут [OperationContract] оставлен в том виде, как он определен сейчас.
928 Часть V. Введение в библиотеки базовых классов .NET Таблица 25.7. Различные именованные свойства атрибута [OperationContract] Свойство Назначение AsyncPattern Указывает, реализована ли операция асинхронно с использованием пары методов Begin/End службы. Это позволяет службе передавать обработку другому потоку серверной стороны; это не имеет отношения к асинхронному вызову метода клиентом! Islnitiating Указывает, может ли эта операция быть начальной операцией сеанса IsOneWay Указывает, состоит ли операция только из одного входного сообщения (и никакого ассоциированного вывода) IsTerminating Указывает, должна ли исполняющая среда WCF пытаться завершить текущий сеанс после выполнения операции Служебные типы как контракты операций И, наконец, вспомните, что при построении служебных типов WCF использование интерфейсов не обязательно. Фактически атрибуты [ServiceContract] и [OperationContract] можно применять только непосредственно к самому служебному типу: // Только для целей иллюстрации; в текущем примере не используется. [ServiceContract (Namespace = "http://MyCompany.com11)] public class ServiceTypeAsContract I [OperationContract] void SomeMethod() { } [OperationContract] void AnotherMethod() { } } Хотя такой подход возможен, явное определение интерфейсного типа для представления контракта службы дает массу преимуществ. Наиболее очевидный выигрыш состоит в том, что один интерфейс может быть применен к нескольким типам служб (написанных на разных языках и в разных архитектурах), чтобы достичь высокой степени полиморфизма. Другое преимущество связано с тем, что интерфейс контракта службы может использоваться в качестве основы для новых контрактов (через наследование интерфейсов), без необходимости заботиться о реализации. В любом случае к данному моменту библиотека службы WCF готова. Скомпилируйте проект, чтобы убедиться в отсутствии опечаток в коде. Исходный код. Проект MagicEightBallServiceLib доступен в подкаталоге MagicEight BallServiceHTTP каталога Chapter 25. Хостинг службы WCF Теперь мы готовы определить хост. Хотя служба производственного уровня должна развертываться в службе Windows или виртуальном каталоге IIS, наш первый хост будет просто консольным приложением по имени MagicEightBallServiceHost. После создания нового проекта консольного приложения добавьте ссылку на сборки System.ServiceModel.dll и MagicEightBallServiceLib.dll и обновите начальный файл кода, добавив импорт пространств имен System. ServiceModel и MagicEightBallServiceLib:
Глава 25. Введение в Windows Communication Foundation 929 using System; using System.Collections .Generic- using System.Linq; using System.Text; using System. ServiceModel; using MagicEightBallServiceLib; namespace MagicEightBallServiceHost { class Program { static void Main(string[] args) { Console.WriteLine("***** Console Based WCF Host *****"); Console.ReadLine(); } } } Первый шаг, который потребуется предпринять при построении хоста для служебного типа WCF — решить, будет ли определяться необходимая логика хостинга полностью в коде либо переместить несколько низкоуровневых деталей в конфигурационный файл приложения. Как упоминалось ранее, преимущество файлов *.config состоит в том, что хост может изменять низкоуровневые механизмы без перекомпиляции и повторного развертывания исполняемых программ. Однако всегда помните, что это не обязательно, и можно жестко закодировать логику хостинга с использованием типов из сборки System.ServiceModel.dll. Этот консольный хост будет использовать конфигурационный файл приложения, поэтому добавьте новый элемент Application Configuration File (Конфигурационный файл приложения) к текущему проекту, выбрав пункт меню Projects Add New Item (ПроектеДобавить новый элемент). Установка ABC внутри файла App.config При построении хоста для служебного типа WCF всегда выполняется один и тот же набор шагов — некоторые через конфигурацию, а некоторые в коде. 1. Определение конечной точки для службы WCF в конфигурационном файле хоста. 2. Программное использование типа ServiceHost для представления доступных служебных типов из этой конечной точки. 3. Обеспечение постоянной работы хоста для обслуживания клиентских запросов. Очевидно, что этот шаг не обязателен, если для хостинга применяется служба Windows или IIS. В мире WCF термин конечная точка (endpoint) представляет адрес, привязку и контракт, объединенные вместе в один пакет. В XML конечная точка выражается элементом <endpoint> и элементами address, binding и contract. Модифицируйте файл *.con- fig, указав в нем конечную точку (доступную через порт 8080), которая представлена данным хостом: <?xml version="l.О" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="MagicEightBallServiceLib.MagicEightBallService"> <endpoint address ="http://localhost:80 80/MagicEightBallService"
930 Часть V. Введение в библиотеки базовых классов .NET binding="basicHttpBinding" contract="MagicEightBallServiceLib.IEightBall"/> </service> </services> </system.serviceModel> </configuration> Обратите внимание, что элемент <system.serviceModel> находится в корне всех настроек WCF хоста. Каждая служба, развернутая на хосте, представлена элементом <service>, который помещен в базовый элемент <services>. Здесь единственный элемент <service> использует (необязательный) атрибут name для указания дружественного имени служебного типа. С помощью вложенного элемента <endpoint> задается адрес, модель привязки (basicHttpBinding в данном примере) и полностью квалифицированное имя типа интерфейса, определяющего контракт службы (IEightBall). Поскольку применяется привязка на основе HTTP, указывается схема http:// с произвольным идентификатором порта. Кодирование с использованием типа ServiceHost При текущем конфигурационном файле действительная логика программирования, необходимая для завершения хоста, чрезвычайно проста. Когда исполняемая программа стартует, создается экземпляр типа ServiceHost, которому сообщается служба WCF, отвечающая за хостинг. Во время выполнения этот объект автоматически читает данные из контекста элемента <system.serviceModel> файла *.config хоста для определения правильного адреса, привязки и контракта, и создает все необходимые механизмы: static void Main(string [ ] args) { Console.WriteLine ("***** Console Based WCF Host *****"); using (ServiceHost ServiceHost = new ServiceHost(typeof(MagicEightBallService))) { // Открыть хост и начать прослушивание входных сообщений. ServiceHost.Open ()/ // Оставить службу в действии до тех пор, пока не будет нажата клавиша <Enter>. Console.WriteLine("The service is ready."); Console.WriteLine("Press the Enter key to terminate service."); Console.ReadLine(); } } После запуска этого приложения вы обнаружите, что хост находится в памяти и готов к приему входящих запросов от удаленных клиентов. Указание базового адреса В настоящее время ServiceHost создается с использованием конструктора, который требует только информацию о служебном типе. Однако в качестве аргумента конструктора также можно передать массив элементов типа System.Uri, чтобы представить коллекцию адресов, для которых доступна данная служба. В настоящий момент адрес обнаруживается через файл *. с on fig; однако если обновить контекст using следующим образом: using (ServiceHost ServiceHost = new ServiceHost(typeof(MagicEightBallService), new Uri[]{new Uri("http://localhost:8080/MagicEightBallService")})) { }
Глава 25. Введение в Windows Communication Foundation 931 то конечную точку можно определить так: <endpoint address ="" binding=,,basicHttpBinding" contract="MagicEightBallServiceLib.IEightBall"/> Разумеется, слишком большой объем жесткого кодирования внутри кодовой базы хоста снижает гибкость, поэтому в текущем примере предполагается, что хост службы создается просто передачей информации о типе, как делалось раньше: using (ServiceHost serviceHost = new ServiceHost(typeof(MagicEightBallService))) { } Один из несколько обескураживающих аспектов написания файлов *. con fig связан с тем, что существует несколько способов конструирования дескрипторов XML, в зависимости от объема жесткого кодирования (как это было в случае с необязательным массивом Uri). Чтобы продемонстрировать другой способ написания файла *. con fig, рассмотрим следующее изменение: <?xml version="l.0" encoding=Mutf-8" ?> <configuration> <system.serviceModel> <services> <service name="MagicEightBallServiceLib.MagicEightBallService"> <!-- Адрес получен из <baseAddresses> --> <endpoint address ="" binding="basicHttpBinding" contract="MagicEightBallServiceLib.IEightBall"/> <!— Перечислить все базовые адреса в выделенном разделе --> <host> <baseAddresses> <add baseAddress ="http://localhost:8080/MagicEightBallService"/> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration> В данном случае атрибут address элемента <endpoint> все еще пуст и, невзирая на то, что массив Uri при создании ServiceHost в коде не указывался, приложение работает, как и раньше, поскольку значение извлекается из контекста base Ad dresses. Преимущество хранения базового адреса в подразделе <baseAddresses> раздела <host> состоит в том, что другие части файла *.config также должны знать адрес конечной точки службы. Поэтому вместо копирования и вставки значений адресов в единственный файл *.config можно изолировать единственное значение, как было показано выше. На заметку! В последующем примере будет представлен графический инструмент конфигурирования, позволяющий создавать конфигурационные файлы менее утомительным образом. В любом случае, перед тем, как построить клиентское приложение для взаимодействия со службой, давайте немного углубимся в изучение роли класса ServiceHost и элемента <service.serviceModel>, а также роли служб обмена метаданными (metadata exchange — МЕХ).
932 Часть V. Введение в библиотеки базовых классов .NET Подробный анализ типа ServiceHost Класс ServiceHost применяется для конфигурирования и представления службы WCF из приложения-хоста. Однако имейте в виду, что этот тип будет использоваться напрямую только при построении специальных сборок *.ехе, предназначенных для хостинга служб. Если же для представления службы применяется IIS (или специфичная для Vista и Windows 7 служба WAS), то объект ServiceHost создается автоматически. Как уже было показано, этот тип требует полного описания службы, которое получается динамически через установки конфигурации файла *. con fig хоста. Хотя это происходит автоматически при создании объекта, можно вручную сконфигурировать состояние объекта ServiceHost с помощью ряда его членов. Кроме Ореп() и Close () (которые взаимодействуют со службой в синхронной манере), есть и другие члены этого класса (они перечислены в табл. 25.8). Таблица 25.8. Избранные члены типа ServiceHost Член Назначение Authorization AddDefaultEndpoints () AddServiceEndpointO BaseAddresses BeginOpen() BeginCloseO CloseTimeout Credentials EndOpen() EndCloseO OpenTimeout State Это свойство получает уровень авторизации для размещенной службы Этот метод появился в .NET 4.0 и применяется для программного конфигурирования хоста службы WCF, чтобы он использовал любое количество готовых конечных точек, предоставленных платформой Этот метод позволяет программно регистрировать конечную точку для хоста Это свойство получает список зарегистрированных базовых адресов для текущей службы Эти методы позволяют асинхронно открывать и закрывать объект ServiceHost, используя стандартный синтаксис делегата .NET Это свойство позволяет устанавливать и получать время, отведенное службе на закрытие Это свойство получает мандаты безопасности, используемые текущей службой Эти методы представляют собой асинхронные аналоги BeginOpen() и BeginCloseO Это свойство позволяет устанавливать и получать время, отведенное службе на открытие Это свойство получает значение, которое указывает текущее состояние объекта коммуникации, представленное перечислением CommunicationState (т.е. открыт, закрыт и создан) Чтобы проиллюстрировать некоторые дополнительные аспекты ServiceHost, модифицируйте класс Program, добавив новый статический метод, который выводит различные аспекты текущего хоста: static void DisplayHostlnfo(ServiceHost host) { Console.WriteLine(); Console.WriteLine ("***** Host Info *****");
Глава 25. Введение в Windows Communication Foundation 933 foreach (System.ServiceModel.Description.ServiceEndpoint s in host.Description.Endpoints) { Console.WriteLine("Address: {0}", se.Address); Console.WriteLine("Binding: {0}", se.Binding.Name); Console.WriteLine("Contract: {0}", se.Contract.Name); Console.WriteLine (); } Console.WriteLine(»**********************»); Console.WriteLine(); } Предположим, что этот новый метод вызывается в Main() после открытия хоста: using (ServiceHost serviceHost = new ServiceHost(typeof(MagicEightBallService))) { // Открыть хост и начать прослушивание входящих сообщений. serviceHost.Open(); DisplayHostlnfо(serviceHost); В результате выводится статистика следующего вида: ***** Console Based WCF Host ***** ***** Host Info ***** Address: http://localhost:8080/MagicEightBallService Binding: BasicHttpBinding (Contract: IEightBall ********************** The service is ready. Press the Enter key to terminate service. Подробный анализ элемента <system.serviceModel> Подобно любому XML-элементу, в <system.serviceModel> может определяться набор подэлементов, каждый из которых может быть квалифицирован многочисленными атрибутами. Хотя все детали набора возможных атрибутов описаны в документации .NET Framework 4.0 SDK, ниже приведен скелет, перечисляющий критически важные подэлементы: <system.serviceModel> <behaviors> </behaviors> <client> </client> <commonBehaviors> </commonBehaviors> <diagnostics> </diagnostics> <comContracts> </comContracts> <services> </services> <bindings> </bindings> </system.serviceModel> На протяжении этой главы будут показаны и более экзотические конфигурационные файлы; в табл. 25.9 приведены краткие описания подэлементов.
934 Часть V. Введение в библиотеки базовых классов .NET Таблица 25.9. Подэлементы <service.serviceModel> Подэлемент Назначение behaviors WCF поддерживает различные поведения конечных точек и служб. По сути, поведение позволяет точнее задавать функциональность хоста или клиента bindings Этот элемент позволяет тонко настраивать каждую привязку WCF (basicHttpBinding, netMsmqBinding и т.д.), а также указывать любые специальные привязки, используемые хостом client Этот элемент содержит список конечных точек, используемых клиентом для подключения к службе. Очевидно, что это не слишком полезно в файле *.config хоста comContracts Этот элемент определяет контракты СОМ, обеспечивающие возможность взаимодействия WCF и СОМ commonBehaviors Этот элемент может устанавливаться только внутри файла machine. conf ig. Он применяется для определения всех поведений, используемых каждой службой WCF на данной машине diagnostics Этот элемент содержит установки для средств диагностики WCF Пользователь может включать или отключать трассировку, счетчики производительности и поставщика WMI, а также добавлять специальные фильтры сообщений services Этот элемент содержит коллекцию служб WCF, представленных хостом Включение обмена метаданными Вспомните, что клиентское приложение WCF взаимодействует со службой WCF через промежуточный прокси. Хотя вполне можно написать код прокси вручную, это было бы довольно утомительно и чревато ошибками. В идеале должен использоваться инструмент для генерации необходимого рутинного кода (включая файлы *. con fig клиентской стороны). К счастью, в .NET Framework 4.0 SDK имеется инструмент командной строки (svcutil.exe), предназначенный именно для этих целей. К тому же Visual Studio 2010 предлагает аналогичную функциональность через пункт меню Projects Add Service Reference (ПроектеДобавить ссылку на службу). Чтобы эти инструменты генерировали необходимый код прокси и файл *. con fig, они должны иметь возможность исследовать формат интерфейсов службы WCF и любых определенных контрактов данных (т.е. имена методов и типы параметров). Обмен метаданными (metadata exchange — МЕХ) — это поведение службы WCF, которое может использоваться для тонкой настройки способа обработки службы исполняющей средой WCF Просто говоря, каждый элемент <behavior> может определять набор действий, на которые данная служба может подписываться. WCF предоставляет многочисленные поведения в готовом виде, к тому же можно строить собственные поведения. Поведение МЕХ (которое по умолчанию отключено) перехватит любые запросы метаданных, отправленные через HTTP GET. Чтобы позволить svcutil.exe или Visual Studio 2010 автоматизировать создание необходимого прокси клиентской стороны и файла *.config, понадобится включить МЕХ. Включение МЕХ осуществляется в файле *. с on fig хоста установкой необходимой настройки (или написанием соответствующего кода С#). Во-первых, необходимо добавить новый элемент <endpoint> конкретно для МЕХ. Во-вторых, потребуется опреде-
Глава 25. Введение в Windows Communication Foundation 935 лить поведение WCF для разрешения доступа HTTP GET. В-третьих, нужно ассоциировать это поведение по имени со службой с помощью атрибута behaviorConfiguration в открывающем элементе <service>. И, наконец, в-четвертых, понадобится добавить элемент <host> для определения базового класса этой службы (МЕХ будет искать здесь местоположение описываемых типов). На заметку! Финальный шаг может быть опущен, если вы передаете объект System.Uri для представления базового адреса в виде параметра конструктору SeviceHost. Взгляните на следующий обновленный файл *. con fig хоста, который создает специальный элемент <behavior> (по имени EightBallServiceMEXBehavior), ассоциируемый со службой через атрибут behaviorConfiguration внутри определения <service>: <?xml version="l. 0м encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="MagicEightBa11ServiceLib.MagicEightBa11Service" behaviorConfiguration = "EightBallServiceMEXBehavior"> <endpoint address ="" binding="basicHttpBinding" contract="MagicEightBallServiceLib.IEightBall" /> <!— Включить конечную точку МЕХ --> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <!— Это необходимо добавить, чтобы МЕХ знал адрес нашей службы --> <host> <baseAddresses> <add baseAddress ="http://localhost:8080/MagicEightBallService" /> </baseAddresses> </host> </service> </services> <!— Определение поведения для МЕХ --> <behaviors> <serviceBehaviors> <behavior name="EightBallServiceMEXBehavior" > <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration> Теперь можно перезапустить службу и увидеть описание метаданных в веб-браузере. Для этого при запущенном хосте просто введите следующий URL в строке адреса: http://localhost:8080/MagicEightBallService На домашней странице службы WCF (рис. 25.6) можно получить базовую информацию о том, как программно взаимодействовать с данной службой. Щелчок на гипер- ссылке в верхней части страницы позволяет просмотреть контракт WSDL. Вспомните, что язык описания веб-служб (Web Service Description Language — WSDL) — это грамматика, описывающая структуру веб-служб в заданной конечной точке.
936 Часть V. Введение в библиотеки базовых классов .NET £ Magicl rvice - Windows Inter та Uj It l-.ttt localhost .: !»у:Е ■■•? - - ~ -|til*t|x|[b^ gf Favorites £ MagicEightBallService Service *| " 0 " □ # " Page» Safety* Tools» 0» MagicEightBallService Service 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 svcutil.exe tool from the command line with the following syntax: svcutil.exe ftttp://localhoat;S0g0/HaqicExgatBall5ervice?wsdr' This will generate a configuration file and a code file that contains the client class, Add the two files to your client application and use the generated client class Щ to call the Service. For example: # class lest , ■* i static void Main О < EigbtBallClxenr client - new EightEallClient(); // Dae the 'client' variable to call operations on the iimcc. // Always close the client. client.Close(); ' ф Internet | Protected Mode Off *i * «uoo% " Рис. 25.6. Просмотр метаданных с использованием МЕХ Хост теперь представляет две различные конечные точки (одна для службы и одна для МЕХ), поэтому консольный вывод хоста будет выглядеть следующим образом: ***** Console Based WCF Host ***** ***** Host Info ***** Address: http://localhost:8080/MagicEightBallService Binding: BasicHttpBinding Contract: IEightBall Address: http://localhost:8 08 0/MagicEightBallService/mex Binding: MetadataExchangeHttpBinding Contract: IMetadataExchange •••••••••••••••••••••• The service is ready. Исходный код. Проект MagicEightBallServiceHost доступен в подкаталоге MagicEight BallServiceHTTP каталога Chapter 25. Построение клиентского приложения WCF Теперь, имея готовый хост, последняя задача заключается в построении фрагмента программного обеспечения для взаимодействия с этим служебным типом WCF. Хотя можно избрать длинный путь и построить всю необходимую инфраструктуру вручную (осуществимая, но трудоемкая задача), в .NET Framework 4.0 SDK предлагается несколько подходов для быстрой генерации прокси клиентской стороны. Для начала создайте новое консольное приложение по имени MagicEightBallServiceClient.
Глава 25. Введение в Windows Communication Foundation 937 Генерация кода прокси с использованием svcutil.exe Первый способ построения прокси клиентской стороны предусматривает использование инструмента командной строки svcutil.exe. С его помощью можно генерировать новый файл на языке С#, представляющий код прокси, а также конфигурационный файл клиентской стороны. Для этого укажите в первом параметре конечную точку службы. Флаг /out: используется для определения имени файла *.cs, содержащего код прокси, а флаг /conf ig: позволяет указать имя генерируемого файла *.conf ig клиентской стороны. Предполагая, что служба запущена, следующий набор параметров, переданный svcutil.exe, приведет к генерации двух новых файлов в рабочем каталоге (вся команда с параметрами должна вводиться в одной строке внутри окна командной строки Visual Studio 2010): svcutil http://localhost:8080/MagicEightBallService /out:myProxy.cs /config-.app. config Открыв файл myProxy.cs, вы найдете там представление интерфейса IEightBall клиентской стороны, а также новый класс по имени EightBallClient, который и является классом прокси. Этот класс унаследован от обобщенного класса System. ServiceModel.ClientBase<T>, где Т — зарегистрированный интерфейс службы. В дополнение к ряду специальных конструкторов, каждый метод прокси (который основан на исходных методах интерфейса) будет реализован для использования унаследованного свойства Channels с целью вызова корректного метода службы. Ниже показан частичный код типа прокси: [System. Diagnostics . DebuggerStepThroughAttnbute () ] [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", .0.0.0")] public partial class EightBallClient : System.ServiceModel.ClientBase<IEightBall>, IEightBall { public string ObtainAnswerToQuestion(string userQuestion) { return base.Channel.ObtainAnswerToQuestion(userQuestion); } } При создании экземпляра типа прокси в клиентском приложении базовый класс установит соединение с конечной точкой, используя установки, указанные в конфигурационном файле приложения клиентской стороны. Во многом подобно конфигурационному файлу серверной стороны, сгенерированный файл App.conf ig стороны клиента содержит элемент <endpoint> и детали, которые касаются привязки basicHttpBinding, используемой для взаимодействия со службой. Кроме того, там имеется следующий элемент <client>, который устанавливает ABC с точки зрения клиента: <client> <endpoint address="http://localhost:8080/MagicEightBallService" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IEightBall" contract="ServiceReference.IEightBall" name="BasicHttpBinding_IEightBall" /> </client> Теперь можно было бы включить эти два файла в проект клиента (вместе со ссылкой на сборку System.ServiceModel.dll) и применять тип прокси для коммуникаций с удаленной службой WCF. Однако воспользуемся другим подходом и посмотрим, как
938 Часть V. Введение в библиотеки базовых классов .NET Visual Studio может помочь в дальнейшей автоматизации создания файлов прокси клиентской стороны. Генерация кода прокси с использованием Visual Studio 2010 Подобно любому хорошему инструменту командной строки, в svcutil.exe предусмотрено огромное количество опций, которые можно использовать для управления процессом генерации прокси. Если же расширенные опции не нужны, те*же два файла можно сгенерировать в IDE-среде Visual Studio 2010. Просто выберите пункт Add Service Reference (Добавить ссылку на службу) в меню Project (Проект). После выбора этого пункта меню будет предложено ввести URI службы. Щелкните на кнопке Go (Перейти), чтобы увидеть описание службы (рис. 25.7). ■и То sec То see a list of available services on • specific server, services, click Discover. http://1ocalhoste080/MagicEightBallService Services: flperatiorw: j, 1 services) found at address 'rrttp://localhost80eO/MagicEightBallSer/ice * ® Ш WagicEightBallService .>* IEightBall ♦ObtainAnswerToQuestion Namespace ServiceReference Advanced- PMC. 25.7. Генерация файлов прокси с использованием Visual Studio 2010 Помимо создания и вставки файлов прокси в текущий проект, этот инструмент автоматически установит ссылки на сборки WCF. В соотве гствии с соглашением об именовании, класс прокси определен в пространстве имен ServiceReference, вложенном в клиентское пространство имен (во избежание возможных конфликтов имен). Ниже приведен полный код клиента: // Нахождение прокси. using MagicEightBallServiceClient.ServiceReferencel; namespace MagicEightBallServiceClient { class Program static void Main(string[] args) { Console.WnteLineC***** Ask the Magic 8 Ball *****\n"); using (EightBallClient ball = new EightBallClient ()) { Console.Write("Your question: " ) ; string question = Console.ReadLine(); string answer = ball.ObtainAnswerToQuestion(question);
Глава 25. Введение в Windows Communication Foundation 939 Console.WriteLine("8-Ball says: {0 } ", answer); } Console.ReadLine () ; } } } Предполагая, что хост WCF запущен, можно выполнить программу клиента. Далее представлен возможный вывод: ***** Ask the Magic 8 Ball ***** Your question: Will I get this book done soon? 8-Ball says: No Press any key to continue . . . Исходный код. Проект MagicEightBallServiceClient доступен в подкаталоге MagicEightBallServiceHTTP каталога Chapter 25. Конфигурирование привязки на основе TCP К этому моменту приложения хоста и клиента сконфигурированы на использование простейшей из привязок на основе HTTP — basicHttpBinding. Вспомните, что преимущество переноса настроек в конфигурационные файлы связано с возможностью менять внутренние механизмы в декларативной манере и предоставлять множество привязок для одной и той же службы. Для целей демонстрации давайте поставим небольшой эксперимент Создайте новую папку на диске С: (или где был сохранен код) по имени EightBallTCP и внутри него два подкаталога с именами Host и Client. Затем в проводнике Windows перейдите в папку bin\Debug проекта хоста и скопируйте MagicEightBallServiceHost.exe, MagicEightBallServiceHost.exe.config и MagicEightBallServiceLib.dll в папку C:\EightBallTCP\Host. Откройте файл *. con fig в простом текстовом редакторе и модифицируйте его содержимое следующим образом: <?xml version="l.О" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="MagicEightBallServiceLib.MagicEightBallService"> <endpoint address ="" binding="netTcpBinding11 contract="MagicEightBallServiceLib.IEightBall"/> <host> <baseAddresses> <add baseAddress ="net.tcp://localhost:80 90/MagicEightBallService"/> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration> По сути, из файла *. con fig удалены все установки МЕХ (поскольку уже построен прокси) и установлено использование типа NetTcpBinding через уникальный порт. Теперь запустите приложение двойным щелчком на его файле *.ехе. Если все сделано правильно, должен появиться вывод, показанный ниже:
940 Часть V. Введение в библиотеки базовых классов .NET ***** Console Based WCF Host ***** ***** Host Info ***** Address: net.tcp://localhost:80 90/MagicEightBallService Binding: NetTcpBinding Contract: IEightBall ********************** The service is ready. Press the Enter key to terminate service. Для завершения этого теста скопируйте файлы MagicEightBallServiceClient.exe и MagicEightBallServiceClient.exe.config из папки bin\Debug клиентского приложения в папку C:\EightBallTCP\Client. Обновите конфигурационный файл следующим образом: <?xml version="l. 0" encoding="utf-811 ?> <configuration> <system.serviceModel> <client> <endpoint address="net.tcp://localhost:8090/MagicEightBallService" binding="netTcpBinding" contract="ServiceReferencel.IEightBall" name="netTcpBinding_IEightBall11 /> </client> </system. serviceMod-el> </configuration> Этот конфигурационный файл клиентской стороны значительно проще файла, создаваемого генератором прокси Visual Studio. Обратите внимание, что существующий элемент <bindings> полностью удален. Изначально файл *.config содержал элемент <bindings> с подэлементом <basicHttpBinding>, который определял множество деталей установок привязки клиента (таймауты и т.п.). В действительности для рассматриваемого примера эти детали никогда не понадобятся, поскольку мы автоматически получим значения по умолчанию лежащего в основе объекта BasicHttpBinding. При необходимости можно было бы модифицировать существующий элемент <bindings>, определив детали подэлемента <netTcpBinding>, но это совершенно не обязательно, если устраивают значения по умолчанию объекта NetTcpBinding. В любом случае, теперь вы должны быть готовы запустить клиентское приложение, и если хост все еще работает в фоновом режиме, вы сможете перемещать данные между сборками по протоколу TCP. Исходный код. Проект MagicEightBallTCP доступен в подкаталоге Chapter 25. Упрощение конфигурационных настроек в WCF 4.0 Работая над первым примером этой главы, вы могли заметить, что логика конфигурации хостинга довольно громоздка. Например, файл *. config (для начальной базовой привязки HTTP) должен определять элемент <endpoint> службы, второй элемент <endpoint> для МЕХ, элемент <baseAddresses> (необязательный) для сокращения избыточных URI, а затем еще раздел <behaviors> для определения характеристик обмена метаданными во время выполнения. По правде говоря, изучение правил написания файлов *.config может оказаться довольно трудным при построении служб WCF. Чтобы еще более усложнить картину, значительное количество служб WCF склонны требовать одинаковых базовых установок в конфигурационном файле хоста. Например, если создается совершенно новая служ-
Глава 25. Введение в Windows Communication Foundation 941 ба WCF и совершенно новый хост, и нужно представить эту службу, используя элемент <basicHttpBinding> с поддержкой МЕХ, необходимый файл *.conf ig будет выглядеть практически идентично созданному ранее. К счастью, в .NET 4.0 API-интерфейс Windows Communication Foundation включает ряд упрощений, в числе которых установки по умолчанию (и прочие сокращения), облегчающие процесс построения конфигурации хоста. Конечные точки по умолчанию в WCF 4.0 Когда в .NET 3.5 вызывался метод Ореп() на объекте ServiceHost, а в конфигурационном файле еще не было определено ни одного элемента <endpoint>, исполняющая среда генерировала исключение. Тот же результат получался при вызове метода AddServiceEndpoint () в коде для указания конечной точки. В версии .NET 4.0 каждая служба WCF автоматически получает конечные точки по умолчанию, которые фиксируют общепринятые детали конфигурации для каждого поддерживаемого протокола. Открыв файл machine.config для .NET 4.0, вы обнаружите в нем новый элемент по имени <protocolMapping>. Этот элемент документирует, какие привязки WCF следует использовать по умолчанию, если никаких привязок явно не указано: <system.serviceModel> <protocolMapping> <add scheme="http" binding="basicHttpBinding"/> <add scheme="net.tcp" binding="netTcpBinding"/> <add scheme="net.pipe" binding="netNamedPipeBinding"/> <add scheme="net.msmg" binding="netMsmgBinding"/> </protocolMapping> </system.serviceModel> Все, что нужно для использования этих привязок по умолчанию — указать ^базовый адрес в конфигурационном файле хоста. Чтобы увидеть это в действии, откройте проект MagicEightBallServiceHost в Visual Studio. После этого модифицируйте файл *. config хостинга, полностью удалив элемент <endpoint> для службы WCF и данные, связанные с МЕХ. В результате конфигурационный файл должен выглядеть примерно так: <configuration> <system.serviceModel> <services> <service name="MagicEightBallServiceLib.MagicEightBallService" > <host> <baseAddresses> <add baseAddress="http://localhost:8080/MagicEightBallService"/> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration Поскольку в <baseAddress> указан действительный HTTP-адрес, хост автоматически использует basicHttpBinding. Запустив хост снова, можно увидеть тот же вывод: ***** Console Based WCF Host ***** ***** Host Info ***** Address: http://localhost:8080/MagicEightBallService Binding: BasicHttpBinding Contract: IEightBall
942 Часть V. Введение в библиотеки базовых классов .NET ********************** The service is ready. Press the Enter key to terminate service. Пока еще не включены данные МЕХ; это будет сделано чуть ниже с использованием другого упрощения .NET 4.0, которое называется конфигурациями поведения по умолчанию. Однако сначала давайте разберемся, как предоставить одиночную службу WCF с множеством привязок. Предоставление одной службы WCF с использованием множества привязок Со времен своего первого выпуска WCF позволяет одному хосту предоставлять службу WCF с несколькими конечными точками. Например, чтобы предоставить MagicEightBallService с использованием привязок HTTP, TCP и именованного канала, необходимо просто добавить новые конечные точки в конфигурационный файл. После перезапуска хоста вся необходимая оснастка будет создана автоматически. Это огромное преимущество по многим причинам. До WCF было трудно предоставить одну службу с применением множества привязок, так как каждый тип привязки (т.е. HTTP и TCP) имел собственную модель программирования. Тем не менее, возможность разрешить вызывающему коду выбирать наиболее подходящую привязку чрезвычайно удобна. Внутренние клиенты могут отдать предпочтение привязкам TCP, внешние клиенты (находящиеся за брандмауэром компании) — использовать для доступа HTTP, в то время как клиенты, находящиеся на той же машине выберут именованный канал. До появления .NET 4.0 для этого в конфигурационном файле хоста нужно было определять несколько элементов <endpoint> вручную. Также для каждого протокола требовалось определять множество элементов <baseAddress>. Однако теперь можно просто поместить в конфигурационный файл следующие данные: <configuration> <system.serviceModel> <services> <service name="MagicEightBallServiceLib.MagicEightBallService"> <host> <baseAddresses> <add baseAddress="http://localhost:8080/MagicEightBallService" /> <add baseAddress="net.tcp://localhost:8099/MagicEightBallService" /> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration> Скомпилировав проект (для обновления развернутого файла *.config) и перезапустив хост, можно будет увидеть следующие данные конечной точки: ***** Console Based WCF Host ***** ***** Host Info ***** Address: http://localhost:8080/MagicEightBallService Binding: BasicHttpBinding Contract: IeightBall Address: net.tcp://localhost:8099/MagicEightBallService Binding: NetTcpBinding Contract: IEightBall ********************** The service is ready. Press the Enter key to terminate service.
Глава 25. Введение в Windows Communication Foundation 943 Теперь, когда служба WCF достижима из двух конечных точек, возникает вопрос: как клиент сможет производить выбор между ними? При генерации прокси клиентской стороны инструмент Add Service Reference назначит каждой представленной конечной точке строковое имя в клиентском файле *.config. В коде можно передавать корректное строковое имя конструктору прокси и быть уверенным, что будет выбрана правильная привязка. Однако прежде чем сделать это, потребуется переустановить МЕХ для модифицированного конфигурационного файла хоста и научиться настраивать параметры привязки по умолчанию. Изменение установок для привязки WCF В случае указания ABC службы в коде С# (это рассматривается далее в главе) для изменения параметров по умолчанию привязки WCF необходимо просто модифицировать значения свойств объекта. Например, чтобы использовать BasicHttpBinding, но изменить установки таймаута, можно написать следующий код: void ConfigureBindinglnCode() { BasicHttpBinding binding = new BasicHttpBinding(); binding.OpenTimeout = TimeSpan.FromSecondsC0); } Настройки привязки всегда можно конфигурировать декларативно. Например, в .NET 3.5 можно создавать конфигурационный файл хоста, в котором изменяется свойство OpenTimeout класса BasicHttpBinding: <configuration> <system.serviceModel> <bindings> <basicHttpBinding> <binding name = "myCustomHttpBinding" OpenTimeout = 0:00:30" /> </basicHttpBinding> </bindings> <services> <service name = "WcfMathService.MyCalc"> <endpoint address = "http://localhost:8080/MyCalc" binding = "basicHttpBinding" bindingConfiguration = "myCustomHttpBinding" contract = "WcfMathService.IBasicMath" /> </service> </services> </system.serviceModel> </configuration> В результате получается конфигурационный файл для службы по имени WcfMathService.MyCalc, которая поддерживает единственный интерфейс IBasicMath. Обратите внимание, что раздел <bindings> позволяет определить именованный элемент <binding>, который изменяет настройки для заданной привязки. Внутри <endpoint> службы специфические настройки можно подключать с использованием атрибута bindingConfiguration. Такая конфигурация хостинга по-прежнему работает и в .NET 4.0; однако в случае использования конечной точки по умолчанию подключить <binding> к <endpoint> не удастся. К счастью, параметрами конечной точки по умолчанию можно управлять, просто опустив атрибут name элемента <binding>. Например, в следующем фрагменте кода изменяются некоторые свойства объектов BasicHttpBinding и NetTcpBinding, используемые по умолчанию:
944 Часть V. Введение в библиотеки базовых классов .NET <configuration> <system.serviceModel> <services> <service name="MagicEightBallServiceLib.MagicEightBallService"> <host> <baseAddresses> <add baseAddress="http://localhost:8080/MagicEightBallService" /> <add baseAddress= "net.tcp://localhost:8099/MagicEightBallService" /> </baseAddresses> </host> </service> </services> <bindings> <basicHttpBinding> <binding openTimeout = 0:00:30" /> </basicHttpBinding> <netTcpBinding> <binding closeTimeout=0:00:15"/> </netTcpBinding> </bindings> </system.serviceModel> </configuration> Конфигурация поведения МЕХ по умолчанию в WCF 4.0 Инструмент генерации прокси должен обнаружить композицию службы во время выполнения, прежде чем он сможет продолжить работу. В WCF такое обнаружение во время выполнения разрешается включением МЕХ. В большинстве конфигурационных файлов хоста МЕХ должно быть включено (по крайней мере, во время разработки); к счастью, способ конфигурирования МЕХ редко изменяется, поэтому .NET 4.0 предлагается несколько удобных сокращений. Наиболее полезное из них — готовая поддержка МЕХ. Не нужно добавлять конечную точку МЕХ, определять именованное поведение службы МЕХ и затем подключать именованную привязку к службе (как это делалось в HTTP-версии MagicEightBallServiceHost). Вместо этого теперь можно просто применить следующий код: <configuration> <system.serviceModel> <services> <service name="MagicEightBallServiceLib.MagicEightBallService"> <host> <baseAddresses> <add baseAddress="http://localhost:8080/MagicEightBallService" /> <add baseAddress= "net.tcp://localhost:8099/MagicEightBallService" /> </baseAddresses> </host> </service> </services> <bindings> <basicHttpBinding> <binding openTimeout = 0:00:30" /> </basicHttpBinding> <netTcpBinding> <binding closeTimpout=0:00:15"/> </netTcpBinding> </bindings>
Глава 25. Введение в Windows Communication Foundation 945 <behaviors> <serviceBehaviors> <behavior> <•-- Для получения МЕХ по умолчанию не назначайте имя элементу <serviceMetadata> --> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration> Трюк состоит в том, что элемент <serviceMetadata> не имеет атрибута name (также обратите внимание, что элементу <service> больше не нужен атрибут behavior Configuration). После этих корректировок получается поддержка МЕХ во время выполнения. Чтобы удостовериться в этом, запустите хост (после компиляции и обновления конфигурационного файла) и введите следующий URL в браузере: http://localhost:808O/MagicEightBallService После этого можно щелкнуть на ссылке wsdl в верхней части веб-страницы и просмотреть WSDL-описание службы (см. рис. 25.6). В консольном окне хоста в выводе будут отсутствовать данные о конечной точке МЕХ, потому что она не определялась явно для IMetadataExchange в конфигурационном файле. Тем не менее, МЕХ включен и можно приступать к построению клиентских прокси. Обновление клиентского прокси и выбор привязки Предполагая, что обновленный хост скомпилирован и выполняется в фоновом режиме, теперь необходимо открыть клиентское приложение и обновить текущую ссылку на службу. Начните с открытия папки Service Refreshes (Ссылки на службу) в Solution Explorer. Затем щелкните правой кнопкой мыши на элементе ServiceReference и выберите в контекстном меню пункт Update Service Reference (Обновить ссылку на службу), как показано на рис. 25.8, [Solution Explorer 1 £9 Solut'or» 'MagicEightBaltServiceClient' A project) j 3 MagkEightBalServiceCEefit r» ,;M Properties > ^| References л \^Л Service References #j ServiceReferenc jj> app.config cJj| Program.cs Update Service Reference Configure Service Reference... View in Object Browser A Cut -J Copy Jj Paste X Delete Rename !Ql Properties Я -'^ Solution Explorer H 6 Ctrl+X Ctrl+C Ctri+V Del Alt+ Enter *■ nx] Рис. 25.8. Обновления прокси и файла *.config клиентской стороны
946 Часть V. Введение в библиотеки базовых классов .NET После этого в файле *.config появятся на выбор две привязки: одна для HTTP и другая — для TCP. Каждой привязке назначено подходящее имя. Ниже приведен частичный листинг обновленного конфигурационного файла: <configuration> <system.serviceModel> <bindings> <basicHttpBinding> <binding name="BasicHttpBinding_IEightBall11 . . . /> </basicHttpBinding> <netTcpBinding> <bmding name="NetTcpBinding_IEightBall11 . . . /> </netTcpBinding> </bindings> </system.serviceModel> </configuration> Клиент может использовать эти имена при создании прокси-объекта для выбора желаемой привязки. Таким образом, если клиент предпочитает применять TCP, можно изменить код С# клиентской стороны следующим образом: static void Main(string [ ] args) { Console.WriteLine ("***** Ask the Magic 8 Ball *****\n"); using (EightBallClient ball = new EightBallClient(llNetTcpBinding_IEightBall") ) { } Console.ReadLine(); } Если же клиент вместо этого будет использовать привязку HTTP, можно написать так: using (EightBallClient ball = new EightBallClient (,,BasicHttpBinding_IEightBall" ) ) { }" На этом текущий пример, который продемонстрировал ряд полезных средств WCF 4.0, завершен. Данные средства упрощают написание конфигурационных файлов хостинга. Далее будет показано, как использовать шаблон проекта WCF Service Library (Библиотека служб WCF). Исходный код. Проект MagicEightBallServiceHTTPDefaultBindings доступен в подкаталоге Chapter 25. Использование шаблона проекта WCF Service Library Прежде чем строить более экзотическую службу WCF, которая будет взаимодействовать с базой данных AutoLot, созданной в главе 21, в следующем примере иллюстрируется ряд важных тем, включая преимущества от использования шаблона проекта WCF Service Library, приложения WCF Test Client, редактора конфигурации WCF, хостинга служб WCF внутри службы Windows и асинхронных клиентских вызовов. Чтобы сосредоточить все внимание на новых концепциях, эта служба WCF также будет умышленно простой.
Глава 25. Введение в Windows Communication Foundation 947 Построение простой математической службы Для начала создайте новый проект WCF Service Library под названием MathServiceLibrary, выбрав соответствующую опцию в узле WCF диалогового окна New Project (рис. 25.2). Затем измените имя начального файла IServicel.cs на IBasicMath.cs. После этого удалите весь код внутри пространства имен MathServiceLibrary и замените его следующим: [ServiceContract(Namespace="http://MyCompany.com")] public interface IBasicMath { [OperationContract] int Add(int x, int y) ; } Измените имя файла Servicel.cs на MathService.es, удалите весь код внутри пространства имен MathServiceLibrary и реализуйте контракт службы, как показано ниже: public class MathService : IBasicMath { public int Add(int x, int y) { // Эмулировать длительный запрос. System.Threading.Thread.SleepE000); return x + y; } } Наконец, откройте файл App.config и замените все вхождения IServicel на IBasicMath, а также все вхождения Servicel на MathService. Обратите внимание, что этот файл *.conf ig уже включает поддержку МЕХ и по умолчанию использует протокол WsHttpBindintg. Тестирование службы WCF с помощью WcfTestClient.exe Одно из преимуществ применения проекта WCF Service Library состоит в том, что при отладке или запуске библиотеки он читает установки из файла *.configH использует их для загрузки приложения WCF Test Client (WcfTestClient.exe). Это приложение с графическим интерфейсом позволяет протестировать каждый член интерфейса службы по мере ее построения, вместо того, чтобы вручную строить хост/клиент, как это делалось ранее, просто для целей тестирования. На рис. 25.9 показана тестовая среда для MathService. Обратите внимание, что двойной щелчок на методе интерфейса позволяет указать входные параметры и вызвать метод. Эта утилита работает в готовом виде после создания проекта WCF Service Library, однако имейте в виду, что данный инструмент можно применять для тестирования любой службы WCF, запустив его в командной строке и указав конечную точку МЕХ. Например, если запустить приложение MagicEightBallServiceHost.exe, можно ввести следующую команду в окне командной сроки Visual Studio 2010: wcftestclient http://localhost:8 080/MagicEightBallService После этого можно вызвать ObtainAnswerToQuestionO аналогичным образом.
948 Часть V. Введение в библиотеки базовых классов .NET File Tools Help В Щь My Service Projects S $j| nttp:/y1ocelhost:80aD/Ma*hService/m«! Щ 5° IBascMath (WSHttpBndmgJBeKJ <rj} Config He IL_ Value 22 E5 Type System.N32 System Ы32 О Start a new proxy Invoke j Name fretum) Type System.lnt32 Formatted |XML | Service invocation completed. Рис. 25.9. Тестирование службы WCF с использованием WcfTestClient.exe Изменение конфигурационных файлов С помощью SvcConfigEditor.exe Другое преимущество применения проекта WCF Service Library состоит в том, что щелчком правой кнопкой мыши на файле App.config внутри Solution Explorer можно активизировать графический редактор конфигурирования службы (Service Configuration Editor), SvcConfigEditor.exe (рис. 25.10). Та же техника может применяться из клиентского приложения, которое ссылается на службу WCF. 1 Solution Explorer -j j3 Л 1  Solution MathSer л «$9 MathServkeL ^1 Properties ^ References _^ -^pp.confic jj JBasicMath £) MathServic 1 ^5 Solution Explorer I iceLibrary' A project] brary J Open Open With.» ^ Edit WCF Configuration Exclude From Project A Cut -J| Copy X Delete Rename u Properties - r Ctrl+X Ctrl* С Dei Aft* Enter ">] Рис. 25.10. Запуск графического редактора файлов *. con fig Запустив этот инструмент, можно изменять данные в формате XML, используя дружественный графический интерфейс. Существует много очевидных преимуществ от применения подобных инструментов для сопровождения файлов *.config. Первое: имеется уверенность, что сгенерированная разметка соответствует ожидаемому формату и свободна от опечаток. Второе: это замечательный способ увидеть правильные значения, которые могут быть присвоены каждому атрибуту. И третье: больше не понадобится вручную вводить данные XML. На рис. 25.11 показан внешний вид редактора Service Configuration Editor. По правде говоря, описание всех интересных опций SvcConfigEditor.exe заняло бы отдельную главу (интеграция с СОМ+, создание файлов *.conf ig и т.п.). Найдите время, чтобы изу-
Глава 25. Введение в Windows Communication Foundation 949 чить этот инструмент, пользуясь детальной справочной системой, которая вызывается нажатием клавиши <F1>. На заметку! Утилита SvcConfigEditor.exe позволяет редактировать (или создавать) конфигурационные файлы, даже если не был выбран начальный проект WCF Service Library. Запустите этот инструмент в окне командной строки Visual Studio 2010 и воспользуйтесь пунктом меню File^Open (Файл^Открыть) для загрузки существующего файла *. con fig для редактирования. Конфигурировать MathService больше не нужно, поэтому можно перейти к задаче построения специального хоста. Рис. 25.11. Работа с редактором WCF Service Configuration Editor Хостинг службы WCF в виде службы Windows Хостинг службы WCF в консольном приложении (или внутри настольного приложения с графическим интерфейсом) — не идеальный выбор для сервера производственного уровня, учитывая, что хост всегда должен оставаться запущенным в фоновом режиме и готовым к обслуживанию клиентов. Даже если свернуть приложение-хост в панели задач Windows, все равно останется вероятность нечаянно закрыть его, тем самым разорвав все соединения с клиентскими приложениями. На заметку! Хотя и верно, что настольное приложение Windows не обязано отображать главное окно, типичная программа *.ехе требует взаимодействия с пользователем для загрузки и запуска. Служба Windows (описанная ниже) может быть сконфигурирована для запуска, даже если ни один пользователь не зарегистрировался на рабочей станции (не вошел в систему). I Если вы строите "домашнее" приложение WCF, то еще одной альтернативой для хостинга библиотеки службы WCF является развертывание ее в службе Windows. Преимущество такого решения состоит в том, что служба Windows может быть сконфигурирована для автоматического запуска вместе с загрузкой системы на целевой машине. Другое преимущество связано с тем, что служба Windows работает невидимо, в фоновом режиме (в отличие от консольного приложения), и не требует участия пользователя.
950 Часть V. Введение в библиотеки базовых классов .NET Чтобы проиллюстрировать построение такого хоста, начните с создания проекта службы Windows по имени MathWindowsServiceHost (рис. 25.12). Переименуйте начальный файл Servicel.cs на MathService.cs, используя Solution Explorer. ЛеЬ Office Cloud Service Reporting Sitvertight kd* WPF Custom Control Library Visual C* t C**j Empty Project Visual C* !g#] Windows Service Visual C# ■*crt WPF User Control Library Visual C# шс0\ Windows Forms Control Libr... Visual C* Per user extensions are currently not allowed to load. 1п«Ые кал4игд. (' > м ноу extenwom Name MathWindowsServiceHost Location: CAMyCode ' Solution- Create new solution * Solution name Type: Visual C* A project for creating Windows Services Create directory for solution Add to source control Рис. 25.12. Создание службы Windows для хостинга службы WCF Спецификация ABC в коде Теперь, предполагая, что ссылки на сборки MathServiceLibrary.dll и System. ServiceModel.dll установлены, все, что осталось сделать — это использовать тип ServiceHost внутри методов OnStartO и OnStopO типа службы Windows. Откройте файл кода класса службы-хоста (щелчком правой кнопкой мыши в визуальном конструкторе и выбором в контекстном меню пункта View Code (Просмотреть код)) и добавьте следующий код: //Не забудьте импортировать пространства имен: using MathServiceLibrary; using System.ServiceModel; namespace MathWindowsServiceHost { public partial class MathWinService: ServiceBase { // Переменная-член типа ServiceHost. private ServiceHost myHost; public MathWinService () { InitializeComponent(); } protected override void OnStart (string[] args) { // Проверить для подстраховки. if (myHost '= null) { myHost.Close () ; myHost = null; } // Создать хост. myHost = new ServiceHost(typeof(MathService) ) ;
Глава 25. Введение в Windows Communication Foundation 951 // Указать ABC в коде. Uri address = new Uri ("http://localhost:8080/MathServiceLibrary"); WSHttpBinding binding = new WSHttpBinding(); Type contract = typeof (IBasicMath); // Добавить эту конечную точку. myHost.AddServiceEndpoint(contract, binding, address); // Открыть хост. myHost.Open (); } protected override void OnStopO { // Остановить хост. if(myHost '= null) myHost.Close (); } } } Хотя ничто не мешает применять конфигурационный файл при построении хоста для службы WCF на основе службы Windows, здесь (для разнообразия) вместо использования файла *.conf ig конечная точка устанавливается программно с помощью классов Uri, WSHttpBinding и Туре. После создания всех аспектов ABC хост программно информируется о них вызовом AddServiceEndpoint(). Если нужно информировать исполняющую среду о том, что необходимо получить доступ к каждой из привязок конечных точек по умолчанию, описанных в конфигурационном файле .NET 4.0 machine.config, можно упростить программную логику, указав базовый адрес при вызове конструктора ServiceHost. В этом случае не потребуется специфицировать ABC в коде вручную или вызывать AddServiceEndPointO, а просто вызвать AddDefaultEndPoints(). Взгляните на следующее изменение кода: protected override void OnStart(string [ ] args) { if (myHost != null) { myHost.Close (); } // Создать хост и указать URL для привязки HTTP. myHost = new ServiceHost(typeof(MathService), new Uri("http://localhost:8080/MathServiceLibrary")); // Выбрать конечные точки по умолчанию. myHost.AddDefaultEndpoints(); // Открыть хост. myHost.Open(); } Включение МЕХ Хотя включить МЕХ можно и программно, сделаем это в конфигурационном файле WCF 4.0. Добавьте новый файл Арр.config к проекту службы Windows, внеся в него следующие настройки МЕХ: <?xml version="l .0" encoding="utf-811 ?> <configuration> <system.serviceModel> <services> <service name=llMathServiceLibrary.MathServicell> </service> </services>
952 Часть V. Введение в библиотеки базовых классов .NET <behaviors> <serviceBehaviors> <behavior> <serviceMetadata httpGetEnabled=lltrue" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration> Создание программы установки для службы Windows Чтобы зарегистрировать службу Windows в операционной системе, нужно добавить к проекту программу установки, которая будет содержать необходимый код для регистрации службы. Для этого щелкните правой кнопкой мыши на поверхности визуального конструктора службы Windows и выберите в контекстном меню пункт Add Installer (Добавить программу установки), как показано на рис. 25.13. MsthWtnService.es (Oesion] хЩ gj View Code То add components Properties window to Line Up Icons У°игс Show Large Icons Add Installer J* Properties РУГИ F7 CtrUV ootbox and use the Tods and events for iew. Рис. 25.13. Добавление программы установки для службы Windows В результате на поверхность визуального конструктора добавятся два новых компонента. Первый компонент (именуемый по умолчанию serviceProcessInstallerl) представляет тип, который может установить новую службу Windows на целевой машине. Выберите этот тип в визуальном конструкторе и воспользуйтесь окном Properties (Свойства) для установки свойства Account в LocalSystem (рис. 25.14). Второй компонент (под названием servicelnstaller) представляет тип, который устанавливает конкретную службу Windows. В окне Properties измените значение свойства ServiceName на MathService (это дружественное отображаемое имя для зарегистрированной службы Windows), установите свойство StartType в Automatic и добавьте дружественное описание зарегистрированной службы Windows в свойстве Description (рис. 25.15). 1 Properties 1 serviceProcessInstallerl System 'Пэте: GenerateMember HeJpTert Modifiers Parent .ServicePrccess.ServicePrccessi servic eProc esslnst aller 1 Ш LocalSystem True Private Projectlnstafler 1 Account 1 Indicates the account type under which the service will run. » nxl nstaller » 1  Рис. 25.14. Служба Windows должна запускаться от имени учетной записи LocalSystem
Глава 25. Введение в Windows Communication Foundation 953 [Properties 1 servicelnstallerl System.Serv : ;i J > U (Name) DelayedAutoStart DisplayName GenerateMember HelpText Modifiers Parent ServiceName 1 i> Ser.icesDependedOn StartType 1 Description 1 Indicates the service's descript 1 service!. ceProces » nxl s.Ser. icelnstaller * 1 servicelnstallerl False H This is the math service! MathService True Private Projecllnstailer MathService Stnngfj Array Automatic on (a brief comment that explains the purpose of the Рис. 25.15. Конфигурирование деталей, связанных с программой установки После этого можно компилировать приложение. Установка службы Windows Служба Windows может быть установлена на машине-хосте с помощью традиционной программы установки (такой как *.msi) или инструмента командной строки installutil.exe. В окне командной строки Visual Studio 2010 перейдите в папку bin\Debug проекта MathWindowsServiceHost и введите следующую команду: installutil MathWindowsServiceHost.exe Предполагая, что установка прошла успешно, можно щелкнуть на значке Services (Службы) в папке Administrative Tools (Администрирование) панели управления. В списке служб, упорядоченном по алфавиту, должно присутствовать дружественное имя службы MathService. Запустите эту службу на локальной машине, щелкнув на ссылке Start (Запустить), как показано на рис. 25.16. Services F.te нсисп Scr.ice; lc View Help В я | ► .■ ■ I i JMathServke Description: This is the math seroce! \ Ertended /. Standard / Nunc £fc V Helper :S|iPs*c Policy Agent Si KtmRm for Distributed Tra... -w. Link-Layer Topology Disco.. ',. Media Center Extender Serv, .,, Microsoft .NET Framework. i* Microsoft .NET Framework . 5» Microsoft .NET Framework . R| Microsoft .NET Framework. S4 Microsoft Г5СЯ Initiator Ser.. : J, Microsoft Office Diagnostic Щ Microsoft Software Shade. Descnpticn Provides tunnel connectivity.. Internet Protocol security (IPs. Coordinates transaction» bet.. Creates a fJetwork Map, consi. Allocs Media Center Extender. Microsoft .NET Framework N.. Microsoft .NET Framework N.. Microsoft NET Framework N.. Microsoft .NET Framework N.. Manages Internet SCSI (iSCSD . Run portions of Microsoft Off. Manages software-based votu. *~ Status Started H Startup Type Automatic Manual Manual Manual Disabled Disabled Disabled Automatic iD... Automatic (D... Manual Manual Manual ra Log On As * 1 Local Syste... Network S... Network S... Local Service !l| *| Local Service Local Syste... Щ Local Syste... 1 Local S>ste... Local Syste... Local Syste... Local Syste... Local Syste.,. Рис. 25.16. Служба Windows, исполняющая роль хоста для службы WCF Теперь, когда служба установлена и работает, остается последний шаг — создание клиентского приложения, которое будет ее использовать. Исходный код. Проект MathWindowsServiceHost доступен в подкаталоге Chapter 25.
954 Часть V. Введение в библиотеки базовых классов .NET Асинхронный вызов службы на стороне клиента Создайте новый проект консольного приложения по имени MathClient и установите ссылку на службу на работающую службу WCF, которая в настоящий момент развернута в службе Windows, функционирующей в фоновом режиме, выбрав для этого пункт меню Project^Add Service Reference (ПроектеДобавить ссылку на службу) в Visual Studio (понадобится ввести URL в поле адреса). Однако не щелкайте пока на кнопке ОК. Обратите внимание на кнопку Advanced (Дополнительно) в нижнем левом углу диалогового окна Add Service Reference (Добавление ссылки на службу), как показано на рис. 25.17. То see a list of available services on a specific server, enter a service URL and click Go. To browse for available services, click Discover. Address: 'ЯЯШШШШЯШшт Services: Go 1 I [oiscovtr :*1 0 3] MathService 5° IBaskMath Select a service contract to view its operations. 1 serviced) found at address 'http://tocalbost80eO/MathServiceLibrary'. Namespace: iServkeReferencej CZEZIi J Рис. 25.17. Ссылка на службу MathService и возможность конфигурирования расширенных установок Щелкните на этой кнопке, чтобы увидеть дополнительные установки конфигурации прокси (рис. 25.18). Используя это диалоговое окно, можно генерировать код, который позволит вызывать удаленные методы в асинхронном режиме, если отмечен флажок Generate Asynchronous Operations (Генерировать асинхронные операции). Попробуйте сделать это. После этого код прокси будет содержать дополнительные методы, которые позволяют вызывать каждый член контракта службы с использованием ожидаемого формата асинхронного вызова Begin/End, описанного в главе 19. Ниже показана простая реализация, в которой вместо строго типизированного делегата AsyncCallback применяется лямбда-выражение. using System; using MathClient.ServiceReference; using System.Threading; namespace MathClient { class Program static void Main(string[] args) { Console.WriteLine ("***** The Async Math Client *****\n");
Глава 25. Введение в Windows Communication Foundation 955 using (BasicMathClient proxy = new BasicMathClient ()) { proxy.Open (); // Складывать числа в асинхронном режиме с использованием лямбда-выражения. IAsyncResult result = proxy.BeginAdd B, 3, ar => { Console.WriteLine( + 5 = {0}", proxy.EndAdd(ar)); }, null) ; while (!result.IsCompleted) { Thread.Sleep B00); Console.WriteLine("Client working..."); Console.ReadLine(); Исходный код. Проект MathClient доступен в подкаталоге Chapter 25. Sen ice Reference Settings 1Д Access tevel'for generated classes: SI Generate asynchronous operations Data Type О Always generate message contracts Collection type: ISystemArray Dictionary collection type System.Collections.Genenc.Dictionary V Reuse types in referenced assemblies • Reuse types in all referenced assemblies Reuse types in specified referenced assemblies: O-Omscorlib О-О System □ 'OSystem.Core □ ^OSystem.Data Q *aSystem.Data.DataSetExtension5 О -О System.Runtime.Serialixation В -Q System.ServiceModel Compatibility ' Add a Web Reference instead of a Service Reference. This will generate code based on .N£T Framework 2.0 Web Services technology. Add ЛеЬ Reference... Рис. 25.18. Дополнительные опции конфигурации прокси клиентской стороны Проектирование контрактов данных WCF В последнем примере этой главы рассматривается конструирование контрактов данных WCF. В ранее созданных службах WCF были определены очень простые методы, оперирующие примитивными типами данных CLR. При использовании любого из типов привязок HTTP (basicHttpBinding, wsHttpBinding и т.п.), входящие и исходящие про-
956 Часть V. Введение в библиотеки базовых классов .NET стые типы данных автоматически форматируются в XML-элементы. Кстати говоря, если применяется привязка на основе TCP (такая как netTcpBinding), то параметры и возвращаемые значения простых типов данных передаются в компактном двоичном формате. На заметку! Исполняющая среда WCF также автоматически кодирует любой тип, помеченный атрибутом [Serializable]. Однако это не является предпочтительным способом определения контрактов WCF и предназначено только для обратной совместимости. Однако при определении контрактов служб, которые используют специальные классы в качестве параметров или возвращаемых значений, эти типы должны быть объявлены с применением контракта данных. Просто говоря, контракт данных — это тип, оснащенный атрибутом [DataContract]. Соответственно, каждое поле, которое планируется использовать как часть предполагаемого контракта, должно помечаться атрибутом [DataMember]. На заметку! Поля контракта данных, не помеченные атрибутом [DataMember], не сериализуют- ся исполняющей средой WCR Теперь посмотрим, как конструировать контракты данных. Начните с создания новой службы WCF, которая будет взаимодействовать с базой данных AutoLot, созданной в главе 21. Эта финальная служба WCF будет создана на основе веб-ориентированного шаблона проекта WCF Service. Вспомните, что служба WCF такого типа автоматически помещается в виртуальный каталог IIS и функционирует подобно традиционной веб-службе XML в .NET. Однажды разобравшись в композиции такой службы WCF, не должно возникать проблем с переносом существующей службы WCF в новый виртуальный каталог IIS. На заметку! В этом примере предполагается, что вы разбираетесь в структуре виртуального каталога IIS (и в самом сервере IIS). Если это не так, обратитесь в главу 32. Использование веб-ориентированного шаблона проекта WCF Service Выбрав пункт меню File^New^Web Site (Файл^Создать^Веб-сайт), создайте новую службу WCF по имени AutoLotWCFService, представленную следующим URI: http:// localhost/AutoLotWCFService (рис. 25.19). Убедитесь, что в раскрывающемся списке Web location (Веб-расположение) выбран элемент HTTP. ^222222НИИНН Installed Templates Visual Basic Visual С* 1 1 1 .NET Framework. 4 - Sort by. Default 4k ASP.NET Web Service ty|j| Empty Web Site *4| Silverlight 1Л Web Site cjjjjl WCFSerwee N «^ |.«K7.5mmtJ Jjci] ASP.NET Reports ЯаГЯГ"*""' Я||г Dynamic Data Lmq to SQL Web Site Фг Dynamic Data Entities Web Site ' http localhost/AutoLotWCFServ.ee Visual C# Visual C# Visual C* Visual C# Visual C# Visual C* ' ' Type: Visual C# A Web site for creating WCF se - .„,„„.., • ■EZI p Cancel | Рис. 25.19. Создание веб-ориентированной службы WCF
Глава 25. Введение в Windows Communication Foundation 957 После этого установите ссылку на сборку AutoLotDAL.dll, созданную в главе 21 (через пункт меню Website^Add Reference (Веб-сайт1^Добавить ссылку)). Будет предоставлен некоторый начальный код (расположенный в папке AddCode), который необходимо удалить. Первым делом, переименуйте исходный файл IService.cs на IAutoLotService.cs и внутри переименованного файла определите начальный контракт службы: [ServiceContract] public interface IAutoLotService { [ Oper at юпСоп tract] void InsertCar (int id, string make, string color, string petname); [OperationContract] void InsertCar(InventoryRecord car) ; [OperationContract] InventoryRecord[] Getlnventory(); } В этом интерфейсе определены три метода, один из которых возвращает массив объектов (еще не созданного) типа InventoryRecord. Вы можете вспомнить, что метод Getlnventory () класса InventoryDAL просто возвращает объект DataTable, из-за чего возникает вопрос: почему бы методу Getlnventory () создаваемой службы не делать то же самое? Хотя и можно было бы вернуть DataTable из метода службы WCF, вспомните, что технология WCF обязана следовать принципам SOA, один из которых — программирование на основе контрактов, а не реализаций. Поэтому вместо возврата внешнему клиенту специфичного для .NET типа DataTable, мы вернем специальный контракт данных (InventoryRecord), который будет корректно выражен в документе WSDL в независимой манере. Также обратите внимание, что в показанном ранее интерфейсе определен перегруженный метод по имени InsertCar(). Первая версия принимает четыре входных параметра, а вторая — один параметр типа InventoryRecord. Контракт данных InventoryRecord может быть определен следующим образом: [DataContract] public class InventoryRecord { [DataMember] public int ID; [DataMember] public string Make; [DataMember] public string Color; [DataMember] public string PetName; } Если реализовать интерфейс в таком виде, построить хост и попытаться вызвать эти методы на стороне клиента, возникнет исключение времени выполнения. Причина в том, что одно из требований описания WSDL состоит в то, что каждый метод, представленный данной конечной точкой, должен быть уникально именован. Таким образом, хотя перегрузка методов замечательно работает в контексте языка С#, современные спецификации веб-служб не допускают существования двух методов с одним и тем же именем InsertCar ().
958 Часть V. Введение в библиотеки базовых классов .NET К счастью, атрибут [OperationContract] поддерживает свойство Name, которое позволяет указать, как метод С# будет представлен в описании WSDL. С учетом этого, обновите вторую версию InsertCarO следующим образом: public interface IAutoLotService { [OperationContract(Nairn* = "InsertCarWithDetails")] void InsertCar(InventoryRecord car) ; } Реализация контракта службы Теперь переименуйте Service.cs на AutoLotService.cs. Тип AutoLotService реализует этот интерфейс, как показано ниже (не забудьте импортировать пространства имен AutoLotConnectedLayer и System.Data в этот файл кода): using AutoLotConnectedLayer; using System.Data; public class AutoLotService : IAutoLotService { private const string ConnString = @"Data Source=(local)\SQLEXPRESS;Initial Catalog=AutoLot"+ ";Integrated Secunty=True"; public void InsertCar(int id, string make, string color, string petname) { InventoryDAL d = new InventoryDAL(); d.OpenConnection(ConnString); d.InsertAuto(id, color, make, petname); d.CloseConnection (); } public void InsertCar(InventoryRecord car) { InventoryDAL d = new InventoryDAL(); d.OpenConnection(ConnString); d.InsertAuto(car.ID, car.Color, car.Make, car.PetName); d.CloseConnection (); } public InventoryRecord[] Getlnventory() { // Сначала получить DataTable из базы данных. InventoryDAL d = new InventoryDAL(); d.OpenConnection(ConnString) ; DataTable dt = d.GetAHInventoryAsDataTable () ; d.CloseConnection (); » // Теперь создать List<T> для хранения полученных данных. List<InventoryRecord> records = new List<InventoryRecord>(); // Скопировать таблицу данных в Listo специальных контрактов. DataTableReader reader = dt.CreateDataReader(); while (reader.Read()) { InventoryRecord r = new InventoryRecord(); r.ID= (int)reader["CarlD"]; r.Color = ((string)reader["Color"]); r.Make = ((string)reader["Make"]); r.PetName = ( (string)reader["PetName"]); records.Add(r);
Глава 25. Введение в Windows Communication Foundation 959 // Трансформировать List<T> в массив элементов типа InventoryRecord. return (InventoryRecord[])records.ToArray(); } I Добавить здесь особо нечего. Для простоты мы жестко закодировали значение строки соединения (которую можно скорректировать согласно существующим настройкам) вместо сохранения ее в файле Web. con fig. Учитывая, что библиотека доступа к данным выполняет всю реальную работу по взаимодействию с базой данных AutoLot, все, что потребуется сделать — это передать входные параметры методу InsertAutoO класса InventoryDAL. Единственное, что здесь представляет интерес — это отображение значений объекта DataTable на обобщенный список элементов типа InventoryRecord (с использованием DataTableReader) с последующей трансформацией List<T> в массив объектов типа InventoryRecord. Роль файла *.svc При создании веб-ориентированной службы WCF обнаруживается, что проект содержит специфичный файл с расширением *.svc. Этот конкретный файл необходим любой службе WCF, развернутой в IIS; в нем описано имя и местоположение реализации службы внутри точки установки. Поскольку имена начального файла и типов WCF были изменены, потребуется модифицировать содержимое файла Service.cs: <ь@ ServiceHost Language="C#" Debug="true" Service="AutoLotService" CodeBehind="~/App_Code/AutoLotService.cs" .> На заметку! Среда .NET 4.0 позволяет развертывать службу WCF в виртуальном каталоге I IS без файла *.svc. Однако при этом служба должна представлять собой не более чем коллекцию файлов кода С#. Служба также будет выглядеть очень похожей на традиционную веб-службу XML из ASP.NET. За дополнительными сведениями обращайтесь в раздел "What's new in Windows Communication Foundation" ("Что нового в Windows Communication Foundation") документации .NET Framework 4.0 SDK. Содержимое файла Web.conf ig Файл Web.config службы WCF, созданной под HTTP, использует ряд упрощений .NET 4.0, рассмотренных ранее в этой главе. Как будет подробно описано далее в этой книге, при рассмотрении ASP.NET, файл Web.config служит той же цели, что и файлы *.conf ig исполняемых программ; однако он также управляет рядом специфических веб-настроек. Например, обратите внимание, что механизм МЕХ включен, и не нужно указывать специальный элемент <endpoint> вручную: <configuration> <system.web> <compilation debug="false" targetFramework=.0" /> </system.web> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior> <i__ Чтобы избежать раскрытия информации метаданных, установите следующее значение в false и удалите конечную точку метаданных перед развертыванием --> <serviceMetadata httpGetEnabled="true"Л> <'-- Для получения деталей исключений при сбоях б цепч:. отпадт установите следующее значение в true.
960 Часть V. Введение в библиотеки базовых классов .NET Перед развертыванием установите его снова в false, чтобы скрыть информацию об исключении --> <serviceDebug includeExceptionDetailInFaults="false"/> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> <system.webServer> <modules runAlIManagedModulesForAllRequests="true"/> </system.webserver> </configuration> Тестирование службы Теперь можно построить клиент любого рода для тестирования службы, включая передачу конечной точки файла *.svc в приложение WcfTestClient.exe: WcfTestClient http://localhost/AutoLotWCFServiсе/Service.svc Чтобы создать специальное клиентское приложение, воспользуйтесь диалоговым окном Add Service Reference (Добавить ссылку на службу), как это делалось ранее в главе, в примерах проектов MagicEightBallServiceClient и MathClient. Исходный код. Проект AutoLotService доступен в подкаталоге Chapter 25. На этом рассмотрение API-интерфейса Windows Communication Foundation завершено. Конечно, эта тема слишком обширна, чтобы раскрыть ее полностью в одной ознакомительной главе, однако, разобравшись с изложенным здесь материалом, можно продолжить изучение WCF самостоятельно. Исчерпывающие сведения о WCF приведены в документации .NET Framework 4.0 SDK. Резюме В этой главе вы ознакомились с API-интерфейсом Windows Communication Foundation (WCF), который является частью библиотеки базовых классов, начиная с версии .NET 3.0. Как объяснялось, основная мотивация, лежащая в основе WCF, состоит в предоставлении унифицированной объектной модели, которая представляет ряд (ранее несвязанных) API-интерфейсов распределенных вычислений, собранных воедино. В начале главы было показано, что служба WCF представляется за счет указания адресов, привязок и контрактов (обозначаемых аббревиатурой ABC). Как вы видели, типичное приложение WCF включает использование трех взаимосвязанных сборок. Первая из них определяет контракты службы и типы службы, которые представляют ее функциональность. Эта сборка развертывается в отдельной исполняемой программе-хосте, виртуальном каталоге IIS либо в службе Windows. И, наконец, клиентская сборка использует сгенерированный файл кода, определяющий тип прокси (и настройки в конфигурационном файле приложения) для взаимодействия с удаленным типом. В главе также рассматривалось применение ряда инструментов программирования WCF, таких как SvcConfigEditor.exe (который позволяет модифицировать содержимое файлов *.config), приложение WcfTestClient.exe (для быстрого тестирования служб WCF) и различные шаблоны проектов Visual Studio 2010. Кроме того, было описано множество упрощений WCF 4.0, включая конечные точки и поведение по умолчанию.
ГЛАВА 26 Введение в Windows Workflow Foundation 4.0 Несколько лет назад в составе версии .NET 3.0 появился API-интерфейс под названием Windows Workflow Foundation (WF). Этот API-интерфейс позволяет моделировать, конфигурировать, наблюдать и выполнять рабочие потоки (которые служат для моделирования бизнес-процессов), используемые внутри конкретного приложения .NET. Готовое решение, предлагаемое WF, дает огромные преимущества при построении программного обеспечения, поскольку отпадает необходимость в ручной разработке сложной инфраструктуры для поддержки приложений рабочих потоков. Будучи новой технологией, API-интерфейс WF в своем первом выпуске имел ряд шероховатостей. Многие разработчики считали, что его среде проектирования, предоставленной в Visual Studio 2008, недоставало блеска, а навигация по сложным рабочим потокам во время разработки была крайне запутанной. К тому же начальный выпуск WF требовал написания громоздкого кода для создания и запуска рабочего потока. При этом даже построение самого рабочего потока было несколько неуклюжим, учитывая тот факт, что кодовая база С# и связанное с рабочим потоком представление в визуальном конструкторе плохо сочетались друг с другом. В .NET 4.0 произошла полная переделка всего API-интерфейса WF. Теперь рабочие потоки моделируются (по умолчанию) с помощью декларативной, основанной на XML грамматики XAML, где данные, используемые рабочим потоком, являются "почетными гражданами". Кроме того, визуальные конструкторы WF в Visual Studio были полностью переписаны с применением технологий Windows Presentation Foundation (WPF). Поэтому если вы использовали предьщушую версию API-интерфейса WF и были ею недовольны, настоятельно рекомендуется читать дальше. Для новичков в мире WF глава начинается с определения роли бизнес-процессов и описания их связи с API-интерфейсом WF. Кроме того, будет представлена концепция действия (activity) WF, две основных разновидности рабочих потоков в WF 4.0 (блок-схема и последовательный поток), а также различными шаблонами проектов и инструментами программирования. После ознакомления с основами будет построено несколько примеров программ, которые проиллюстрируют использование программной модели WF для установки бизнес-процессов, которые выполняются под неусыпным надзором исполняющей среды WF. На заметку! Полное описание WF 4.0 невозможно уместить в единственную ознакомительную главу. Более глубокое изложение дается в книге Pro WF: Windows Workflow in .NET 4.0 (Apress, 2010 г.).
962 Часть V. Введение в библиотеки базовых классов .NET Определение бизнес-процесса Любое реальное приложение должно иметь возможность моделировать различные бизнес-процессы. Бизнес-процесс (business process) — это концептуально объединенная группа задач, которая логически работает как единое целое. Например, предположим, что строится приложение, позволяющее приобретать автомобили через Интернет Как только пользователь отправил заказ, приводится в действие большое количество процессов. Начать можно с проверки кредитоспособности. Если пользователь прошел такую проверку, можно запустить транзакцию базы данных, чтобы исключить элемент из таблицы Inventory (склад), добавить новый элемент в таблицу Orders (заказы) и обновить информацию о счете заказчика. После завершения транзакции базы данных может понадобиться отправить покупателю электронное письмо с подтверждением, а затем вызвать удаленную веб-службу, чтобы передать заказ дилеру. Все вместе эти задачи представляют единый бизнес-процесс. Моделирование бизнес-процессов — это еще одна деталь, которую должны принимать во внимание программисты, часто посредством написания специального кода, который гарантирует не только правильное моделирование бизнес-процесса, но также корректно выполняется внутри самого приложения. Например, может потребоваться написать код, учитывающий возможные сбои, выполняющий трассировку и протоколирование (чтобы видеть текущее состояние бизнес-процесса), поддерживающий постоянство (для сохранения состояния длительно выполняющегося процесса) и т.д. Понятно, что создание такой инфраструктуры с нуля потребует массы времени и ручной работы. Если команда разработчиков фактически построит платформу бизнес-процесса для своих приложений, то на этом их работа не завершится. Просто говоря, "сырая" кодовая база С# не может быть легко объяснена членам команды — непрограммистам, которые также нуждаются в понимании бизнес-процесса. Суровая правда состоит в том, что эксперты предметной области, менеджеры, продавцы и члены команды графического дизайна часто не говорят на языке кода. Учитывая это, программистам понадобятся инструменты моделирования (вроде Microsoft Visio, белой доски и т.п.), чтобы графически представить процесс в общепонятной терминологии. Очевидная проблема, возникающая здесь, состоит в том, что придется постоянно поддерживать в согласованном состоянии две сущности. Если изменяется код, то нужно обновить диаграмму. Если меняется диаграмма, то придется обновить код. Более того, при построении изощренных приложений, использующих стопроцентный кодовый подход, кодовая база очень слабо представляет внутренний "поток" приложения. Например, типичная программа .NET может состоять из сотен специальных типов (не говоря уже о многочисленных типах из библиотек базовых классов). Хотя программисты могут представлять себе, какие объекты вызывают другие объекты, сам код далеко не в состоянии заменить живую документацию, которая объяснит последовательность действий простым человеческим языком. Хотя команда разработчиков может разрабатывать внешнюю документацию и графики рабочих потоков, мы опять упираемся в проблему нескольких представлений одного и того же процесса. Роль WF 4.0 По существу, API-интерфейс Windows Workflow Foundation 4.0 позволяет программистам декларативно проектировать бизнес-процессы, используя лредварительно разработанный набор действий. Поэтому вместо использования только специального множества сборок для представления определенных бизнес-действий и всей необходимой инфраструктуры можно применять визуальные конструкторы WF среды Visual Studio 2010 для создания бизнес-процессов на этапе проектирования.
Глава 26. Введение в Windows Workflow Foundation 4.0 963 Благодаря этому, WF позволяет построить скелет бизнес-процесса, который затем может быть наполнен конкретным кодом, где это необходимо. При программировании с применением API-интерфейса WF для представления всего бизнес-процесса, а также кода, определяющего его, может использоваться единственная сущность. В дополнение к дружественному визуальному представлению этого процесса применяется единственный документ WF, и больше не надо беспокоиться о возможном рассогласовании нескольких документов. Еще лучше то, что документ WF наглядно иллюстрирует сам процесс. С минимальной помощью специалиста даже представители не технического персонала быстро понимают то, что моделирует визуальный конструктор WF. Построение простого рабочего потока При построении приложений на основе рабочих потоков вы, несомненно, отметите, что процесс выглядит совершенно иначе, чем для типичного приложения .NET. Например, вплоть до этого момента каждый пример кода начинался с создания нового рабочего пространства проекта (чаще всего проекта Console Application) и предусматривал написание кода для представления программы в целом. Приложение WF также состоит из специального кода; однако, вдобавок к этому, производится встраивание непосредственно в сборку модели самого бизнес-процесса. Еще один аспект WF, который существенно отличается от других видов приложений .NET, заключается в том, что подавляющее большинство рабочих потоков будут моделироваться в декларативной манере, с использованием грамматики на основе XML, называемой XAML. По большей части не придется непосредственно писать код разметки, поскольку IDE-среда Visual Studio 2010 сделает это автоматически, в процессе работы с инструментами проектирования WE В этом состоит значительное отличие от предыдущей версии API-интерфейса WF, где в качестве стандартного средства моделирования рабочего потока применялся код С#. На заметку! Имейте в виду, что диалект XAML, используемый в WF, не идентичен диалекту XAML, применяемому для WPR Вы ознакомитесь с синтаксисом и семантикой XAML для WPF в главе 27, поскольку в отличие от WF для XAML, там довольно часто приходится непосредственно редактировать сгенерированный визуальным конструктором XAML-код. Чтобы приступить к работе с рабочими потоками, откройте Visual Studio 2010. В диалоговом окне New Project (Новый проект) выберите проект Workflow Console Application (Консольное приложение рабочего потока) и назначьте ему имя Fir stWorkf low ExampleApp (рис. 26.1). Теперь взгляните на рис. 26.2, на котором показана начальная диаграмма рабочего потока, сгенерированная Visual Studio 2010. Как видите, здесь мало что происходит, помимо появления в визуальном конструкторе сообщения, предлагающего поместить действия на поверхность проектирования. Для этого простого тестового рабочего потока откройте панель инструментов Visual Studio 2010 и найдите действие WriteLine в разделе Primitives (Примитивы), как показано на рис. 26.3. Найдя это действие, перетащите его на область с надписью Drop activity here (Поместите сюда действие) и в поле редактирования Text введите сообщение в двойных кавычках. На рис. 26.4 показан один возможный рабочий поток.
Часть V. Введение в библиотеки базовых классов .NET i Visual C# Windows Web Office Cloud Repotting SharePcmt Sitverlight Test WCF Other Languages j.NET Framework4 • № i Activity Designer Library Visual C# C^0 Activity Library Visual C* (АЛ WCF Workflow Service Appli... Visual C# Ш FirstWorkflowExampleApp C:\MyCode I FrrsflrVorkflow£xatnp!eApp П Type: Visual C* A blank Workflow Console Application Create directory for solu Add to source control Рис. 26.1. Создание нового консольного приложения рабочего потока Workflowljtaml x| Workflowl Expand Ail Collapse Ail Drop activity here Рис. 26.2. Визуальный конструктор рабочего потока — это контейнер для действий, моделирующих бизнес-процесс Toolbox l> Control Flow с> Flowchart ' Messaging > Runtime л Primitives It Pointer Ar-fJ Assign ■ф Delay Щ InvokeMethod Щ WriteUne > Transaction :> Collection > Error Handling л General V WriteLine Version 4.0,0.0 from Microsoft Corporation Managed ,NET Component Writes text to a TextWrrter There are no usable controls in this group. Drag an item onto this text to add it to the toolbox. Рис. 26.3. Панель инструментов отображает все действия по умолчанию, доступные в WF 4.0
Глава 26. Введение в Windows Workflow Foundation 4.0 965 Workflowlxaml* X| Worfcflowl m Expand All Collapse All ГЛ Л nteLine Text "First Workflow!" Рис. 26.4. Действие WriteLine отображает текст в TextWriter, в данном случае — на консоли Просмотр полученного кода XAML Закройте визуальный конструктор рабочего потока, щелкните правой кнопкой мыши на Workflow]..xaml в Solution Explorer и выберите в контекстном меню пункт View code (Просмотреть код). Отобразится лежащее в основе XAML-представление рабочего потока. Как видите, корневым узлом этого XML-документа является <Activity>. В открывающем объявлении корневого элемента имеется огромное количество определений пространств имен XML, и почти в каждом будет присутствовать конструкция clr-namespace. Как будет объясняться более подробно в главе 27, когда файл XAML нуждается в ссылке на определения типов .NET, содержащихся во внешних сборках (или в другом пространстве имен текущей сборки), необходимо строить отображение .NET на XAML, используя конструкцию clr-namespace. <Activity mc : Ignorable=llsapM x:Class = "FirstWorkf lowExampleApp . Workf lowl" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns :mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns : sl = "clr-namespace : System; assembly=System11 xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=Systern.Core" xmlns:sa="clr-namespace:System.Activities;assembly=Systern.Activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=Systern.Activities" xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scgl="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=Systern.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=Systern.Core" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <x:Members> <x:Property Name="MessageToShow" Type="InArgument(x:String)" /> </x:Members> <sap:VirtuallzedContainerService.HintSize> 251,240 </sap:VirtuallzedContainerService.HintSize>
966 Часть V. Введение в библиотеки базовых классов .NET <mva:VisualBasic.Settings> Assembly references and imported namespaces for internal implementation </mva:VisualBasic.Settings> <WriteLine sadrXamlDebuggerXmlReader.FileName= "C:\My Code\ FirstWorkflowExampleApp\Workflowl.xaml" sap:VirtualizedContainerService.HintSize=11,200" Text="[MessageToShow]" /> </Activity> Элемент <Activity> — это контейнер для всех задач, представляющих рабочий поток. Здесь имеется только одно поддействие <WriteLine>. Обратите внимание, что атрибут Text установлен на основе данных, введенных в визуальном конструкторе рабочего потока. Теперь запомните, что при построении рабочих потоков с использованием Visual Studio 2010 обычно не придется вручную модифицировать разметку. Используя этот визуальный конструктор (и различные связанные с WF инструменты, интегрированные в Visual Studio 2010), можно моделировать процесс, а разметка будет сгенерирована IDE-средой. По этой причине в настоящей главе мы не углубляемся в детали XAML для WF. Однако знайте, что можно просматривать разметку, сгенерированную IDE-средой при перетаскивании действий на поверхность визуального конструктора. В любом случае разметка, находящаяся в файле XAML, всегда отображается на "реальные" типы в сборке .NET. Например, каждый элемент в файле XAML (такой как <Activity>) представляет собой декларативное определение объекта .NET (данном случае System.Activities.Activity). Атрибуты, которые появляются в открывающем определении элемента, отображаются на свойства или события в связанном типе класса. Во время выполнения разметка воплощается в объектной модели времени выполнения, где каждое описание элемента будет использовано для установки состояния связанного объекта .NET. Возможность определять структуру рабочего потока в декларативной манере с использованием XAML предоставляет массу преимуществ, самое заметное из которых — инструментальная поддержка. Например, инструменты проектирования из Visual Studio 2010 можно включить в специальное приложение с графическим интерфейсом. После этого можно построить простой инструмент, предназначенный не программистам из команды, для создания бизнес-процессов, реализацией которых займутся программисты. Результат может быть сохранен в виде файла *.xaml и импортирован в разрабатываемые проекты .NET. Другое замечательное преимущество использования разметки для определения рабочего потока состоит в том, что становится возможной загрузка внешних файлов XAML в память на лету, позволяя менять работу бизнес-процессов. Например, можно написать код, который читает файл *.xaml на лету и заполняет связанную объектную модель. Поскольку логика рабочего потока не кодируется жестко в сборку, изменение функциональности бизнес-процесса может заключаться в простом изменении разметки и перезапуске приложения. Следует понимать, что WF является чем-то большим, нежели просто симпатичным визуальным конструктором, который позволяет моделировать действия бизнес-процесса. При построении диаграммы WF разметка может быть всегда расширена для представления поведения процесса во время выполнения. Фактически, при желании можно вообще избегать использования XAML и писать рабочие потоки исключительно на С#. Однако в этом случае мы вернемся к той же основополагающей проблеме: код, который не понятен нетехническому персоналу. В результате запуска приложения в окне консоли отобразится заданное сообщение: First Workflow1 Press any key to continue . . .
Глава 26. Введение в Windows Workflow Foundation 4.0 967 Выглядит неплохо, но пока непонятно, что же запустило этот рабочий поток? И как можно гарантировать, что консольное приложение останется в рабочем состоянии достаточно долго, чтобы рабочий поток мог быть завершен? Ответы на эти вопросы требуют понимания исполняющего механизма рабочего потока. Исполняющая среда WF 4.0 Следующее, что нужно понять — API-интерфейс WF также состоит из исполняющего механизма, который загружает, выполняет, выгружает и иными способами манипулирует процессом рабочего потока. Исполняющий механизм WF может быть развернут внутри любого из доменов приложений .NET; однако имейте в виду, что отдельный домен приложения может иметь только один работающий экземпляр механизма WF. Вспомните из главы 16, что домен приложения (AppDomain) — это раздел внутри процесса Windows, который играет роль хоста для приложения .NET и любых внешних библиотек кода. Как таковой, механизм WF может быть встроен в простую консольную программу, настольное приложение с графическим интерфейсом (Windows Forms или Windows Presentation Foundation (WPF)) или же представлен службой Windows Communication Foundation (WCF). На заметку! Шаблон проекта WCF Workflow Service Application — замечательная начальная точка, если необходимо построить службу WCF (см. главу 25), которая внутренне использует рабочие потоки. При моделировании бизнес-процесса, который должен использоваться широким разнообразием систем, также есть возможность написания документа WF внутри проекта С# Class Library Таким образом, новые приложения смогут просто ссылаться на библиотеку *.dll, чтобы повторно использовать заранее предопределенную коллекцию бизнес-процессов. Это очевидно полезно, когда не хочется многократно пересоздавать одни и те же рабочие потоки. Хостинг рабочего потока с использованием класса Workf lowlnvoker Хост-процесс исполняющей среды WF может взаимодействовать с упомянутой исполняющей средой посредством различных приемов. Простейший способ предусматривает использование класса Workf lowlnvoker из пространства имен System.Activities. Этот класс позволяет запустить рабочий поток с помощью всего одной строки кода. Открыв файл Program.cs текущего проекта Workflow Console Application, вы увидите следующий метод Main (): static void Main(string [ ] args) { Workflowlnvoker.Invoke(new Workflowl () ) ; } Использование Workf lowlnvoker очень удобно, когда нужно просто запустить рабочий поток и не следить за ним далее. Метод Invoke () будет выполнять рабочий поток в синхронизированном блокирующем режиме. Вызывающий поток блокируется до тех пор, пока весь рабочий поток не завершится либо не будет прерван принудительно. Поскольку метод Invoke () — синхронный вызов, гарантируется, что весь рабочий поток на самом деле будет завершен до окончания Main(). Фактически, если добавить какой-нибудь код после вызова Workf lowlnvoker. Invoke (), он будет выполнен, когда рабочий поток завершится (или в худшем случае будет принудительно прерван):
968 Часть V. Введение в библиотеки базовых классов .NET static void Main(string [ ] args) { Workflowlnvoker.Invoke (new Workflowl()); Console.WriteLine("Thanks for playing"); } Передача аргументов рабочему потоку с использованием класса Workf lowlnvoker Когда хост-процесс запускает рабочий поток, ему очень часто нужно передать специальные стартовые аргументы. Например, предположим, что пользователю программы необходимо дать возможность указывать сообщение, которое нужно отобразить в действии WriteLine вместо жестко закодированного текстового сообщения. В нормальном коде С# можно было бы создать специальный конструктор класса для приема таких аргументов. Однако рабочий поток всегда создается с использованием конструктора по умолчанию! Более того, большинство рабочих потоков определены только с помощью XAML, а не процедурного кода. Оказывается, метод Invoke () имеет несколько перегрузок, одна из которых позволяет передавать аргументы рабочему протоку при запуске. Эти аргументы представлены с использованием переменной Dictionary<string, object>, содержащей набор пар "имя/значение", который будет применяться для установки идентично именованных (и типизированных) переменных-аргументов в самом рабочем потоке. Определение аргументов с использованием визуального конструктора рабочих потоков Для определения аргументов, которые получат данные входящего словаря, воспользуемся визуальным конструктором рабочих потоков. В окне Solution Explorer щелкните правой кнопкой мыши на Workf lowl. xaml и выберите в контекстном меню пункт View Designer (Просмотреть визуальный конструктор). Обратите внимание на кнопку Arguments (Агрументы) в нижней части визуального конструктора. Щелкните на ней и в открывшемся диалоговом окне добавьте входной аргумент типа String по имени MessageToShow (присваивать значение по умолчанию этому новому аргументу не нужно). Удалите начальное сообщение из действия WriteLine. На рис. 26.5 показан конечный результат. WorkflowLxamr X Workfiowl Expand AM ColiapseAU Name MessageToShow Create Argument ГА WriteLine Text Enter a VB expression Direction Argument type Default value к in String I Enter a VB expression Arguments I Рис. 26.5. Аргументы рабочего потока могут использоваться для приема аргументов от хоста
Глава 26. Введение в Windows Workflow Foundation 4.0 969 Теперь в свойстве Text действия WriteLine можно просто ввести MessageToShow в качестве вычисляемого выражения. Во время ввода вы заметите помощь со стороны средства IntelliSense (рис. 26.6). Workflowl латГ X Workflowl Expand All Collapse All 1 Name MessageToShow Gwfe Argument Щ WrrteUne Text «e| • *1$ MarlcupExtension Л% MarkupExtensionReturnTypeAttribute **$Math «*JMe j{) Media ;■**$ MemberDefinition ,j* MessageToShow IO Microsoft Common j AH Arguments Рис. 26.6. Использование специального аргумента в качестве ввода для действия После этого имеется вся необходимая инфраструктура, и в метод Main() класса Program можно вносить показанные ниже изменения. Обратите внимание, что для объявления переменной Dictionaryo в файл Program.cs понадобится импортировать пространство имен System.Collections.Generic. static void Main(string[] args) { Console.WriteLine ("***** Welcome to this amazing WF application *****"); // Получить от пользователя данные для передачи рабочему потоку. Console.Write("Please enter the data to pass the workflow: "); string wfData = Console.ReadLine(); // Упаковать данные в словарь. Dictionary<stnng, ob]ect> wfArgs = new Dictionary<stnng, ob]ect> () ; wfArgs.Add("MessageToShow", wfData); // Передать словарь рабочему потоку. Workflowlnvoker.Invoke(new Workflowl(), wfArgs); Console.WriteLine("Thanks for playing"); } Важно отметить, что строковые значения каждого члена переменной Dictionaryo должны быть именованы в соответствие с переменными-аргументами рабочего потока. Запуск модифицированной программы дает вывод, подобный следующему: ***** Welcome to this amazing WF application ***** Please enter the data to pass the workflow: Hello Mr. Workflow! Hello Mr. Workflow! Thanks for playing Press any key to continue . . . Помимо метода Invoke () другими действительно интересными членами Workflowlnvoker являются BeginlnvokeO и EndlnvokeO, которые позволяют запускать рабочий поток во вторичном потоке выполнения, используя шаблон асинхронного
970 Часть V. Введение в библиотеки базовых классов .NET делегата .NET (см. главу 19). Если необходим большая степень контроля над тем, как исполняющая среда WF манипулирует рабочим потоком, можете вместо него использовать класс Workf lowApplication. Хостинг рабочего потока с использованием класса Workf lowApplication Класс Workf lowApplication (вместо Workf lowlnvoker) необходимо использовать, когда требуется сохранять или загружать длительно выполняющийся рабочий поток с использованием служб постоянного хранения WF и получать уведомления о различных событиях, которые происходят на протяжении жизненного цикла экземпляра рабочего потока, работать с "закладками" WF и прочими расширенными средствами. При наличии опыта работы с предыдущей версией API-интерфейса WF использование Workf lowApplication может показаться похожим на работу с классом Workf lowRuntime в .NET 3.5, в том плане, что для запуска экземпляра рабочего потока понадобится вызывать метод Run(). В результате вызова метода Run() новый фоновый поток выполнения извлекается из пула потоков CLR. Таким образом, если не добавить дополнительную поддержку для обеспечения ожидания основного потока завершения вторичного потока, то экземпляр рабочего потока может вообще не получить шанса завершить свою работу! Один из способов обеспечить достаточное время ожидания для завершения работы фонового потока предусматривает применение объекта AutoResetEvent из пространства имен System.Threading (на самом деле именно это и делалось в коде запуска рабочих потоков .NET 3.5). Ниже показаны изменения в текущем примере, которые позволяют теперь использовать Workf lowApplication вместо Workf lowlnvoker. static void Main(string [ ] args) { Console.WriteLine ("***** Welcome to this amazing WF application *****"); // Получить данные от пользователя для передачи рабочему потоку. Console.Write("Please enter the data to pass the workflow: "); string wfData = Console.ReadLine(); // Упаковать данные в словарь. Dictionary<string, object> wfArgs = new Dictionary<string,object>() ; wfArgs.Add("MessageToShow", wfData); // Использовать для информирования первичного потока о необходимости ожидания. AutoResetEvent waitHandle = new AutoResetEvent(false) ; // Передать рабочему потоку. Workf lowApplication app = new Workf lowApplication (new Workflow]. (), wfArgs); // Связать событие с данным объектом app. //О факте завершения уведомить другой поток //и вывести на консоль соответствующее сообщение. app.Completed = (completedArgs) => { waitHandle.Set() ; Console.WriteLine("The workflow is done!"); }; // Запустить рабочий поток. app.Run (); // Подождать, пока не появится уведомление о завершении рабочего потока. waitHandle.WaitOne(); Console.WriteLine("Thanks for playing"); } Вывод будет похожим на вывод предыдущей итерации этого проекта:
Глава 26. Введение в Windows Workflow Foundation 4.0 971 ***** Welcome to this amazing WF application ***** Please enter the data to pass the workflow: Hey again1 Hey again' The workflow is done! Thanks for playing Press any key to continue . . . Преимущество использования класса Workf lowApplication связано с возможностью вмешаться в события (как это было сделано непрямо с использованием свойства Completed) и подключать более сложные службы (постоянного хранения, закладки и т.п.). В данном ознакомительном вступлении в WF 4.0 детали этих служб времени выполнения не рассматриваются. Поведения времени выполнения и службы исполняющей среды Windows Workflow Foundation 4.0 подробно описаны в документации .NET Framework 4.0 SDK. Переделка первого рабочего потока На этом первый взгляд на WF 4.0 завершен. Хотя этот пример тривиален, вы узнали несколько очень интересных и полезных вещей. Было показано, как передать объект Dictionary, содержащий пары "имя/значение", которые затем попадают в виде идентично именованных аргументов в рабочий поток. Это действительно полезно, когда необходимо получить пользовательский ввод (такой как идентификационный номер клиента, его номер карточки социального страхования и т.п.), который будет использоваться рабочим потоком для выполнения действий. Также было продемонстрировано, что рабочий поток .NET 4.0 определяется в декларативной манере (по умолчанию) с использованием основанной на XML грамматики под названием XAML. С помощью XAML можно указывать действия, которые содержит рабочий поток. Во время выполнения эти данные применяются для создания корректной объектной модели в памяти. И последнее: были представлены два разных подхода к запуску рабочего потока с использованием классов Workf lowlnvoker и WorkflowApplication. Исходный код. Проект FirstWorkf lowExampleApp доступен в подкаталоге Chapter 26. Знакомство с действиями Windows Workflow 4.0 Вспомните, что цель WF — позволить моделировать бизнес-процесс в декларативной манере, с последующим его выполнением исполняющей средой WF. На жаргоне WF бизнес-процесс состоит из любого количества действий. Попросту говоря, действие WF — это атомарный "шаг" в общем процессе. При создании нового приложения рабочего потока вы обнаружите, что панель инструментов содержит значки для встроенных действий, сгруппированные по категориям. Эти готовые действия используются для моделирования бизнес-процесса. Каждое действие в панели инструментов отображается на реальный класс в сборке System. Activities.dll (и чаще всего содержится внутри пространства имен System. Activities.Statements). Несколько таких готовых действий будут использоваться на протяжении этой главы. Полную информацию можно найти в документации .NET Framework 4.0 SDK. Действия потока управления Первая категория действий в панели инструментов позволяет представлять задачи организации циклов и принятия решений в более крупном рабочем потоке. Их по ль-
972 Часть V. Введение в библиотеки базовых классов .NET за должна быть очевидной, учитывая, что похожие задачи часто приходится решать в коде С#. Действия потока управления перечислены в табл. 26.1; обратите внимание, что некоторые из них допускают параллельную обработку действий с использованием Task Parallel Library (см. главу 19). Таблица 26.1. Действия потока управления в WF 4.0 Действие Назначение DoWhile Циклическое действие, которое выполняет содержащиеся внутри действия как минимум один раз и повторяет это, пока логическое условие истинно ForEach<T> Выполняет действие по одному разу для каждого значения, представленного в коллекции ForEach<T>.Values If Моделирует условие If-Then-Else Parallel Действие, которое выполняет все дочерние действия одновременно и асинхронно ParallelForEach<T> Перечисляет элементы коллекции и выполняет каждый элемент коллекции параллельно Pick Представляет моделирование потока управления на основе событий PickBranch Потенциальный путь выполнения внутри родительского действия Pick Sequence Выполняет набор дочерних действий последовательно Switch<T> Выбирает одно из возможных действий для выполнения, в зависимости от значения заданного выражения типа, указанного в параметре типа объекта While Выполняет содержащийся элемент рабочего потока, пока условие истинно Действия блок-схемы Рассмотрим действия блок-схемы (flowchart), которые довольно важны, учитывая, что действие Flowchart часто является первым элементом, который помещается на поверхность визуального конструктора рабочих потоков. Концепция рабочего потока в виде блок-схемы появилась в .NET 4.0. Она позволяет строить рабочий поток, используя известную модель, где выполнение рабочего потока основано на множестве ветвящихся путей, выбор каждого из которых зависит от истинности или ложности определенного внутреннего условия. В табл. 26.2 описаны члены этого набора действий. Таблица 26.2. Действия блок-схемы в WF 4.0 Действие Назначение Flowchart Моделирует рабочие потоки, используя известную парадигму блок-схемы. Это — самое первое действие, которое помещается на поверхность визуального конструктора FlowDecision Действие, предоставляющее возможность моделировать условный узел с двумя возможными исходами FlowSwitch<T> Узел, позволяющий моделировать конструкцию переключения, с одним выражением и одним исходом для каждого соответствия
Глава 26. Введение в Windows Workflow Foundation 4.0 973 Действия обмена сообщениями Рабочий поток может легко вызывать члены внешней веб-службы или службы WCF, а также получать уведомления от внешних служб, используя действия обмена сообщениями (messaging activities). Поскольку эти действия очень тесно связаны с разработкой WCF, они собраны в отдельную сборку .NET— System.ServiceModel.Activities.dll. Внутри этой библиотеки имеется пространство имен Activities, в котором определены основные действия, перечисленные в табл. 26.3. Таблица 26.3. Действия обмена сообщениями в WF 4.0 Действие Назначение CorrelationScope Используется для управления дочерними действиями сообщений InitializeCorrelation Инициализирует корреляцию без отправки или приема сообщения Receive Принимает сообщение от службы WCF Send Отправляет сообщение службе WCF SendAndReceiveReply Отправляет сообщение службе WCF и захватывает возвращенное значение TransactedReceiveScope Действие, которое позволяет направить транзакцию в рабочий поток или созданные диспетчером транзакции стороны сервера Наиболее часто используемыми действиями обмена сообщениями являются Send и Receive, которые позволяют взаимодействовать с внешними веб-службами XML или службами WCF. Действия исполняющей среды и действия-примитивы Следующие две категории действий в панели инструментов — Runtime (Исполняющая среда) и Primitives (Примитивы) — позволяют строить рабочий поток, который производит вызовы исполняющей среды WF (в случае Persists и TerminateWorkf low) и выполняет общие операции, такие как помещение текста в выходной поток или вызов метода на объекте .NET. Эти действия описаны в табл. 26.4. Таблица 26.4. Действия исполняющей среды и действия-примитивы в WF 4.0 Действие Назначение Persist Заставляет рабочий поток сохранить свое состояние в базе данных, используя службу постоянства WF TerminateWorkf low Прерывает выполняющийся экземпляр рабочего потока, инициируя на хосте событие Workf lowApplication.Completed, и выдает сообщение об ошибке. После такого прерывания рабочий поток не может быть возобновлен Assign Позволяет устанавливать свойства действия с использованием присваиваемых значений, определенных в визуальном конструкторе рабочего потока Delay Заставляет рабочий поток приостановиться на заданный период времени InvokeMethod Вызывает метод указанного объекта или типа WriteLine Записывает заданную строку в указанный экземпляр типа, унаследованного от TextWriter. По умолчанию это будет стандартный выходной поток (т.е. консоль); однако можно сконфигурировать и другие потоки, такие как FileStream
974 Часть V. Введение в библиотеки базовых классов .NET Пожалуй, наиболее интересным и полезным действием из этого набора является InvokeMethod, потому что оно позволяет вызывать методы классов .NET в декларативной манере. Действие InvokeMethod можно также сконфигурировать для сохранения любого возвращенного значения, которое присылает вызванный метод. Действие TerminateWorkf low может пригодиться, когда нужно рассчитывать на точку невозврата. Если экземпляр рабочего потока добирается до этого действия, он инициирует событие Completed, которое может быть перехвачено на стороне хоста, как это делалось в первом примере. Действия транзакций При построении рабочего потока может понадобиться обеспечить выполнение группы действий в атомарной манере — в том смысле, что они либо все должны выполниться успешно, либо все вместе быть отменены. Даже если соответствующие действия не работают непосредственно с реляционной базой данных, основные действия, представленные в табл. 26.5, позволяют добавлять их в транзакционный контекст. Таблица 26.5. Действия транзакций в WF 4.0 Действие Назначение CancellationScope Ассоциирует логику отмены внутри главного потока выполнения CompensableActivity Любое действие, поддерживающее компенсацию своих дочерних действий TransactionScope Действие, обозначающее границы транзакции Действия над коллекциями и действия обработки ошибок Последние две категории, которые осталось рассмотреть в этой вводной главе, позволяют декларативно манипулировать обобщенными коллекциями и реагировать на исключения времени выполнения. Действия коллекций незаменимы, когда требуется манипулировать объектами, представляющими бизнес-данные (такими как заказы на покупку, объекты медицинской информации или отслеживание заказов) на лету в XAML. Действия ошибок, с другой стороны, позволяют реализовать логику try/catch/throw внутри рабочего потока. Этот последний набор действий WF 4.0 описан в табл. 26.6. Таблица 26.6. Действия над коллекциями и действия обработки ошибок в WF 4.0 Действие Назначение AddToCollection<T> Добавляет элемент к указанной коллекции ClearCollection<T> Очищает указанную коллекцию от всех элементов ExistInCollection<T> Определяет, представлен ли указанный элемент в данной коллекции RemoveFromCollection<T> Удаляет элемент из указанной коллекции Rethrow Инициирует ранее перехваченное исключение из действия Catch Throw Инициирует исключение TryCatch Содержит элементы рабочего потока, которые должны быть выполнены исполняющей средой рабочего потока внутри блока обработки исключений
Глава 26. Введение в Windows Workflow Foundation 4.0 975 Ознакомившись со многими из действий по умолчанию, можно приступать к построению некоторых интересных рабочих потоков, которые их используют По ходу дела вы узнаете о двух ключевых действиях, которые обычно функционируют как корень рабочего потока— Flowchart и Sequence. Построение рабочего потока в виде блок-схемы В первом примере было показано, как перетащить действие WriteLine непосредственно на поверхность визуального конструктора рабочих потоков. Хотя и верно, что любое действие, представленное в панели инструментов Visual Studio 2010, может быть первым элементом, помещенным в визуальный конструктор, только несколько из них способны содержать в себе дочерние поддействия (что, очевидно, очень важно). При построении нового рабочего потока велика вероятность, что первым элементом, который будет помещен на поверхность визуального конструктора, будет действие Flowchart или Sequence. Упомянутые действия обладают способностью содержать любое количество внутренних дочерних действий (включая дополнительные Flowchart или Sequence), представляющих сущность бизнес-процесса. Для начала создайте новое консольное приложение рабочего потока по имени EnumerateMachineDataWF. Затем переименуйте начальный файл *.xaml в MachinelnfoWF.xaml. Из раздела Flowchart (Блок-схема) панели инструментов перетащите на поверхность визуального конструктора действие Flowchart. В окне Properties (Свойства) измените значение свойства DisplayName на что-то более осмысленное, такое как Show Machine Data Flowchart (свойство DisplayName определяет то, как элемент называется в визуальном конструкторе). Теперь поверхность визуального конструктора рабочего потока должна выглядеть примерно так, как на рис. 26.7. MachinelnfoWF-xaml X вд Woricflowl IJEj Show Machine Data Flowchart IIJ ,1..ML 1„LMM— • Start Expand AH Collapse All a а^НКЕкЯ? Рис. 26.7. Начальное действие Flowchart Скоба захвата, которая появляется в нижнем правом углу действия Flowchart (при наведении курсора мыши), позволяет увеличивать и уменьшать размер пространства блок-схемы. Его понадобится увеличить, чтобы добавить внутрь новые действия. Подключение действий к блок-схеме Большой значок Start (Пуск) представляет точку входа в действие Flowchart, которое в данном примере является первым действием всего рабочего потока и будет инициировано при запуске рабочего потока с помощью классов Workf lowlnvoker или Workf lowApplication. Этот значок может располагаться в любом месте визуального
976 Часть V. Введение в библиотеки базовых классов .NET конструктора, но рекомендуется его переместить в левый верхний угол, чтобы освободить побольше места. Цель состоит в том, чтобы собрать блок-схему, соединяя любое количество дополнительных действий вместе, обычно используя в процессе действие FlowDecision. Для начала перетащите действие WriteLine на поверхность визуального конструктора, изменив значение его свойства DisplayName на Greet User. Наведя курсор мыши на значок Start, вы увидите петельки для стыковки с каждой стороны. Щелкните на ближайшей к действию WriteLine петельке для стыковки и перетащите ее к петельке для стыковки действия WriteLine. Между этими двумя элементами появится соединение, которое означает, что первым выполняемым действием рабочего потока будет Greet User. Теперь, подобно первому примеру, добавьте аргумент рабочего потока (через кнопку Arguments (Аргументы)) по имени UserName типа string без какого-либо значения по умолчанию. Чуть позже оно будет передано динамически через специальный объект Dictionaryo. Наконец, установите в качестве значения свойства Text действия WriteLine следующий оператор кода: "Hello" & UserName Что здесь может показаться непривычным? Если вы привыкли писать код С#, то заметите здесь ошибку, потому что для конкатенации строк должен использоваться символ +, а не &. Однако всякий раз, когда встречается код условия в рабочем потоке, должен применяться синтаксис Visual Basic! Причина в том, что API-интерфейс WF 4.0 использует вспомогательную сборку, которая кодировалась для обработки операторов VB, а не С# (странно, но это правда). Добавьте второе действие WriteLine на поверхность визуального конструктора и соедините с предыдущим. На этот раз для свойства Text определите жестко закодированное строковое значение "Do you want me to list all machine drives?" и измените значение свойства DisplayName на Ask User. На рис. 26.8 показано соединение между текущими действиями рабочего потока. ТА Greet User it- Text "Hello" & UserName Start Щ Ask User Text "Do you want me to list all rn Рис. 26.8. Рабочие потоки в виде блок-схемы соединяют действия вместе Работа с действием invokeMethod Поскольку большая часть рабочего потока определяется в декларативной манере с помощью XAML, наверняка будет часто использоваться действие InvokeMethod, которое позволяет вызывать методы реальных объектов в различных точках рабочего потока. Перетащите один из таких элементов на поверхность визуального конструктора, измените значение свойства DisplayName на Get Y or N, и установите соединение между ним и действием Ask User типа WriteLine. Первым свойством для конфигурирования действия InvokeMethod является TargetType. Оно представляет имя класса, определяющего статический член, который требуется вызвать. В раскрывающемся списке свойства TargetType действия InvokeMethod выберите пункт Browse for Types... (Обзор типов...), как показано на рис. 26.9.
Глава 26. Введение в Windows Workflow Foundation 4.0 977 Щ GetYorN TargetType TargetObject MethodName Рис. 26.9. Указание цели для InvokeMethod В открывшемся диалоговом окне выберите класс System.Console из сборки mscorlib.dll (если ввести имя типа в поле Type Name (Имя типа), он будет автоматически найден). После нахождения класса System.Console щелкните на кнопке ОК. В визуальном конструкторе введите ReadLme в качестве значения свойства MethodName действия InvokeMethod. Подобным образом действие InvokeMethod конфигурируется на вызов метода Console.ReadLineO по достижении этого шага рабочего потока. Как известно, Console.ReadLineO возвращает строковое значение, которое содержит символы, введенные с клавиатуры до нажатия клавиши <Enter>; однако нужен какой-то способ получить его. Этим сейчас и займемся. Определение переменных уровня рабочего потока Определение переменной рабочего потока в XAML почти идентично определению аргумента, в том плане, что это можно делать непосредственно в визуальном конструкторе (на этот раз через кнопку Variables (Переменные)). Отличие в том, что аргументы используются для захвата данных, переданных хостом, в то время как переменные просто указывают на данные в рабочем потоке, которые будут использованы для влияния на его поведение времени выполнения. Щелкнув на кнопке Variables визуального конструктора, добавьте новую строковую переменную по имени YesOrNo. Обратите внимание, что при наличии в рабочем потоке нескольких родительских контейнеров (например, Flowchart, который содержит внутри Sequence) можно указать область видимости переменной. В рассматриваемом примере доступен единственный выбор — корневое действие Flowchart. Выберите в визуальном конструкторе действие InvokeMethod и в окне Property (Свойства) установите для свойства Result новую переменную (рис. 26.10). [Properties ' ПХ] 1 Syste m. Act iv ities S tatementsJnvokeMethod h: ii Search: В Misc DisplayName GenericTypeArguments MethodName 1 Parameters Ru nAsynchronousJy TargetObject 1 TargetType Gear GetYorN (Collection) ГЛ| ReadLme (Collection) [3| Щ YesOrNo (Tl a Enter a VB cxpressi [... д (SystemXonsole »l| (null) Рис. 26.10. Полностью сконфигурированное действие InvokeMethod
978 Часть V. Введение в библиотеки базовых классов .NET Теперь, получив фрагмент данных от вызова внешнего метода, можно использовать его для принятия решений во время выполнения внутри блок-схемы, используя действие FlowDecision. Работа с действием FlowDecision Действие FlowDecision служит для выбора одного из двух возможных действий, в зависимости от истинности или ложности булевской переменной либо оператора, возвращающего булевское значение. Перетащите одно из этих действий на поверхность визуального конструктора и соедините с действием InvokeMethod (рис. 26.11). LXj Show Machine Data Flowchart • Щ Greet User T ext Enter a VB exp ness ion ! ijfg GetYorN J , , Щ Ask User TargetType j Systcm.Comote »| Text "Do you want me to list all m« TargetObject Enter a VB expressior MethodName ReadLine b True False Decision Рис. 26.11. Действие FlowDecision позволяет организовать ветвление по двум направлениям На заметку! Если необходимо оформить условие множественного ветвления внутри блок-схемы, используйте действие FlowSwitch<T>. Оно позволяет определить любое количество путей, из которых выбирается один в зависимости от значения определенной переменной рабочего потока. Установите для свойства Condition (в окне Properties) действия FlowDecision следующий оператор кода, который можно ввести непосредственно в редакторе: YesOrNo.ToUpper () = "Y" Здесь производится проверка значения переменной YesOrNo, представленное в верхнем регистре, на предмет равенства "Y". Не забывайте, что здесь используется синтаксис VB, так что должна применяться операция проверки равенства VB (=), а не С# (==). Работа с действием TerminateWorkf low Теперь необходимо построить действия, которые происходят с обеих сторон от действия FlowDecision. На стороне "false" подключитесь к действию WriteLine, выводящему жестко закодированное сообщение, за которым следует действие TerminateWorkf low (рис. 26.12). Строго говоря, использовать действие TerminateWorkf low не обязательно, поскольку рабочий поток и так завершается по завершении ветви "false". Однако с помощью данного действия можно сгенерировать исключение для хоста рабочего потока, информируя его о причине останова. Это исключение может быть сконфигурировано в окне Properties.
Глава 26. Введение в Windows Workflow Foundation 4.0 979 (Ц User Said No Text Xbye..." Decision t ф TerminateWorkflow Рис. 26.12. Ветвь "false" Выберите действие TerminateWorkflow в визуальном конструкторе и в окне Properties щелкните на кнопке с многоточием для свойства Exception. Откроется редактор, который позволит определить исключение, как это бы делалось в коде (рис. 26.13). Рис. 26.13. Конфигурирование исключения для генерации по достижении действия TerminateWorkflow Завершите конфигурирование этого действия, установив для свойства Reason значение "YesOrNo was false". Построение условия "true" Чтобы начать построение условия "true" для действия FlowDecision, подключите действие WriteLine, которое просто отображает жестко закодированную строку, сообщающую о согласии пользователя продолжить работу. Соедините его с новым действием InvokeMethod, которое вызовет метод GetLogicalDrives () класса System. Environment. Для этого установите свойство TargetType в System.Environment, a свойство MethodName — в GetLogicalDrives. Добавьте новую переменную уровня рабочего потока (щелкнув на кнопке Variables в визуальном конструкторе рабочих потоков) по имени DriveNames типа string[]. Для указания массива строк выберите пункт Array of [T] в раскрывающемся списке Variable Туре и укажите string в открывшемся диалоговом окне. На рис. 26.14 показана первая часть условия "true". Теперь установите переменную DriveHames в качестве значения свойства Result этого нового действия InvokeMethod. Работа с действием ForEach<T> Следующая часть рабочего потока будет выводить имена всех дисков в окне консоли, что подразумевает необходимость ъ циклическом проходе по данным, представленным в переменной DriveNames, которая была сконфигурирована как массив объектов string. Действие ForEach<T> — это эквивалент WF XAML ключевого слова f oreach в С#, и настраивается это действие похожим образом (во всяком случае, концептуально).
980 Часть V. Введение в библиотеки базовых классов .NET Щ User Said Yes! Text "Wonderful!" ■4 True Decision iQ InvokeMethod TargetType J System.Environm< TargetObject Enter a VB expressic MethodName GefcLogicalDrives Рис. 26.14. Конфигурирование исключения для генерации по достижении действия TerminateWorkf low Перетащите одно из этих действий на поверхность визуального конструктора и соедините с предыдущим действием InvokeMethod. Чуть позже вы сконфигурируете действие ForEach<T>, а пока, чтобы завершить ветвь "true", поместите еще одно действие WriteLine на поверхность визуального конструктора. На рис. 26.15 показано, как теперь должен выглядеть рабочий поток. X Show Machine Data Flowchart ^Ш^ ГА Greet User ^^ Text "Hello" & UserName Start T Щ Ask User Text Do you want me to list P User Said Vest <* True Text "Wonderful!" ..? S^f InvokeMethod TargetType j System.Environm* ▼ J TargetObject Enter a VB express tor MethodName GetLogicalDrives all m i Decision Щ GetYorN TargetType jSystem.Console * | TargetObject Enter a VB expresstor MethodName ReadLine rj User Said No Fake ► Text "K,bye..." f Q TerminateWorkflow .!£J ForEach<String> ОшЫе-d f Щ WriteLine ck to view Text "Thanks for using this workfk lh. Рис. 26.15. Завершенный рабочий поток высшего уровня Чтобы избавиться от присутствующей в визуальном конструкторе ошибки, понадобится завершить конфигурирование действия ForEach<T>. Сначала в окне Properties укажите параметр типа для обобщения, которым в данном примере будет string.
Глава 26. Введение в Windows Workflow Foundation 4.0 981 Свойство Values — это место, откуда берутся данные, и в данном случае это переменная DriveNames (рис. 26.16). ■ Properties ISystem.Activrties.Statements.For£ach<System5tring> [j|~]ii Search: IB Misc DispfayName TypeArgument Values ForEach<String> [String DriveNames - П x] Clear » 1 У Рис. 26.16. Установка типа для перечисления Это конкретное действие нуждается в дополнительном редактировании, для чего нужно дважды щелкнуть в визуальном конструкторе, чтобы открыть новый визуальный мини-конструктор, предназначенный только для данного действия. Не все действия WF 4.0 поддерживают двойной щелчок с открытием визуального мини-конструктора, но это несложно определить по самому действию (оно сообщает "Double-click to view" ("Дважды щелкните для просмотра")). Выполните двойной щелчок на действии ForEach<T> и добавьте одиночное свойство WriteLine, которое выведет все значения s-tring (рис. 26.17). jfl ForEach<String> Foreach item in Body fA WriteLine Text «tern DriveNames Рис. 26.17. Завершающий шаг конфигурирования действия ForEach<T> На заметку! В визуальном мини-конструкторе ForEach<T> можно добавлять любое нужное количество действий. Все они будут выполняться на каждой итерации цикла. Покончив с этим, воспользуйтесь ссылками в верхнем левом углу визуального конструктора, чтобы вернуться на верхний уровень рабочего потока (такие навигационные цепочки часто используются при углублении в набор действий). Завершение приложения Пример почти готов. Все, что осталось сделать — это обновить метод Main() класса Program, добавив перехват исключения, которое будет инициировано, если пользователь ответит "N" и тем самым вызовет генерацию объекта Exception. Измените код, как показано ниже (не забыв импортировать в файл кода пространство имен System. Collect ions. Generic):
982 Часть V. Введение в библиотеки базовых классов .NET static void Main(string [ ] args) { try { Dictionary<stnng, ob]ect> wfArgs = new Dictionary<stnng, ob]ect>(); wfArgs.Add("UserName", "Andrew"); Workf lowlnvoker. Invoke (new Workflow]. () , wfArgs); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.Data["Reason"]); } } \ Обратите внимание, что причина ("Reason") исключения может быть получена из свойства Data класса System. Except ion. Если запустить программу и ввести "Y" в ответ на вопрос о перечислении дисков, то появится следующий вывод: Hello Andrew Do you want me to list all machine drives? У Wonderful! C:\ D:\ E:\ F:\ G:\ H:\ I:\ Thanks for using this workflow Однако если ввести "N" (или любое другое значение, отличное от "Y" или "у"), вывод будет таким: Hello Andrew Do you want me to list all machine drives? n K, bye. . . YesOrNo was false Промежуточные итоги У новичков в работе со средой рабочих потоков может возникнуть вопрос: в чем состоит преимущество кодирования столь простого процесса с использованием WF XAML по сравнению с кодом С#? В конце концов, можно было бы вообще обойтись без Windows Workflow Foundation и просто написать следующий класс С#: class Program { static void Main(string [ ] args) { try { ExecuteBusinessProcess (); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.Data["Reason"]); } }
Глава 26. Введение в Windows Workflow Foundation 4.0 983 private static void ExecuteBut me ^Process () { string UserName = "Andrew"; Console.WriteLine("Hello {0}", UserName); Console.WriteLine("Do you want me to list all machine drives?"); string YesOrNo = Console.ReadLine () ; if (YesOrNo. ToUpperO == "Y") { Console.WriteLine("Wonderful'"); string[] DriveNames = Environment.GetLogicalDrives(); foreach (string item in DriveNames) { Console*.WriteLine (item) ; } Console.WriteLine("Thanks for using this workflow"); } else { Console.WriteLine ("F, Bye..."); Exception ex = new Exception("User Said No!"); ex. Data ["Reason" ] = "YesOrNo viols false"; } } } Вывод этой программы был бы абсолютно идентичен предыдущему рабочему потоку на базе XAML. Так зачем вообще связываться со всеми этими действиями? Прежде всего, учтите, что не всем удобно читать код С#. Если придется объяснять этот бизнес- процесс, скажем, продавцам и нетехническим менеджерам, что вы выберете — код С# или блок-схему? Ответ должен быть очевиден. Однако более важно то, что API-интерфейс WF имеет множество дополнительных служб времени выполнения, включая сохранение длительно выполняющихся рабочих потоков в базе данных, автоматическое отслеживание событий рабочего потока и тому подобное. Если представить себе объем работы, которую пришлось бы сделать, чтобы воспроизвести всю эту функциональность в новом проекте, польза от WF станет еще более очевидной. Наконец, имея возможность декларативно генерировать рабочие потоки с использованием визуальных конструкторов и XAML, можно передать конструирование рабочих потоков экспертам и менеджерам, ограничившись при этом включением XAML в проекты С#. С учетом сказанного, API-интерфейс WF — не обязательно правильный выбор для всех программ .NET. Тем не менее, для наиболее традиционных бизнес-приложений возможности определения, хостинга, выполнения и мониторинга рабочих потоков действительно очень полезны. Как с любой новой технологией, понадобится определить, что будет полезно для текущего проекта. Давайте рассмотрим другой пример работы с API- интерфейсом WF, на этот раз упаковав рабочий поток в отдельную сборку *.dll. Исходный код. Проект EnumerateMachinelnfoWF доступен в подкаталоге Chapter 26.
984 Часть V. Введение в библиотеки базовых классов .NET Изоляция рабочих потоков в выделенных библиотеках Хотя создание консольного приложения рабочего потока — замечательный способ поэкспериментировать с API-интерфейсом WF, готовый для реальной производственной эксплуатации рабочий поток должен быть упакован в специальную сборку .NET *.dll. После этого рабочие потоки можно многократно использовать на двоичном уровне в различных проектах. Хотя можно было бы начать с использования проекта библиотеки классов С# в качестве стартовой точки, более простой способ построения библиотеки рабочего потока состоит в том, чтобы начать с шаблона проекта Activity Library (Библиотека действий), находящегося в узле Workflow (Рабочий поток) диалогового окна New Project. Преимущество этого типа проекта состоит в том, что он автоматически устанавливает все необходимые ссылки на сборки WF и предоставляет файл *.xaml для создания начального рабочего потока. Создаваемый рабочий поток будет моделировать процесс запроса к базе данных AutoLot с целью проверки, имеется ли в таблице Inventory указанный автомобиль соответствующего цвета и изготовителя. Если запрошенный автомобиль есть на складе, для хоста будет сформирован ответ в виде выходного параметра. Если нужного элемента на складе нет, будет сгенерировано сообщение руководителю отдела продаж о том, чтобы он нашел автомобиль нужного цвета. Определение начального проекта Создайте новый проект Activity Library по имени ChecklnventoryWorkf lowLib (рис. 26.18). Сразу после создания проекта переименуйте начальный файл Activity].. xaml в Checklnventory.xaml. Рис. 26.18. Построение библиотеки действий Прежде чем двигаться дальше, закройте визуальный конструктор рабочего потока и просмотрите лежащее в основе определение XAML, щелкнув правой кнопкой мыши на файле XAML и выбрав в контекстном меню пункт View Code (Просмотреть код). Удостоверьтесь, что атрибут х:Class в корневом элементе <Activity> соответствующим образом обновлен (если нет, замените Activityl на Checklnventory): <Activity x:Class="CheckInventoryWorkflowLib.Checklnventory" ... >
Глава 26. Введение в Windows Workflow Foundation 4.0 985 На заметку! При создании библиотеки рабочего потока всегда проверяйте значение х:Class корневого действия, поскольку клиентские программы используют это имя для создания экземпляра рабочего потока. В этом рабочем потоке в качестве первичного действия используется Sequence вместо Flowchart. Перетащите новое действие Sequence на поверхность визуального конструктора (оно находится в области Control Flow (Поток управления) панели инструментов) и измените значение свойства DisplayName на Look Up Product. Как следует из названия, действие Sequence позволяет создавать последовательные задачи, которые выполняются одна за другой. Однако это не обязательно означает, что дочерние действия должны следовать строго линейному порядку. Последовательность может содержать блок-схемы, другие последовательности, параллельную обработку данных, ветвление if/else и все, что имеет смысл для проектируемого бизнес-процесса. Импорт сборок и пространств имен Поскольку рабочий поток будет взаимодействовать с базой данных AutoLot, следующий шаг состоит в добавлении ссылки на сборку AutoLot.dll с помощью диалогового окна Add Reference (Добавить ссылку) среды Visual Studio 2010. В этом примере используется автономный уровень, поэтому рекомендуется взять финальную версию этой сборки, созданную в главе 22 (в случае установки ссылки на финальную версию, созданную в главе 23, также понадобится установить ссылку на System.Data.Entity.dll, как того требует ADO.NET Entity Framework). В рабочем потоке для опроса возвращенного объекта DataTable с целью определения наличия запрошенного товара на складе используется LINQ to DataSet. Поэтому также потребуется установить ссылку на System.Data.DataSetExtensions.dll. После добавления ссылок на эти сборки щелкните на кнопке Imports (Импорты) в нижней части визуального конструктора рабочих потоков. Отобразится список всех пространств имен .NET, которые можно использовать в визуальном конструкторе рабочих потоков (воспринимайте эту часть как декларативную версию ключевого слова С# using). Для добавления пространств имен из всех ссылаемых сборок необходимо просто ввести их в текстовом поле, находящемся в верхней части редактора Imports. Для удобства импортируйте AutoLotDisconnectedLayer и System.Data.DataSetExtensions. После этого можно будет ссылаться на содержащиеся типы без необходимости использования полностью квалифицированных имен. На рис. 26.19 показано финальное содержимое области Imports. Enter or Sel'ecf namespace Imported namespaces AutoLotDisconnectedLayer Microsoft. VisualBasic Microsoft. VisuaiBasicActivrties System System Activities System Activities .Expressions System Activrties.Statements System Activities.Validation System. Activities ,Xamllntegration System.Collections.Generic System .Data System.Data. DataSetExtensions System.Li nq Я Imports _ Рис. 26.19. Подобно ключевому слову С# using, область Imports позволяет включать пространства имен .NET
986 Часть V. Введение в библиотеки базовых классов .NET Определение аргументов рабочего потока Определите два новых входных аргумента уровня рабочего потока с именами RequestedMake и RequestedColor, оба типа String. Подобно предыдущему примеру, здесь хост рабочего потока создаст объект Dictionary, который содержит данные, отображаемые на эти аргументы, так что нет необходимости присваивать этим элементам значения по умолчанию в редакторе Arguments. Как и можно было предположить, рабочий поток использует эти значения для выполнения запроса к базе данных. Тот же самый редактор Arguments применяется и для определения выходного аргумента по имени FormattedResponse типа String. Когда нужно вернуть данные из рабочего потока обратно хосту, можно создать любое количество выходных аргументов, которые могут быть перечислены хостом по завершении рабочего потока. На рис. 26.20 показано текущее состояние визуального конструктора рабочих потоков. Name RequestedMake RequestedColor FormattedResponse \Crcate Argument Direction In In Out Argument type String String String Default value Default value not j upported Рис. 26.20. Аргументы рабочего потока предоставляют способ передачи и возврата данных из рабочего потока Определение переменных рабочего потока Теперь в рабочем потоке необходимо объявить переменную-член, которая соответствует классу InventoryDALDisLayer из AutoLot.dll. Вспомните из главы 22, что этот класс позволяет получать данные из Inventory, возвращенные в виде DataTable. Выберите действие Sequence в визуальном конструкторе и, щелкнув на кнопке Variables, создайте переменную по имени AutoLotlnventory. В раскрывающемся списке Variable Туре (Тип переменной) выберите пункт Browse for Types... (Обзор типов...) и введите тип InventoryDALDisLayer (рис. 26.21). Browse and Select а Type Name: AutoLotDisconnectedlayer.InventoryDALDisLayer * Referenced assernblies> л AutoLotOAL A.0.0.0] л AutoLotDiscormectedLa Cancel Рис. 26.21. Переменные рабочего потока предлагают способ декларативного их определения внутри контекста Убедившись, что новая переменная выбрана, в окне Properties щелкните на кнопке с многоточием для свойства Default. Откроется окно редактора кода, размеры которого можно изменять произвольным образом. Этот редактор значительно упрощает ввод сложного кода присваивания переменной. Введите следующий код (VB), который создаст новую переменную InventoryDALDisLayer:
Глава 26. Введение в Windows Workflow Foundation 4.0 987 New InventoryDALDisLayer("Data Source=(local)\SQLEXPRESS;" + "Initial Catalog=AutoLot;Integrated Security=True") Объявите вторую переменную рабочего потока типа System.Data.DataTable по имени Inventory, опять используя пункт меню Browse for Types... (установите ее значение по умолчанию в Nothing). Позднее переменной Inventory будет присвоен результат вызова GetAllInventoryO на переменной InventoryDALDisLayer. Работа с действием Assign Действие Assign позволяет устанавливать значение переменной, которое может быть результатом выполнения любых допустимых операторов кода. Перетащите действие Assign (находящееся в области Primitives панели инструментов) на действие Sequence. В поле редактирования То укажите переменную Inventory. В окне Properties щелкните на кнопке с многоточием для свойства Value и введите следующий код: AutoLotInventory.GetAllInventory () После того как действие Assign встречается в рабочем потоке, получится объект DataTable, содержащий все записи из таблицы Inventory. Однако проверить наличие корректного товара на складе необходимо вручную с использованием значений аргументов RequestedMake и RequestedColor, присланных от хоста. Для определения существования товара применяется LINQ to DataSet. После этого используется действие If для выполнения XAML-эквивалента оператора if /else/then. Работа с действиями If и Switch Перетащите действие If на узел Sequence непосредственно под действием Assign. Поскольку это действие представляет собой основанный на XAML способ принятия решения во время выполнения, прежде всего оно должно быть сконфигурировано для проверки булевского выражения. В редакторе условий Condition введите следующую проверку запроса LINQ to DataSet: (From car In Inventory.AsEnumerable() Where CType(car("Color") , String) = RequestedColor And CType(car("Make"), String) = RequestedMake Select car).Any() Этот запрос использует данные RequestedColor и-RequestedMake, поступившие от хоста, для извлечения всех записей из DataTable об автомобилях нужного изготовителя и цвета (обратите внимание, что операция Ctype — это эквивалент операции явного приведения в С#). Вызов расширяющего метода Апу() вернет true или false, в зависимости от того, содержит ли результат запроса какие-то данные. Следующей задачей будет конфигурирование набора действий, которые будут выполнены, когда определенное условие истинно или ложно. Вспомните, что конечная цель — отправить пользователю сформатированное сообщение, если запрошенный им автомобиль есть на складе. Однако чтобы усложнить задачу, мы будем возвращать уникальное сообщение, зависящее от того, какого производителя автомобиль был запрошен (BMW, Yugo или что-то еще). Перетащите действие Switch<T> (находящееся в области Control Flow панели инструментов) в область Then действия If. Откроется диалоговое окно с запросом обобщенного типа. Укажите String. В области Expression действия Switch введите RequestedMake. Вы увидите, что опция Default (По умолчанию) переключателя уже на месте, и будет предложено добавить действие, представляющее ответ. В рассматриваемом примере требуется только одно действие Assign; когда нужно выполнить более сложные действия, то областью по умолчанию, скорее всего, будет Sequence или Flowchart.
988 Часть V. Введение в библиотеки базовых классов .NET После добавления действия Assign в область редактирования Default (по щелчку на ссылке Add Activity (Добавить действие)), присвойте аргументу FormattedResponse следующий оператор кода: String. Format ("Yes, we have a {0} {1} you can purchase", - ReguestedColor, ReguestedMake) Теперь редактор Switch будет выглядеть, как показано на рис. 26.22. ♦ 3 Swhch<String> Expression RequestedMake Default A* Assign FormattedRespons . ! Add new case z String.Format("Yes, A Рис. 26.22. Определение задачи по умолчанию для действия Switch Теперь щелкните на ссылке Add New Case (Добавить новый вариант) и введите BMW (без двойных кавычек) для первой ветви Case, после чего еще раз щелкните для ввода финальной ветви со значением Yugo (снова без кавычек). В каждую из этих областей Case перетащите действие Assign, в обоих случаях для присваивания значения переменной FormattedResponse. В случае BMW укажите такое значение: String. Format ("Yes sir! We can send you {0} {1} as soon as {2}!", _ ReguestedColor, ReguestedMake, DateTime.Now) В случае Yugo используйте следующее выражение: String. Format ("Please, we will pay you to get this {0} off our lot!", _ ReguestedMake) После этого действие Switch будет выглядеть, как показано на рис. 26.23. 0 Switch<String> Expression RequestedMake Default *8 Assign FormattedRespons : 4 Case BMW Case Yugo Add new case String.Format("Yes, AJ Assign Assign Рис. 26.23. Окончательный вид действия Switch Построение специального действия кода Впечатляющим средством визуального конструктора рабочего потока является его способность встраивать сложные операторы кода (и запросы LINQ) в файл XAML, поскольку наверняка возникнет необходимость написать код в выделенном классе. Существуют несколько способов сделать это с помощью API-интерфейса WF, но наиболее простой из них предусматривает создание класса, расширяющего Code Activity,
Глава 26. Введение в Windows Workflow Foundation 4.0 989 или же, если действие должно возвращать значение — CodeActivity<T> (где Т — тип возвращаемого значения). Рассмотрим пример создания специального действия, которое будет выводить данные в текстовый файл, информируя персонал отдела продаж о запросе автомобиля, которого нет на складе. Выберите пункт меню Projects Add New Item (Проекте Добавить новый элемент), укажите в качестве шаблона Code Activity (Действие кода) и назначьте ему имя CreateSalesMemoActivity.cs (рис. 26.24). Рис. 26.24. Добавление нового действия кода Если специальное действие требует какого-то ввода для последующей обработки, он может быть представлен свойством, инкапсулирующим объект InArgument<T>. Тип класса InArgument<T> — это специфическая сущность API-интерфейса WF, которая обеспечивает возможность передачи данных, предоставленных рабочим потоком, самому специальному классу действия. Создаваемому действию понадобится два таких свойства, которые будут представлять производителя и цвет искомого товара на складе. Кроме того, специальное действие кода должно переопределить виртуальный метод Execute(), который будет вызван исполняющей средой WF, когда до него дойдет очередь выполнения рабочего потока. Обычно в этом методе будут использоваться свойства InArgument<T> для получения переданной рабочей нагрузки. Реальное переданное значение должно получаться непрямо с помощью метода GetValueO входного CodeActivityContext. Ниже приведен код специального действия, которое будет генерировать новый файл *.txt, описывающий ситуацию для отдела продаж: public sealed class CreateSalesMemoActivity : CodeActivity { // Два свойства для специального действия. public InArgument<string> Make { get; set; } public InArgument<string> Color { get; set; } // Если действие возвращает значение, унаследовать его от // CodeActivity<TResult> и вернуть значение из метода Execute(). protected override void Execute(CodeActivityContext context) { // Вывод сообщения в локальный текстовый файл. StringBuilder salesMessage = new StringBuilder(); salesMessage.AppendLine("***** Attention sales team1 *****»); salesMessage.AppendLine ("Please order the following ASAP1");
990 Часть V. Введение в библиотеки базовых классов .NET salesMessage.AppendFormat( {0} {1}\п", context.GetValue(Color), context.GetValue(Make)); salesMessage.AppendLine с*********************************»); System.IO.File.WriteAllText("SalesMemo.txt", salesMessage.ToString()); } } Как должно использоваться специальное действие кода? Для специального действия кода в WF 4.0 допускается строить специальный визуальный конструктор. Однако это требует понимания WPF, поскольку специальный визуальный конструктор использует многие из тех технологий, что применяются для построения объектов WPF Window или UserControl. Технология WPF начнет рассматриваться в следующей главе, а пока ограничимся простейшим подходом. Скомпилируйте сборку рабочего потока. Открыв окно визуального конструктора рабочих потоков в Visual Studio 2010, взгляните в верхнюю часть панели инструментов. Там должно появиться специальное действие (рис. 26.25). IToclbox v П X | 1 л ChecWwentoryWorkflawLfb ^ Pointer _J GeateSaleslVlemoAttn 1 л Control Flow llf Pointer +} DoWhtle 2 . Version 1.0,0,0 Managed ,NE1 Component 'Щ ForEach<T> & « * -J Рис. 26.25. Специальное действие кода появляется в панели инструментов Visual Studio 2010 Перетащите действие Sequence в ветвь Else действия If. Затем перетащите специальное действие в Sequence. Теперь в окне Properties можно присвоить значения каждому из представленных свойств. Используя переменные RequestedMake и RequestedMake, установите свойства Make и Color действия, как показано на рис. 26.26. Рис. 26.26. Установка свойств специального действия кода Для завершения рабочего потока перетащите финальное действие Assign в действие Sequence ветви Else и установите в качестве значения FormattedResponse строку "Sorry, out of stock" (Сожалеем, нет на складе). На рис. 26.27 показан окончательный вид рабочего потока. Скомпилируйте проект и переходите к финальной части главы, где будет построен клиентский хост, который будет использовать этот рабочий поток. Исходный код. Проект ChecklnventoryWorkf lowLib доступен в подкаталоге Chapter 26.
Глава 26. Введение в Windows Workflow Foundation 4.0 991 ^ Look Up Product jfei Condition (From car In Inventory.AsEnumerableQ §| Swrtch<String> Expression RequestedMake Default Case "BWM" Case "Yugo" Add net* case *S Assign Inventory Then \ = AutoLotInventory.( * Assign Assign Assign Л Else j|| Sequence Л 4^J| CreateSalesMemoActivity Агв Assign FormattedRespons = "Sorry, out of stock V Рис. 26.27. Завершенный последовательный рабочий поток Использование библиотеки рабочего потока Библиотеку рабочего потока может использовать приложение любого рода; однако мы здесь предпочтем простоту и построим простое консольное приложение под названием Workf lowLibraryClient. Создав проект, понадобится установить ссылки не только на сборки CheckInventoryWorkflowLib.dll и AutoLot.dll, но также и на ключевую библиотеку WF 4.0 — System.Activities.dll, которая находится на вкладке .NET диалогового окна Add Reference в Visual Studio 2010. После этого поместите в файл Program.cs следующий код: using System; using System.Linq; using System.Activities; using System. Collections .Generic- using ChecklnventoryWorkflowLib; namespace WorkflowLibraryClient { class Program static void Main(string [ ] args) Console.WnteLine ("**** Inventory Look up // Получить предпочтения пользователя. Console.Write("Enter Color: "); string color = Console.ReadLine(); Console.Write("Enter Make: " ) ; string make = Console.ReadLine(); ');
992 Часть V. Введение в библиотеки базовых классов .NET // Упаковать данные для рабочего потока. Dictionary<stnng, object> wfArgs = new Dictionary<string, object>() {"RequestedColor", color}, {"RequestedMake", make} try // Отправить данные рабочему потоку. Workflowlnvoker.Invoke(new Checklnventory(), wfArgs); catch (Exception ex) Console.WriteLine(ex.Message); } } } Как это уже делалось в других примерах, воспользуемся классом Workf lowlnvoker для запуска рабочего потока в синхронном режиме. Теперь давайте посмотрим, как получить возвращаемое значение рабочего потока. Вспомните, что как только рабочий поток завершился, необходимо получить сформатированный результат Получение выходного аргумента рабочего потока Метод Workf lowlnvoker. Invoke () вернет объект, реализующий интерфейс IDictionary<string, objectx Поскольку рабочий поток может возвращать любое количество выходных аргументов, необходимо указать имя нужного выходного аргумента как строковое значение для индексатора типа. Модифицируйте логику try/catch следующим образом: try { // Отпарвить данные рабочему потоку. IDictionary<string, object> outputArgs = Workflowlnvoker.Invoke (new Checklnventory(), wfArgs); // Вывести выходное сообщение на консоль. Console.WriteLine(outputArgs["FormattedResponse"] ) ; } catch (Exception ex) { Console.WriteLine(ex.Message); } Теперь можно запустить приложение и ввести изготовителя и цвет автомобиля, имеющегося в таблице Inventory базы данных AutoLot. Ниже показан результирующий вывод: **** Inventory Look up **** Enter Color: Black Enter Make: BMW Yes sir! We can send you Black BMW as soon as 2/17/2010 9:23:01 PM! Press any key to continue . . . Если ввести информацию об элементе, которого на складе нет, вывод будет выглядеть следующим образом:
Глава 26. Введение в Windows Workflow Foundation 4.0 993 **** Inventory Look up **** Enter Color: Pea Soup Green Enter Make: Viper Sorry, out of stock Press any key to continue . . . Кроме того, вы обнаружите в папке bin\Debug клиентского приложения новый файл *.txt, в котором сохранено напоминание для продавцов: ***** Attention sales team1 ***** Please order the following ASAP1 1 Pea Soup Green Viper На этом знакомство с новым API-интерфейсом WF 4.0 завершено. Как упоминалось в начале этой главы, если вы работали с предыдущей версией API-интерфейса Windows Workflow Foundation, то заметите, что вся программная модель была полностью переделана (и существенно улучшена). В этой главе рассматривались лишь некоторые ключевые аспекты этого API- интерфейса .NET 4.0, но был построен достаточный фундамент для дальнейших самостоятельных исследований. Исходный код. Проект Workf lowLibraryClientproject доступен в подкаталоге Chapter 26. Резюме По существу, WF позволяет моделировать внутренние бизнес-процессы приложения непосредственно в самом приложении. Помимо простого моделирования общего рабочего потока, однако, в WF предлагается завершенная исполняющая среда и несколько служб, которые дополняют общую функциональность этого API-интерфейса (службы постоянного хранения и отслеживания и т.п.). Хотя в главе эти службы не рассматривались непосредственно, помните, что приложение WF производственного уровня почти наверняка будет использовать их. В .NET 4.0 программная модель WF, изначально представленная в .NET 3.0, полностью поменялась. Теперь можно проектировать рабочие потоки целиком в декларативной манере, используя основанную на XML грамматику под названием XAML. Вдобавок XAML можно применять не только для определения состояния каждого действия в рабочем потоке, но также неявно встраивать "реальный код" в документ XAML, используя визуальные конструкторы WF. В этой вводной главе вы ознакомились с двумя ключевыми действиями верхнего уровня — Flowchart и Sequence. Хотя каждое из них управляет потоком логики уникальным способом, оба они могут содержать одинаковые дочерние действия и выполняться хостом в одинаковой манере (через Workf lowlnvoker или Workf lowApplication). Кроме того, вы узнали, как передавать аргументы хоста в рабочий поток с применением обобщенного объекта Dictionary и как получать выходные аргументы из рабочего потока с помощью обобщенного объекта, совместимого с I Dictionary.
ЧАСТЬ VI Построение настольных пользовательских приложений с помощью WPF В этой части... Глава 27. Введение в Windows Presentation Foundation и XAML Глава 28. Программирование с использованием элементов управления WPF Глава 29. Службы визуализации графики WPF Глава 30. Ресурсы, анимация и стили WPF Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления
ГЛАВА 27 Введение в Windows Presentation Foundation и XAML Когда вышла версия 1.0 платформы .NET, программисты, которым нужно было строить графические настольные приложения, использовали два API-интерфейса Windows Forms и GDI+, упакованные в основном в сборки System.Windows.Forms.dll и System.Drawing.dll. Хотя Windows Forms и GDI+ — блестящие API-интерфейсы для построения традиционных настольных графических интерфейсов, начиная с версии .NET 3.0, также предлагается альтернативный API-интерфейс под названием Windows Presentation Foundation (WPF). Эта вводная глава о WPF начинается с рассмотрения мотивации, лежащей в основе этой новой технологии построения пользовательских интерфейсов, что поможет увидеть разницу между моделями программирования Windows Forms/GDI+ и WPF. Затем мы рассмотрим различные типы приложений WPF, поддерживаемых этим API- интерфейсом и изучим роль типов Application, Window, ContentControl. Control, UIElementHFramewprkElement. Попутно вы научитесь перехватывать действия мыши и клавиатуры, определять данные на уровне приложения и решать другие распространенные задачи WPF, не используя ничего, кроме кода С#. В этой главе вы ознакомитесь с грамматикой на основе XML, которая называется расширяемым языком разметки приложений (Extensible Application Markup Language — XAML). Вы изучите синтаксис и семантику XAML, включая синтаксис присоединяемых свойств, роль преобразователей типов, расширения разметки, а также узнаете, как происходит генерация, загрузка и разбор XAML во время выполнения. Будет показано, как интегрировать данные XAML в кодовую базу С# WPF (и выгоды, с этим связанные). Глава завершается рассмотрением различных специфичных для WPF инструментов, поставляемых в составе IDE-среды Visual Studio 2010. Будет написан собственный редактор/анализатор XAML, демонстрирующий манипулирование XAML во время выполнения для построения динамического пользовательского интерфейса. На заметку! В остальных главах, посвященных WPF, будет описан Microsoft Expression Blend — инструмент для генерации XAML-разметки.
Глава 27. Введение в Windows Presentation Foundation и XAML 997 Мотивация, лежащая в основе WPF С годами в Microsoft разработали многочисленные инструменты для создания пользовательского интерфейса (С/C++/Windows API, VB6, MFC и т.д.), предназначенные для построения настольных приложений. Каждый из этих программных инструментов предлагает кодовую базу для представления основных аспектов приложения с графическим интерфейсом, включая главные окна, диалоговые окна, элементы управления, системы меню и т.п. В начальном выпуске платформы .NET API-интерфейс Windows Forms быстро стал предпочтительной моделью разработки пользовательских интерфейсов, благодаря его простой, но очень мощной объектной модели. Хотя с помощью Windows Forms было успешно разработано множество полноценных настольных приложений, следует признать, что его программная модель довольно сисси- метрична. Просто говоря, сборки System.Windows.Forms.dll и System.Drawing.dll не обеспечивают прямой поддержки многих дополнительных технологий для построения полноценного настольного приложения. Чтобы проиллюстрировать это утверждение, рассмотрим природу разработки графического интерфейса, предшествующую WPF (т.е. .NET 2.0; см. табл. 27.1). Таблица 27.1. Решения .NET 2.0 для обеспечения желаемой функциональности Желаемая функциональность Решение .NET 2.0 Построение форм с элементами управления Windows Forms Поддержка двухмерной графики GDI+ (System.Drawing.dll) Поддержка трехмерной графики API-интерфейсы DirectX Поддержка потокового видео API-интерфейсы Windows Media Player Поддержка документов нефиксированного формата Программное манипулирование PDF-файлами Как видите, разработчик Windows Forms вынужден заимствовать типы из множества несвязанных API-интерфейсов и объектных моделей. Хотя и верно, что использование всех этих разнообразных API-интерфейсов синтаксически похоже (в конце концов, это просто код С#), каждая технология требует радикально иного мышления. Например, навыки, необходимые для создания трехмерной анимации с использованием DirectX, совершенно отличаются от тех, что нужны для привязки данных к экранной сетке. Честно говоря, программисту Windows Forms чрезвычайно трудно в равной мере овладеть природой каждого из этих API-интерфейсов. На заметку! В приложении А предлагается введение в разработку приложений Windows Forms и GDI+. Унификация различных API-интерфейсов Технология WPJF (появившаяся в версии .NET 3.0) специально создавалась для того, чтобы объединить все эти ранее несвязанные программистские задачи в рамках единой объектной модели. Таким образом, при разработке трехмерной анимации больше не возникает необходимости в ручном кодировании с использованием API-интерфейсов DirectX (хотя это можно делать), поскольку нужная функциональность встроена в WPF. Чтобы увидеть, насколько все проясняется, взгляните на табл. 27.2, в которой проиллюстрирована модель настольной разработки, предложенная в .NET 3.0.
998 Часть VI. Построение настольных пользовательских приложений с помощью WPF Таблица 27.2. Решения .NET 3.0 для обеспечения желаемой функциональности Желаемая функциональность Решение .NET 3.0 и последующих версий Построение форм с элементами управления WPF Поддержка двухмерной графики WPF Поддержка трехмерной графики WPF Поддержка потокового видео WPF Поддержка документов нефиксированного формата WPF Очевидное преимущество здесь в том, что программисты .NET теперь имеют единый, симметричный API-интерфейс для всех общих нужд построения графических интерфейсов. Освоив ключевые сборки WPF и грамматику XAML, вы будете поражены, насколько быстро с их помощью можно создавать очень сложные пользовательские интерфейсы. Обеспечение разделения ответственности через XAML Возможно, одним из наиболее значительных преимуществ WPF стал способ четкого отделения внешнего вида и поведения приложения Windows от программной логики, управляющей этим. Используя XAML, можно определить пользовательский интерфейс приложения через разметку XML. Эта разметка (в идеале генерируемая с помощью инструментов, таких как Microsoft Expression Blend) может быть затем присоединена к управляемому коду для обеспечения деталей функциональности программы. На заметку! Применение XAML не ограничивается приложениями WPF. Любое приложение может использовать XAML для описания дерева объектов .NET, даже если они не имеют отношения к визуальному пользовательскому интерфейсу. Например, в API-интерфейсе Windows Workflow Foundation основанная на XAML грамматика применяется для определения бизнес-процессов и специальных действий. По мере погружения в WPF вы удивитесь тому, какую гибкость обеспечивает "разметка рабочего стола". XAML позволяет определять не только простые элементы пользовательского интерфейса (кнопки, таблицы, окна списков и т.п.), но также двух- и трехмерную графику, анимацию, логику привязки данных и функциональность мультимедиа (вроде воспроизведения видео). Например, определение круглой кнопки, которая анимирует логотип компании, требует всего нескольких строк разметки. Как будет показано в главе 31, элементы управления WPF могут быть модифицированы стилями и шаблонами, что позволяет строить внешний вид приложения с минимальными усилиями. В отличие от Windows Forms, единственной веской причиной для построения библиотеки специальных элементов управления WPF может быть необходимость в изменении поведения элемента управления (т.е. добавление специальных методов, свойств или событий, переопределение виртуальных методов в подклассах). Если вы просто хотите изменить внешний вид элемента управления (как в случае с круглой анимированной кнопкой), это можно сделать полностью через разметку. Обеспечение оптимизированной модели визуализации Такие наборы инструментов для построения графических интерфейсов, как Windows Forms, MFC или VB6, обрабатывают все запросы на визуализацию (включая визуализацию элементов управления, подобных кнопкам и окнам списков), используя низкоуров-
Глава 27. Введение в Windows Presentation Foundation и XAML 999 невый API-интерфейс, основанный на С (GDI), который является составной частью ОС Windows в течение многих лет. GDI обеспечивает адекватную производительность простых графических программ; однако если пользовательскому интерфейсу приложения нужна была высокопроизводительная графика, приходилось обращаться к DirectX. Программная модель WPF существенно отличается в том, что при визуализации графических данных GDI не используется^ Все операции визуализации (т.е. двух- и трехмерная графика, анимация, визуализация графических интерфейсных элементов) теперь используют API-интерфейс DirectX. Очевидной выгодой этого является тот факт, что приложение WPF автоматически использует преимущества аппаратной и программной оптимизации. К тому же приложения WPF могут задействовать очень развитые графические службы (эффекты размытия, сглаживания, прозрачности и т.п.) без сложностей, присущих прямому программированию для API-интерфейса DirectX. На заметку! Хотя WPF выносит все запросы визуализации на уровень DirectX, нельзя утверждать, что приложение WPF будет работать столь же быстро, как приложение, построенное на основе неуправляемого C++ и DirectX. При разработке настольного приложения, которое требует максимальной скорости выполнения (вроде трехмерной игры), неуправляемый C++ и DirectX по-прежнему остаются наилучшим подходом. Упрощение программирования сложных пользовательских интерфейсов Чтобы закрепить тему, повторимся еще раз: Windows Presentation Foundation (WPF) — это новый API-интерфейс, предназначенный для построения настольных приложений, который интегрирует различные настольные API-интерфейсы в рамках единой объектной модели, с обеспечением четкого разделения ответственности через XAML. В дополнение к этим важнейшим моментам приложения WPF также выигрывают от других дополнительных средств, многие из которых будут рассматриваться в последующих главах. Ниже кратко перечислены основные службы WPF • Множество диспетчеров компоновки (намного больше, чем в Windows Forms) для обеспечения исключительно гибкого контроля над размещением содержимого. • Использование расширенного механизма привязки данных для связи содержимого с элементами пользовательского интерфейса разнообразными способами. • Встроенный механизм стилей, позволяющий определять "темы" для приложения WPF. • Использование векторной графики, которая позволяет автоматически изменять размеры содержимого для соответствия размеру и разрешению экрана, принимающего приложение. • Поддержка двух- и трехмерной графики, анимации и воспроизведения видео и аудио. • Развитый типографский API-интерфейс, поддерживающий документы XML Paper Specification (XPS), фиксированные документы (WYSIWYG), документы нефиксированного формата и аннотации в документах (например, API-интерфейс Sticky Notes). • Поддержка взаимодействия с унаследованными моделями графического интерфейса (т.е. Windows Forms, ActiveX и Win32 HWND). Например, можно встраивать специальные элементы управления Windows Forms в приложение WPF и наоборот. Теперь, имея представление о вкладе WPF в платформу, рассмотрим различные типы приложений, которые могут быть созданы с использованием этого API-интерфейса.
1000 Часть VI. Построение настольных пользовательских приложений с помощью WPF Различные варианты приложений WPF API-интерфейс WPF может использоваться для построения широкого разнообразия приложений с графическим интерфейсом, которые в основном отличаются структурой навигации и моделями развертывания. Ниже будут представлены их краткие описания. Традиционные настольные приложения Первая (и наиболее популярная) форма — это традиционная исполняемая сборка, которая запускается на локальной машине. Например, WPF можно использовать для построения текстового редактора, программы рисования или мультимедийной программы, такой как цифровой музыкальный проигрыватель, средство просмотра фотографий и т.п. Подобно любому другому настольному приложению, эти файлы *.ехе могут устанавливаться традиционными средствами (программами установки, пакетами Windows Installer и т.п.) или же посредством технологии ClickOnce, позволяющей распространять и устанавливать настольные приложения через удаленный веб-сервер. Говоря языком программиста, этот тип приложений WPF использует (как минимум) типы Window и Application в дополнение к ожидаемому набору диалоговых окон, панелей инструментов, панелей состояния, систем меню и прочих элементов пользовательского интерфейса. WPF позволяет строить как базовые, простые бизнес-приложения без каких-либо излишеств, так и встраивать средства подобного рода. На рис. 27.1 показан пример настольного приложения WPF для просмотра медицинских карточек пациентов в учреждении здравоохранения. к < Patient Monitoring Рис. 27.1. Настольное приложение WPF с развитым интерфейсом
Глава 27. Введение в Windows Presentation Foundation и XAML 1001 К сожалению, на печатной странице невозможно отразить весь набор средств данного окна. Обратите внимание, что в правом верхнем углу главного окна отображается график реального времени, показывающий синусный ритм пациента. Если щелкнуть на кнопке Patient Details (Информация о пациенте) в нижнем правом углу, произойдут несколько анимаций, которые трансформируют пользовательский интерфейс к следующему виду (рис. 27.2). Рис. 27.2. Трансформации и анимации очень легко реализуются с помощью WPF Можно ли построить подобное приложение без WPF? Безусловно. Однако объем и сложность кода будут намного выше. На заметку! Этот пример приложения доступен для загрузки (вместе с исходным кодом) на официальном веб-сайте WPF по адресу http://windowsclient.net. Здесь можно найти множество примеров проектов WPF (и Windows Forms), разборов технологии и форумов. WPF-приложения на основе навигации Приложения WPF могут также использовать структуру на основе навигации, которая позволяет традиционному настольному приложению вести себя подобно приложению веб-браузера. Применяя эту модель, можно построить настольную программу *.ехе, которая включает в себя кнопки "вперед" и "назад", позволяющие конечному пользователю перемещаться вперед и назад по различным экранам пользовательского интерфейса, именуемым страницами. Само приложение поддерживает список страниц и обеспечивает необходимую инфраструктуру для навигации по ним, попутно передавая данные и поддерживая список хронологии. Для примера посмотрите на проводник Windows (рис. 27.3), в котором используется эта функциональность. Обратите внимание, что кнопки навигации (и список хронологии) находятся в верхнем левом углу окна.
1002 Часть VI. Построение настольных пользовательских приложений с помощью WPF Рис. 27.3. Настольная программа на основе навигации Несмотря на то что настольное приложение WPF может принимать веб-подобную схему навигации, помните, что это всего лишь вопрос дизайна пользовательского интерфейса. Само приложение остается в виде той же исполняемой сборки, запускаемой на настольной машине, и помимо внешнего сходства не имеет никакого отношения к веб-приложениям. Говоря языком программистов, эта разновидность WPF-приложений построена с использованием таких типов, как Application, Page, NavigationWindow и Frame. Приложения ХВАР WPF также позволяет строить приложения, которые могут размещаться внутри веб-браузера. Такая разновидность приложений WPF называется браузерными приложениями XAML, или ХВАР. Согласно этой модели, конечный пользователь переходит по заданному URL-адресу, указывающему на приложение ХВАР (которое представляет собой коллекцию объектов Page), затем прозрачно загружает и устанавливает его на локальной машине. В отличие от традиционной установки исполняемого приложения с помощью ClickOnce, программа ХВАР располагается непосредственно в браузере и принимает встроенную систему навигации браузера. На рис. 27.4 показана ХВАР- программа в действии (а именно — пример WPF Expenselt, поставляемый в составе .NET Framework 4.0 SDK). Преимущество технологии ХВАР состоит в том, она позволяет создавать сложные пользовательские интерфейсы, которые являются более выразительными, чем типичная веб-страница, построенная с помощью HTML и JavaScript. Объект Page в WPF может использовать те же службы, что и настольное приложение WPF, включая анимации, двух- и трехмерную графику, темы и т. п. По сути, веб-браузер в данном случае — просто контейнер объектов Page, а не средство отображения веб-страниц ASP.NET Однако, учитывая, что объекты Page развертываются на удаленном веб-сервере, приложения ХВАР можно легко сопровождать в разных версиях и обновлять без необходимости поставки исполняемых сборок на пользовательские настольные машины. Подобно традиционному веб-приложению, объекты Page можно легко обновлять на вебсервере, и пользователь всегда будет получать самую актуальную версию, обращаясь по заданному URL-адресу. Возможным недостатком этой разновидности программ WPF является то, что ХВАР могут работать только внутри веб-браузеров Microsoft Explorer или Firefox. При развертывании такого приложения в корпоративной сети компании совместимость браузеров не должна быть проблемой, так как системные администраторы могут просто диктовать
Глава 27. Введение в Windows Presentation Foundation и XAML 1003 выбор браузера, обязательного для установки на пользовательских машинах. Однако, открывая доступ к ХВАР-приложению внешнему миру, невозможно гарантировать, что каждый пользователь будет работать с браузером Internet Explorer или Firefox, а потому некоторые из них просто не смогут его просмотреть. Другая проблема состоит в том, что машина, которая выполняет ХВАР-приложение, должна иметь локальную установку платформы .NET, поскольку объекты Page пользуются теми же сборками .NET, что и традиционные приложения. Учитывая это, ХВАР- приложения ограничены только средами Windows и не могут просматриваться на системах, работающих под управлением Mac OS или Linux. Рис. 27.4. Программы ХВАР загружаются на локальную машину и выполняются внутри веб-браузера Отношения между WPF и Silverlight WPF и XAML также предоставляют фундамент для межплатформенной, межбрау- зерной, основанной на WPF технологии, которая называется Silverlight. На самом высоком уровне Silverlight можно рассматривать скорее как конкурента Adobe Flash, но с преимуществами использования С# и XAML, а не как новый набор инструментов и языков. Silverlight является подмножеством функциональности WPF, используемым для построения интерактивных подключаемых модулей для более крупных HTML-страниц. Однако в действительности Silverlight — это совершенно уникальный дистрибутив платформы .NET, включающий в себя уменьшенные версии среды CLR и библиотек базовых классов .NET. В отличие от ХВАР, устанавливать .NET Framework на машине пользователя не требуется. До тех пор, пока целевая машина имеет установленную исполняющую среду Silverlight, браузер будет загружать ее и отображать приложения Silverlight автоматически. А лучше всего то, что дополнения Silverlight не ограничены операционными системами Windows. В Microsoft также разработали исполняющую среду Silverlight для Mac OS. На заметку! Проект Mono (см. приложение Б) предлагает версию Silverlight с открытым исходным кодом под названием Moonlight, которая предназначена для операционных систем Linux. Этот API-интерфейс в сочетании с Silverlight предоставляет действительно межплатформенную модель для построения исключительно насыщенных веб-страниц.
1004 Часть VI. Построение настольных пользовательских приложений с помощью WPF С помощью Silverlight можно строить исключительно многофункциональные (и интерактивные) веб-приложения. Например, подобно WPF, Silverlight обладает векторной системой графики, поддержкой анимации и поддержкой мультимедиа. Более того, в приложения можно включать подмножество библиотеки базовых классов .NET. Это подмножество содержит набор элементов управления WPF, поддержку LINQ, обобщенные типы коллекций, поддержку веб-служб и полезное подмножество mscorlib.dll (файловый ввод-вывод, манипулирование XML и т.п.). На заметку! В этом издании Silverlight подробно не рассматривается, однако большая часть знаний WPF пригодится при конструировании подключаемых модулей Silverlight. Дополнительные сведения об этом API-интерфейсе доступны по адресу http://silverlight.net. Исследование сборок WPF Независимо от того, какого типа приложение WPF вы собираетесь строить, в конечном итоге WPF — это лишь немногим более чем коллекция типов, встраиваемых в сборки WPF. В табл. 27.3 описаны основные сборки, используемые для построения приложений WPF, ссылка на каждую из которых должна включаться при создании нового проекта (как и следовало ожидать, WPF-проекты в Visual Studio 2010 и Expression Blend автоматически получают ссылки на необходимые сборки). Таблица 27.3. Основные сборки WPF Сборка Назначение PresentationCore.dll Эта сборка определяет многочисленные типы, составляющие фундамент уровня графического интерфейса в WPR Например, она включает поддержку интерфейса WPF Ink API (для программирования перьевого ввода для Pocket PC и Tablet PC), несколько примитивов анимации (через пространство имен System.Windows.Media.Animation) и множество типов визуализации графики PresentationFoundation.dll Эта сборка содержит большинство элементов управления WPF, классы Application и Window, а также поддержку интерактивных двухмерных геометрий. К тому же эта библиотека предоставляет базовую функциональность для чтения и записи документов XAML во время выполнения System.Xaml.dll Эта сборка предоставляет пространства имен, которые позволяют программно взаимодействовать с документами XAML во время выполнения. В основном эта сборка нужна только в том случае, если разрабатываются инструменты поддержки WPF или нужен абсолютный контроль над XAML во время выполнения WindowsBase.dll Эта сборка определяет основные типы, составляющие инфраструктуру API-интерфейса WPF. Здесь находятся типы потоков WPF, типы безопасности, различные преобразователи типов и прочие программные примитивы (описанные в главе 29) Все вместе эти три сборки определяют ряд новых пространств имен и сотни новых классов .NET, интерфейсов, структур, перечислений и делегатов. Хотя за полной информацией следует обращаться к документации по .NET Framework 4.0 SDK, некоторые основные пространства имен описаны в табл. 27.4.
Глава 27. Введение в Windows Presentation Foundation и XAML 1005 Таблица 27.4. Основные пространства имен WPF Пространство имен Назначение System.Windows System.Windows.Controls System. Windows.Data System. Windows. Documents System. Windows. In к System. Windows. Markup System. Windows. Media System.Windows.Navigation System.Windows.Shapes Это корневое пространство имен WPF. Здесь вы найдете основные типы (такие как Application и Window), которые необходимы любому настольному проекту WPF Здесь вы найдете все ожидаемые графические элементы (виджеты) WPF, включая типы для построения систем меню, всплывающих подсказок и многочисленные диспетчеры компоновки Содержит типы для работы с механизмом привязки данных WPF, а также для поддержки шаблонов привязки данных Содержит типы для работы с API-интерфейсом документов, что позволяет интегрировать в WPF-приложения функциональность в стиле PDF через протокол XML Paper Specification (XPS) Поддерживает Ink API — интерфейс, позволяющий получать ввод от пера или мыши, реагировать на жесты (gesture) и т.п. Этот API-интерфейс полезен для программ Tablet PC, однако может использоваться и в любых WPF-приложениях Это пространство имен определяет множество типов, обеспечивающих программный разбор разметки XAML (вместе с эквивалентным двоичным форматом BAML) Это корневое пространство имен для нескольких пространств имен, связанных с мультимедиа. Внутри этих пространств имен определены типы для работы с анимацией, визуализацией трехмерной графики, визуализацией текста и прочие мультимедийные примитивы Это пространство имен предоставляет типы для обеспечения логики навигации, используемой браузерными приложениями XAML (XBAP), а также настольными приложениями на основе страничной навигационной модели Это пространство имен определяет различные типы двухмерной графики, автоматически реагирующей на ввод мыши Чтобы начать путешествие по программной модели WPF, давайте рассмотрим два члена пространства имен System.Windows, которые являются общими для разработки всех настольных приложений: Application и Window. На заметку! Новички в разработке настольных приложений на платформе .NET должны иметь в виду, что сборки System.Windows.Forms.* и System.Drawing.* не связаны с WPF! Эти библиотеки представляют изначальный инструментальный набор для построения графических интерфейсов .NET — Windows Forms (см. приложение А). Роль класса Application Класс System.Windows.Application представляет глобальный экземпляр работающего приложения WPF. В этом типе предусмотрен метод Run() (для запуска приложения), серия событий, которые можно обрабатывать для взаимодействия с приложением на протяжении его времени жизни (вроде Start up HExit), и ряд членов, специфичных для браузерных приложений XAML (таких как события, инициируемые при перемеще-
1006 Часть VI. Построение настольных пользовательских приложений с помощью WPF нии пользователя по страницам). В табл. 27.5 описаны ключевые свойства, о которых нужно знать. Таблица 27.5. Ключевые свойства типа Application Свойство Назначение Current Это статическое свойство позволяет получить доступ к работающему объекту Application из любого места кода. Это может быть очень полезно, когда окну или диалоговому окну нужно получить доступ к объекту Application, который создал их, обычно для взаимодействия с переменными или функциональностью уровня приложения MainWindow Это свойство позволяет программно получать и устанавливать главное окно приложения Properties Это свойство позволяет устанавливать и получать данные, доступные через все аспекты приложения WPF (окна, диалоговые окна и т.п.) StartupUri Это свойство получает или устанавливает URI, который указывает окно или страницу для автоматического открытия при запуске приложения Windows Это свойство возвращает тип WindowCollection, который обеспечивает доступ ко всем окнам, которые созданы в потоке, создавшем объект Application. Это может весьма пригодиться, когда необходимо выполнить итерацию по всем открытым окнам приложения и изменить их состояние (например, свернуть все окна) Конструирование класса Application Любое WPF-приложение нуждается в определении класса, расширяющего Application. Внутри этого класса определяется точка входа программы (метод Main ()), которая создает экземпляр данного подкласса и обычно обрабатывает события Startup и Exit. Чуть ниже мы рассмотрим полный проект, а пока вот быстрый пример: // Определяет глобальный объект приложения для данной программы WPF. class MyApp : Application { [STAThread] static void Main(string[] args) { // Создать объект приложения. MyApp app = new MyApp () ; // Зарегистрировать события Startup/Exit. app. Star tup += (s, e) => { /* Запуск приложения */ }; app.Exit += (s, e) => { /* Завершение приложения */ }; } } Чаще всего в обработчике события Startup будут обрабатываться входные аргументы командной строки, и запускаться главное окно программы. Обработчик Exit, как и следовало ожидать — это место, где помещается необходимая логика завершения программы (сохранение пользовательских предпочтений, запись в реестр Windows и т.п.). Перечисление элементов коллекции Application. Windows Другое интересное свойство класса Application — это Windows, предоставляющее доступ к коллекции, в которой представлены все окна, загруженные в память для текущего WPF-приложения. Запомните, что создаваемые новые объекты Window автомати-
Глава 27. Введение в Windows Presentation Foundation и XAML 1007 чески добавляются в коллекцию Application.Windows. Ниже приведен пример метода, который сворачивает все окна приложения (возможно, в ответ на нажатие определенного сочетания клавиш или выбор пункта меню конечным пользователем): static void MinimizeAllWindows() { foreach (Window wnd in Application.Current.Windows) { wnd.WindowState = WindowState.Minimized; } } В следующем примере мы построим полный тип, унаследованный от Application. А пока давайте рассмотрим основную функциональность типа Window и изучим в процессе ряд важных базовых классов WPF. Роль класса Window Класс System.Windows.Window представляет одиночное окно, принадлежащее типу-наследнику Application, включая все диалоговые окна, отображаемые главным окном. Как и можно было ожидать, тип Window имеет ряд родительских классов, каждый из которых добавляет свою функциональность. На рис. 27.5 показана цепочка наследования (и реализованные интерфейсы) типа System.Windows .Window, как она выглядит в браузере объектов Visual Studio 2010. По мере чтения этой и последующих глав, вы начнете понимать функциональность, предлагаемую многими базовыми классами WPF. В следующем разделе будет предоставлен краткий перечень функциональности, предлагаемой Рис> 27.5. Иерархия наследования типа каждым базовым классом (детали ищите в до- window кументации по .NET Framework 4.0 SDK). Роль класса System.Windows.Controls.ContentControl Непосредственным предком Window является ContentControl — вероятно, наиболее впечатляющий из всех классов WPF. Этот базовый класс обеспечивает производные типы способностью размещать в себе содержимое, которое представлено коллекцией объектов, помещенных на поверхность элемента управления, через свойство Content. Модель содержимого WPF позволяет очень легко настраивать базовый вид и поведение элемента управления Content. Например, когда речь идет о типичном элементе управления "кнопка", то обычно предполагается, что его содержимым будет базовый строковый литерал (OK, Cancel, Abort и т.п.). В случае использования XAML для описания элемента управления WPF, и значение, которое необходимо присвоить свойству Content, может быть выражено в виде простой строки, можете установить свойство Content внутри открывающего определения элемента, как показано ниже: <!-- Явная установка значения Content --> <Button Height="80" Width=00" Content="ClickMe"/> Browse My Solution НШШШшшшяшшя < Search» чтжт *-Q| Base Types л ^ ContentControl л •% Control л ^J FrameworkElement л ~o IFrameworklnputElement ~° DnputElement -0 DnputElement *o IQueryAmbierrt *4> ISupportlmtialize л % UlElement ~° lAnimatable *"° DnputElement л ij Visual л I» DependencyObject л -А% DispatcherObject *% Object *o lAddChild ra|
1008 Часть VI. Построение настольных пользовательских приложений с помощью WPF На заметку! Свойство Content может также устанавливаться в коде С#, что позволяет изменять внутренности элемента управления во время выполнения. Содержимое может быть любым. Например, предположим, что нужна "кнопка", которая содержит в себе нечто более интересное, чем простая строка, возможно, специальную графику или текст. На других платформах построения пользовательских интерфейсов, таких как Windows Forms, пришлось бы строить специальный элемент управления, что потребовало бы написания значительного объема кода и сопровождения нового класса. С моделью содержимого WPF это не требуется. Когда в свойстве Content должно быть установлено значение, которое не может быть выражено простым массивом символов, его нельзя присвоить с использованием атрибута в открывающем определении элемента управления. Вместо этого понадобится определить данные содержимого неявно, внутри контекста элемента. Например, показанный ниже элемент <Button> включает содержимое <StackPanel>, которое само содержит некоторые уникальные данные (а именно — <Ellipse> и <Label>): <•-- Неявная установка в свойстве Content сложных данных --> <Button Height="80" Width=00"> <StackPanel> <Ellipse Fill = ,,Red" Width=,,25" Height=5,,/> <Label Content ="OK|,,/> </StackPanel> </Button> Для установки сложного содержимого можно также использовать синтаксис XAML вида свойство-элемент. Рассмотрим следующий функциональный эквивалент определения <Button>, который устанавливает свойство Content явно с помощью синтаксиса "свойство-элемент" (дополнительную информацию о XAML вы найдете далее в главе): <!— Установка свойства Content с использованием синтаксиса "свойство-элемент" —> <Button Height="80" Width=00"> <Button.Content> <StackPanel> <Ellipse Fill="Red" Width=5" Height=5"/> <Label Content ="OK!"/> </StackPanel> <Button.Content> </Button> Имейте в виду, что не каждый элемент WPF унаследован от ConentConrtol и потому не все элементы поддерживают эту уникальную модель содержимого. К тому же некоторые элементы управления WPF вносят некоторые дополнения к только что рассмотренной модели. В главе 28 роль содержимого WPF рассматривается более подробно. Роль класса System.Windows.Controls.Control В отличие от ContentControl, все элементы управления WPF разделяют базовый класс Control в качестве общего предка. Этот базовый класс предоставляет множество членов, незаменимых для обеспечения функциональности пользовательского интерфейса. Например, Control определяет свойства для установки размеров элемента управления, прозрачности, порядка обхода по нажатию клавиши <ТаЬ>, дисплейного курсора, цвета фона и т.д. Более того, этот родительский класс обеспечивает поддержку шаблонных служб. Как объясняется в главе 31, элементы управления WPF могут полностью изменять свой внешний вид, используя шаблоны и стили. В табл. 27.6 описаны некоторые ключевые члены типа Control, сгруппированные по функциональности.
Глава 27. Введение в Windows Presentation Foundation и XAML 1009 Таблица 27.6. Ключевые члены типа Control Член Назначение Background, Foreground, BorderBrush, Эти свойства позволяют устанавливать базовые BorderThickness, Padding, настройки, касающиеся того, как элемент управ- HorizontalContentAlignment, ления будет визуализирован и позиционирован Vert icalContent Alignment FontFamily, FontSize, FontStretch, Эти свойства управляют настройками шрифтов FontWeight IsTabStop, Tablndex Эти свойства используются для установки порядка обхода элементов управления в окне (по нажатию <ТаЬ>) MouseDoubleClick, Эти события обрабатывают двойной щелчок PreviewMouseDoubleClick навиджете Template Это свойство позволяет получать и устанавливать шаблон элемента, который может быть использован для изменения вывода визуализации виджета Роль класса System.Windows.FrameworkElement Этот базовый класс предоставляет множество низкоуровневых членов, которые используются повсюду в WPF, например, для поддержки раскадровки (для анимации), привязки данных, а также возможности именования членов (через свойство Name), получения ресурсов, определенных производным типом, и установки общих измерений производного типа. Ключевые члены перечислены в табл. 27.7. Таблица 27.7. Ключевые члены типа FrameworkElement Член Назначение ActualHeight, ActualWidth, Управляют размерами производного типа MaxHeight, MaxWidth, MinHeight, MinWidth, Height, Width ContextMenu Cursor HorizontalAlignment, VerticalAlignment Name Resources ToolTip Получает или устанавливает всплывающее меню, ассоциированное с производным типом Получает или устанавливает курсор мыши, ассоциированный с производным типом Управляет позиционированием типа внутри контейнера (такого как панель или окно списка) Позволяет назначать имя типу, чтобы обращаться к его функциональности в файле кода Предоставляет доступ к любому ресурсу, определенному типом (система ресурсов WPF объясняется в главе 30) Получает или устанавливает всплывающую подсказку, ассоциированную с производным типом
1010 Часть VI. Построение настольных пользовательских приложений с помощью WPF Роль класса System.Windows.UIElement Из всех типов в цепочке наследования Window базовый класс UIElement предлагает максимум функциональности. Его ключевая задача — обеспечивать производный тип многочисленными событиями, чтобы он мог принимать фокус и обрабатывать входящие запросы. Например, в этом классе предусмотрены многочисленные события для обслуживания операций перетаскивания, перемещений курсора мыши, клавиатурного ввода и ввода с помощью пера (для Pocket PC и Tkblet PC). Модель событий WPF будет подробно описана в главе 29; однако многие из основных событий покажутся знакомыми (MouseMove, KeyUp, MouseDown, MouseEnter, MouseLeave и т.п.). В дополнение к определению десятков событий, этот родительский класс предоставляет множество свойств для управления фокусом, состоянием доступности, видимостью и логикой проверки попаданий (табл. 27.8). Таблица 27.8. Ключевые члены типа UIElement Член Назначение Focusable, IsFocused Позволяют устанавливать фокус на определенный производный тип isEnabled Позволяет управлять доступностью определенного производного типа IsMouseDirectlyOver, Предлагают простой способ выполнения логики проверки попадания IsMouseOver IsVisible, Visibility Позволяют работать с установкой видимости производного типа RenderTransf orm Позволяет устанавливать трансформацию, которая будет использована для визуализации производного типа Роль класса Sys tem. Windows. Media .Visual Класс Visual предлагает основную поддержку визуализации в WPF, включая проверку попадания на визуализированные данные, трансформацию координат и вычисления границ. Фактически для рисования данных на экране класс Visual взаимодействует с подсистемой DirectX. Как объясняется в главе 29, WPF поддерживает три возможных способа визуализации графических данных, каждый из которых отличается от других в отношении функциональности и производительности. Применение типа Visual (и его потомков вроде DrawingVisual) обеспечивает наиболее легковесный способ визуализации графических данных, но также подразумевает участие большого объема управляемого кода для обеспечения работы всех необходимых служб. Более подробно об этом речь пойдет в главе 29. Роль класса Sys tem. Windows. DependencyObj ec t WPF поддерживает специальную разновидность свойств .NET, именуемую свойствами зависимости (dependency properties). Этот подход позволяет типу вычислять значение свойства на основе значений других свойств (отсюда и "зависимость"). Чтобы тип участвовал в этой новой схеме, он должен быть унаследован от базового класса DependencyObject. Вдобавок DependencyObject позволяет типам-наследникам поддерживать присоединяемые свойства (attached properties), которые представляют собо^ форму свойств зависимости, очень удобную при программировании в модели привязки данных WPF, а также при установке элементов пользовательского интерфейса внутри различных типов панелей WPF.
Глава 27. Введение в Windows Presentation Foundation и XAML 1011 Базовый класс Dependency Object предоставляет два ключевых метода для всех производных типов: GetValueO и SetValue(). С помощью этих членов можно устанавливать само свойство. Другие части инфраструктуры позволяют "регистрировать" тех, кто может использовать свойства зависимости или присоединяемые свойства. Хотя свойства зависимости — это ключевой аспект разработки WPF, большую часть времени их детали скрыты от глаз. В главе 29 содержатся дополнительные подробности об этом "новом" типе свойств. Роль класса System.Windows.Threading.DispatcherObject И последний базовый класс типа Window (помимо System.Object, который здесь не требует дополнительных пояснений) — DispatherObject. Этот тип включает одно свойство, представляющее интерес — Dispatcher, которое возвращает ассоциированный объект System.Windows.Threading.Dispatcher. Класс Dispatcher — это точка входа в очередь событий приложения WPF, предоставляющая базовые конструкции для работы с параллелизмом и многопоточностью. По большому счету, это низкоуровневый класс, который в большинстве приложений WPF может быть проигнорирован. Построение приложения WPF без XAML Учитывая всю функциональность, предлагаемую родительскими классами типа Window, представить окно в приложении можно, либо напрямую создав объект Window, либо указав этот класс в качестве родительского для строго типизированного наследника. В следующем примере рассматриваются оба подхода. Хотя большинство приложений WPF используют XAML, так поступать вовсе не обязательно. Все, что может быть выражено на XAML, также можно выразить в коде и (по большей части) наоборот. При желании можно построить полный проект WPF, используя лежащую в основе объектную модель и процедурный код. Чтобы проиллюстрировать сказанное, давайте построим минимальное, но полное приложение без применения XAML, работая с классами Application и Window напрямую. Создайте новое консольное приложение по имени Wpf AppAllCode. Откройте диалоговое окно Add Reference (Добавление ссылки) и добавьте ссылку на сборки WindowBase.dll, PresentationCore.dll, System.Xaml.dll и PresentationFramework.dll. Поместите в начальный файл С# следующий код, который создает главное окно с минимальной функциональностью: // Простое приложение WPF, написанное без XAML. using System; using System.Windows; using System.Windows.Controls; namespace WpfAppAllCode { // В этом первом примере определяется один класс для // представления самого приложения и главного окна. class Program : Application { [STAThread] static void Hain (string [ ] arqzs) { // Обработка событий Startup и Exit с последующим запуском приложения. Program app = new Program(); арр.Startup += AppStartUp; app. Exit i-= AppExit; app.Run(); // Инициирует событие Startup. }
1012 Часть VI. Построение настольных пользовательских приложений с помощью WPF static void AppExit(object sender, ExitEventArgs e) { MessageBox.Show ("App has exited"); } static void AppStartUp(object sender, StartupEventArgs e) { // Создать объект Window и установить некоторые базовые свойства. Window mainWindow = new Window(); mainWindow.Title = "My First WPF App!"; mainWindow.Height = 200; mainWindow.Width = 300; mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen; mainWindow.Show(); } } } На заметку! Метод Main() в приложении WPF должен быть снабжен атрибутом [STAThread], что гарантирует безопасность к потокам унаследованных СОМ-объектов, используемых приложением. Если не аннотировать Main() подобным образом, возникнет исключение времени выполнения. Обратите внимание, что класс Program расширяет класс System.Windows. Application. Внутри метода Main() создается экземпляр объекта приложения, затем обрабатываются события Startup и Exit с помощью синтаксиса группового преобразования методов. Вспомните из главы 11, что эта сокращенная нотация исключает необходимость вручную указывать делегаты, используемые определенным событием. Разумеется, при желании можно указывать лежащие в основе делегаты прямо по имени. В следующем модифицированном методе Main() обратите внимание, что событие Startup работает в сочетании с делегатом StartupEventHandler, который может указывать только на методы, принимающие Object в качестве первого параметра и StartupEventArgs — в качестве второго. Событие Exit, с другой стороны, работает с делегатом ExitEventHandler, который требует, чтобы указанный им метод принимал тип ExitEventArgs во втором параметре: [STAThread] static void Main(string[] args) { //На этот раз специфицируем лежащие в основе делегаты. MyWPFApp app = new MyWPFApp(); app.Startup += new StartupEventHandler(AppStartUp); app.Exit += new ExitEventHandler(AppExit); app.Run(); // Инициирует событие Startup. } В любом случае метод AppStartUp () сконфигурирован для создания объекта Window, выполнения некоторых установок базовых свойств и вызова Show() для отображения окна на экране в немодальном режиме (метод ShowDialogO может применяться для открытия модального диалогового окна). Метод AppExit() просто использует класс MessageBox из WPF для отображения диагностического сообщения при завершении приложения. После компиляции и запуска проекта вы обнаружите очень простое главное окно, которое может быть свернуто, развернуто и закрыто. Чтобы немного украсить его, понадобится добавить некоторые элементы пользовательского интерфейса. Но прежде чем сделать это, давайте переработаем код с использованием строго типизированного и хорошо инкапсулированного класса-наследника Window.
Глава 27. Введение в Windows Presentation Foundation и XAML 1013 Создание строго типизированного окна Сейчас класс-наследник Application при запуске приложения напрямую создает экземпляр типа Window. В идеале нужно было бы создать класс, унаследованный от Window, чтобы инкапсулировать его внешний вид и функциональность. Предположим, что создано следующее определение класса в текущем пространстве имен Wpf AppAllCode (если этот класс помещен в новый файл С#, необходимо импортировать пространство имен System.Windows): class MainWindow : Window { public MainWindow(string windowTitle, int height, int width) { this.Title = windowTitle; this.WindowStartupLocation = WindowStartupLocation.CenterScreen; this.Height = height; this.Width = width; } } Теперь можно модифицировать обработчик событий Startup для прямого создания экземпляра MainWindow: static void AppStartUp(object sender, StartupEventArgs e) { // Создать объект MainWindow. MainWindow wnd = new MainWindow ("My better WPF App! ", 200, 300); wnd.Show(); } После компиляции и запуска программы получается вывод, идентичный предыдущей версии. Очевидное преимущество состоит в том, что теперь есть строго типизированный класс окна для построения. На заметку! Когда создается объект window (или производный от window), он автоматически добавляется к внутренней коллекции окон класса Application (через некоторую логику конструктора, находящуюся в самом классе Window). С помощью свойства Application.Windows можно проходить по списку объектов Window, находящихся в данный момент в памяти. Создание простого пользовательского интерфейса Добавление элементов пользовательского интерфейса к Window включает выполнение перечисленных ниже базовых шагов. 1. Определить переменную-член для представления нужного элемента управления. 2. Сконфигурировать внешний вид и поведение элемента управления при конструировании объекта Window. 3. Присвоить элемент управления унаследованному свойству Content или, в качестве альтернативы — передать его в параметре унаследованному методу AddChild(). Вспомните, что модель содержимого элемента управления WPF требует, чтобы свойство Content устанавливалось только один раз. Разумеется, окно с единственным элементом управления было бы не особенно бесполезным. Поэтому почти в каждом случае "единственной порцией содержимого", которая присваивается свойству Content, на самом деле является диспетчер компоновки, такой как DockPanel, Grid, Canvas или StackPanel. Внутри диспетчера компонозки можно иметь любую комбинацию внутрен-
1014 Часть VI. Построение настольных пользовательских приложений с помощью WPF них элементов, в том числе другие вложенные диспетчеры компоновки (более подробно этот аспект разработки WPF описан в главе 28). Пока что добавим единственный объект типа Button к объекту-наследнику Window. В результате щелчка на этой кнопке текущее окно будет закрываться, что неявно прервет приложение, поскольку других окон в памяти не существует. Посмотрите на следующую модификацию класса MainWindow (не забудьте импортировать System. Windows.Controls для получения доступа к классу Button): class MainWindow : Window { // Наш элемент пользовательского интерфейса. private Button btnExitApp = new Button(); public MainWindow(string windowTitle, int height, int width) { // Сконфигурировать кнопку и установить как дочерний элемент управления. btnExitApp.Click += new RoutedEventHandler(btnExitApp_Clicked) ; btnExitApp.Content = "Exit Application"; btnExitApp.Height = 25; btnExitApp.Width = 100; // Установить в качестве содержимого окна единственную кнопку. this.AddChild(btnExitApp); // Сконфигурировать окно. this.Title = windowTitle; this.WindowStartupLocation = WindowStartupLocation.CenterScreen; this.Height = height; this.Width = width; this.Show (); } private void btnExitApp_Clicked(object sender, RoutedEventArgs e) { // Закрыть окно. this.Close (); } } Однако обратите внимание, что событие Click кнопки WPF работает в сочетании с делегатом по имени RoutedEventHandler. Это вызывает очевидный вопрос: что такое маршрутизируемое событие? Модели событий WPF подробно рассматриваются в следующей главе, а пока просто учтите, что цели делегата RoutedEventHandler должны принимать Object в качестве первого параметра и RoutedEventArgs — в качестве второго. После компиляции и запуска приложения отображается измененное окно, показанное на рис. 27.6. Кнопка автоматически помещена в центр клиентской области окна, что является поведением по умолчанию, когда содержимое не помещено в тип панели WPF. Рис. 27.6. Простое WPF-приложение, написанное полностью на С#
Глава 27. Введение в Windows Presentation Foundation и XAML 1015 Взаимодействие с данными уровня приложения Вспомните, что в классе Application имеется свойство по имени Properties, которое позволяет определить коллекцию пар "имя/значение" через индексатор типа. Поскольку этот индексатор определен для операций над типом System.Object, в коллекции можно сохранять элементы любого рода (включая экземпляры пользовательских классов) для последующего извлечения по дружественному имени. Используя этот подход, можно очень просто разделить данные среди всех окон в приложении WPF. Для целей иллюстрации модифицируем текущий обработчик событий, чтобы он проверял входящие параметры командной строки на присутствие значения /GODMODE (распространенный "мошеннический" код для многих игр). Если эта лексема найдена, внутри коллекции свойств значение bool под именем GodMode устанавливается в true (в противном случае — в false). Звучит достаточно просто, тем не менее, может возникнуть один вопрос: как передать обработчику события Startup входной аргумент командной строки (обычно получаемый методом Main ())? Один из подходов предусматривает вызов статического метода Environment.GetCommandLineArgs(). Однако те же самые аргументы автоматически добавляются во входной параметр StartupEventArgs и доступны через свойство Args. С учетом этого, ниже показано первое обновление текущей кодовой базы: static void AppStartUp (object sender, StartupEventArgs e) { // Проверить входящие аргументы командной строки //на предмет наличия флага /GODMODE. Application.Current.Properties["GodMode"] = false; foreach(string arg in e.Args) { if (arg.ToLower() == "/godmode") { Application.Current.Properties["GodMode"] = true; break; } } // Создать объект MainWindow. MainWindow wnd = new MainWindc vi ("Ily better WPF App! ", 200, 300) ; } Данные уровня приложения доступны везде внутри WPF-приложения. Все, что нужно для этого сделать — получить точку доступа к глобальному объекту приложения (через Application. Си г rent) и исследовать коллекцию. Например, обработчик событий Click для Button можно было бы изменить следующим образом: private void btnExitApp_Clicked(object sender, RoutedEventArgs e) { // Включил ли пользователь /godmode? if ( (bool)Application.Current.Properties["GodMode"]) MessageBox.Show("Cheater'"); this.Close(); } Если теперь конечный пользователь запустит программу следующим образом: WpfAppAllCode.exe /godmode то получит окно сообщения, отображаемое по завершению приложения.
1016 Часть VI. Построение настольных пользовательских приложений с помощью WPF На заметку! Вспомните, что аргументы командной строки можно указывать и в Visual Studio Для этого щелкните на значке Properties (Свойства) в окне Solution Explorer, перейдите на вкладку Debug (Отладка) и введите /godmode в поле Command line arguments (Аргумента командной строки). Обработка закрытия объекта Window Конечные пользователи могут завершить работу окна, применяя для этого многочисленные встроенные средства уровня системы (например, щелкнув на кнопке закрытия X на рамке окна) или непосредственно вызвав метод Close () в ответ на некоторое действие пользователя с интерактивным элементом (например, File^Exit (Файло Выход)). В любом случае WPF предлагает два события, которые можно перехватить для определения того, действительно ли пользователь намерен остановить работу окна и удалить его из памяти. Первое такое событие — это Closing, которое работает в сочетании с делегатом CancelEventHandler. Этот делегат ожидает целевой метод, принимающий System.Component Model. CancelEventArgs во втором параметре. CancelEventArgs предоставляет свойство Cancel, которое, будучи установленным в true, предотвратит действительное закрытие окна (это удобно, когда необходимо спросить пользователя, действительно ли он хочет закрыть окно или, возможно, сначала следует сохранить свою работу). Если пользователь действительно желает закрыть окно, то CancelEventArgs .Cancel можно установить в false, что затем приведет к выдаче события Closed (работающего с делегатом System. Event Handle г), представляющего собой точку, в которой окно полностью и безвозвратно готово к закрытию. Давайте модифицируем класс MainWindow для обработки этих двух событий, добавив следующие операторы кода к текущему конструктору: public MainWindow(string windowTitle, int height, int width) { this.Closing += MainWindow_Closing; this.Closed += MainWindow_Closed; } Теперь реализуем обработчики событий, как показано ниже: private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) { // Проверить, действительно ли пользователь желает закрыть окно. string msg = "Do you want to close without saving?"; MessageBoxResult result = MessageBox.Show(msg, "MyApp", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result == MessageBoxResult.No) { // Если не хочет, отменить закрытие. , е.Cancel = true; } } private void MainWindow_Closed(object sender, EventArgs e) { MessageBox.Show("See ya!");
Глава 27. Введение в Windows Presentation Foundation и XAML 1017 Рис. 27.7. Перехват события Closing объекта Window Запустите программу и попытайтесь закрыть окно, щелкнув либо на значке X в правом верхнем углу окна, либо на кнопке. Должно открыться показанное на рис. 27 7 диалоговое окно с запросом подтверждения. Щелчок на кнопке Yes (Да) приведет к завершению приложения, а щелчок на кнопке No (Нет) оставит окно в памяти. Перехват событий мыши API-интерфейс WPF предоставляет множество событий, которые можно перехватывать для организации взаимодействия с мышью. В частности, в базовом классе UIElement определены такие события мыши, как MouseMove, MouseUp, MouseDown, MouseEnter, MouseLeave и т.д. Рассмотрим, например, обработку события MouseMove. Это событие работает в сочетании с делегатом System.Windows.Input.MouseEventHandler, который ожидает, что его целевая функция будет принимать тип System.Windows.Input.MouseEventArgs во втором параметре. Используя MouseEventArgs (как в приложении Windows Forms), можно извлечь координаты позиции (х, у) курсора мыши и прочие связанные с ним детали. Рассмотрим следующее частичное определение: public class MouseEventArgs : InputEventArgs public Point GetPosition(IlnputElement relativeTo); public MouseButtonState LeftButton { get; } public MouseButtonState MiddleButton { get; } public MouseDevice MouseDevice { get; } public MouseButtonState RightButton { get; } public GtylusDevice StylusDevice { get; } public MouseButtonState XButtonl { get; } public MouseButtonState XButton2 { get; } } На заметку! Свойства XButtonl и XButton2 позволяют взаимодействовать с "расширенными кнопками мыши" (такими как кнопки "вперед" и "назад", которые имеются в некоторых устройствах) Они часто используются для взаимодействия с хронологией навигации в браузере для переходов между посещенными страницами. Метод GetPosition() позволяет получать значение (х, у) относительно элемента пользовательского интерфейса в окне. Если вы заинтересованы в захвате позиции относительно активного окна, просто передайте this. Обработаем событие MouseMove в конструкторе класса MainWindow следующим образом: public MainWindow(string windowTitle, int height, int width) { this.MouseMove += MainWindow_MouseMove; } Ниже приведен обработчик события MouseMove, который отобразит местоположение мыши в области заголовка окна (обратите внимание, что возвращенный тип Point транслируется в строковое значение с помощью ToStnngO):
1018 Часть VI. Построение настольных пользовательских приложений с помощью WPF protected void MainWindow_MouseMove (object sender, System.Windows.Input.MouseEventArgs e) { // Установить в заголовке окна текущие координаты X,Y мыши. this.Title = e.GetPosition(this) .ToStringO ; } Перехват клавиатурных событий Обработка клавиатурного ввода также очень проста. UIElement определяет ряд событий, которые можно перехватывать для отслеживания нажатий клавиш клавиатуры на активном элементе (например, KeyUp, KeyDown). Оба события — KeyUp и KeyDown — работают с делегатом System.Windows.Input.KeyEventHandler, который ожидает второго параметра типа KeyEventArgs, определяющего несколько важных общедоступных свойств: public class KeyEventArgs : KeyboardEventArgs public bool IsDown { get; } public bool IsRepeat { get; } public bool IsToggled { get; } public bpol IsUp { get; } public Key Key { get; } public KeyStates KeyStates { get; } public Key SystemKey { get; } } Чтобы проиллюстрировать обработку события KeyDown в конструкторе MainWindow (как это сделано для предыдущего события) и реализации обработчика события, который изменяет содержимое кнопки текущей нажатой клавишей, используйте следующий код: private void MainWindow_KeyDown ( object sender, System.Windows.Input.KeyEventArgs e) { // Отобразить на кнопке нажатую клавишу. btnExitApp.Content = е.Key.ToString (); В качестве последнего штриха дважды щелкните на значке Properties (Свойства) в окне Solution Explorer и на вкладке Application (Приложение) установите для Output Type (Тип вывода) в Windows Application (Windows- приложение). На рис. 27.8 показан конечный результат работы первой WPF-программы. К этому моменту WPF может показаться не более чем еще одной платформой для построения графических Рис 27 8 Пепвая WPF- пользовательских интерфейсов, которая обеспечивает программа, написанная без (почти) те же самые службы, что и Windows Forms, MFC использования XAML и VB6. Заодно возникает вопрос: зачем нужен еще один инструментальный набор для создания пользовательских интерфейсов. Чтобы оценить уникальность WPF, потребуется освоить основанную на XML грамматику — XAML. -В аш п* ! UftShift - i^^^^.p Исходный код. Проект Wpf AppAllCode доступен в подкаталоге Chapter 27.
Глава 27. Введение в Windows Presentation Foundation и XAML 1019 Построение приложения WPF с использованием только XAML Типичное WPF-приложение не состоит исключительно из кода, как в первом примере. Файлы кода С# дополняются связанным исходным файлом XAML, и вместе они представляют сущность конкретного Window или Application, а также других типов классов, которые пока не рассматривались, вроде UserControl и Page. Это называется подходом файла кода к построению WPF-приложения, и именно он будет интенсивно использоваться при рассмотрении WPF в остальной части книги. Однако прежде чем двигаться дальше, в следующем примере мы рассмотрим создание WPF-приложения на основе только файлов XAML. Хотя этот подход применять не рекомендуется, он поможет лучше понять, каким образом блоки разметки XAML трансформируются в кодовую базу С# и, в конечном итоге, в сборку .NET. На заметку! В следующем примере используются приемы XAML, которые пока еще не рассматривались. Можете просто загрузить файлы решения в текстовый редактор и проследить код строку за строкой; однако не используйте для этого среду Visual Studio 2010! Некоторая часть представленной здесь разметки не будет отображаться в визуальных конструкторах XAML этой среды. В общем случае файлы XAML будут содержать разметку, описывающую внешний вид и поведение окна, а связанные файлы кода С# — логику реализации. Например, файл XAML для объекта Window может описывать общую систему разметки, элементы управления внутри системы разметки и указывать имена различных обработчиков событий. Связанный файл С# должен содержать логику реализации этих обработчиков событий и любой специальный код, необходимый приложению. Расширяемый язык разметки приложений (Extensible Application Markup Language — XAML) — это основанная на XML грамматика, позволяющая определять состояние (и до некоторой степени функциональность) дерева объектов .NET через разметку. Хотя XAML часто применяется при построении пользовательских интерфейсов с WPF, на самом деле его можно использовать для описания любого дерева неабстрактных типов .NET (включая разработанные вами типы, определенные в отдельной сборке .NET), при условии, что каждый из них имеет конструктор по умолчанию. Как вы вскоре убедитесь, разметка, находящаяся в файле *.xaml, трансформируется в полноценную объектную модель. Поскольку грамматика XAML основана на XML, мы получаем вместе с ним все преимущества и недостатки XML. Положительная сторона XAML заключается в том, что этому языку присущ самоописательный характер (как любому документу XML). По большому счету каждый элемент в файле XAML представляет имя типа (такое как Button, Window или Application) в рамках заданного пространства имен .NET Атрибуты в пределах контекста открытия элемента отображаются на свойства (Height, Width, и т.п.) и события (Startup, Click и т.д.) указанного типа. Учитывая тот факт, что XAML является просто декларативным способом определения состояния объекта, виджет WPF можно определить через разметку либо в процедурном коде. Например, следующий код XAML: <!-- Определение WPF Button в XAML --> <Button Name = "btnClickMe" Height = 0м Width = 00" Content = "Click Me" /> может быть представлен программно так: // Определение того же элемента WPF Button в коде С#. Button btnClickMe = new Button (); btnClickMe.Height = 40; btnClickMe.Width = 100; btnClickMe.Content = "Click Me";
1020 Часть VI. Построение настольных пользовательских приложений с помощью WPF Отрицательным моментом является то, что XAML может быть довольно многословным и (как любой документ XML) зависимым от регистра, а потому сложные определения XAML влекут собой немалую работу с разметкой. Большинству разработчиков не приходится вручную создавать полные XAML-описания WPF-приложений. Большая часть задачи возлагается на инструменты разработки, такие как Visual Studio 2010, Microsoft Expression Blend или другие продукты от независимых поставщиков. Как только инструмент сгенерировал базовую разметку XAML, при необходимости можно провести ее тонкую настройку вручную. Определение MainWindow в XAML Хотя инструменты могут генерировать приемлемый код XAML, все же важно понимать основы синтаксиса XAML и то, как разметка в конечном итоге трансформируется в корректную сборку .NET. Чтобы проиллюстрировать XAML в действии, в следующем примере мы построим полноценное приложение WPF всего из пары файлов *.xaml. Первый класс-наследник Window (MainWindow) был определен в С# как тип класса, расширяющего базовый класс System.Windows.Window. Этот класс содержит единственный объект Button, который вызывает зарегистрированный обработчик события по щелчку. Определение того же типа Window с применением грамматики XAML может выглядеть следующим образом (предполагается, что эта разметка содержится в файле MainWindow. xaml): <!-- Определение класса Window --> <Window x:Class="SimpleXamlApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="A Window built using 100% XAML" Height=00" Width=00" WindowStartupLocation ="CenterScreen"> <!-- Это окно имеет в качестве содержимого единственную кнопку --> <Button x:Name="btnExitApp" Width=33" Height=4" Content = "Close Window" Click ="btnExitApp_Clicked"/> <!-- Реализация обработчика события кнопки Click --> <x:Code> <■[CDATA[ private void btnExitApp_Clicked (object sender, RoutedEventArgs e) { this.Close (); } ]]> </x:Code> </Window> Прежде всего, обратите внимание, что корневой элемент <Window> использует атрибут Class для указания имени класса, который будет сгенерирован при обработке этого файла XAML. Кроме того, атрибут Class снабжен префиксом х:. Заглянув в открывающий элемент <Window>, вы увидите, что этому префиксу дескриптора XML присваивается строка "http://schemas.microsoft.com/winfx/2006/xaml" для построения объявления пространства имен XML. Детали этого определения пространства имен XML станут ясны чуть позже в этой главе, а пока просто имейте в виду, что всякий раз, когда хотите сослаться на элемент, определенный в пространстве имен XAML http://schemas. microsoft.com/winfx/2006/xaml, вы должны указывать префикс — лексему х:. В контексте открывающего дескриптора <Window> заданы значения для атрибутов Title, Height, Width и WindowStartupLocation, которые напрямую отображаются на одноименные свойства, поддерживаемые типом System.Windows.Window из сборки PresentationFramework.dll.
Глава 27. Введение в Windows Presentation Foundation и XAML 1021 Далее обратите внимание, что в контексте определения окна находится разметка, описывающая внешний вид и поведение экземпляра Button, который будет использован для неявной установки свойства Content окна. Помимо установки имени переменной (с применением x:Name) и его общих размеров, мы также обрабатываем событие Click типа Button, присвоив метод делегату, вызываемому при возникновении события Click. Финальный аспект файла XAML — элемент <x:Code>, который позволяет определять обработчики событий и прочие методы этого класса непосредственно внутри файла *.xaml. В качестве меры безопасности сам код помещен в контекст CDATA, чтобы предотвратить попытки анализатора XML напрямую интерпретировать данные (хотя в данном примере это не обязательно). Важно отметить, что использовать специальную функциональность внутри элемента <x:Code> не рекомендуется. Хотя подход на основе одного файла изолирует все действия в одном месте, все же встроенный код не обеспечивает ясного отделения разметки пользовательского интерфейса от программной логики. В большинстве приложений WPF код реализации находится в связанном файле С# (как и мы поступим в конечном итоге). Определение объекта Application в XAML Вспомните, что XAML может применяться для определения разметки любого неабстрактного класса .NET, поддерживающего конструктор по умолчанию. Исходя из этого, можно также определить объект приложения в разметке. Рассмотрим следующее содержимое нового файла MyApp.xaml: <!— Похоже, отсутствует метод Main()! Однако атрибут StartupUri является его Функциональным эквивалентом --> <Application x:Class=MSimpleXamlApp.MyAppM xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xamlll> </Application> Как видите, здесь отображение между классом-наследником Application и его описанием XAML не столь очевидно, как в случае с XAML-определением MainWindow. В частности, не видно никаких следов метода Main(). Учитывая, что каждая исполняемая программа .NET должна иметь точку входа, вы не ошибетесь, если предположите, что она будет сгенерирована во время компиляции на основе части свойства StartupUri. Значение, присвоенное StartupUri, представляет ресурс XAML, отображаемый при запуске приложения. Хотя метод Main() автоматически создается во время компиляции, при желании можно использовать элемент <x:Code> для определения других блоков кода С#. Например, чтобы вывести сообщение при завершении программы, необходимо реализовать обработчик события Exit, как показано ниже: <Application x:Class="SimpleXamlApp.MyApp11 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml11 Exit ="AppExit"> <x:Code> <! [CDATA[ private void AppExit(object sender, ExitEventArgs e) { MessageBox . Show ("App has exited11); } ]]> </x:Code> </Application>
1022 Часть VI. Построение настольных пользовательских приложений с помощью WPF Обработка файлов XAML с помощью msbuild.exe Все готово к трансформации разметки в корректную сборку .NET. Однако использовать для этого напрямую компилятор С# не удастся. На данный момент компилятор С# не имеет возможность распознавать разметку XAML. Однако утилита командной строки msbuild.exe знает, как трансформировать XAML в код С#, и компилирует этот код на лету, когда она информирована о правильных целевых файлах *.targets. msbuild.exe — это инструмент, который скомпилирует код .NET на основе инструкций, содержащихся в основанном на XML сценарии сборки. В свою очередь, этот сценарий сборки содержит те же данные, что находятся в файле *.csproj, сгенерированном Visual Studio. Поэтому можно компилировать программу .NET в командной строке с помощью msbuild.exe или же применять саму среду Visual Studio 2010. На заметку! Полное описание утилиты msbuild.exe выходит за рамки настоящей главы. Исчерпывающие сведения о ней можно найти в разделе "MSBuild" документации .NET Framework 4.0 SDK. Ниже приведен очень простой сценарий SimpleXamlApp.csproj, содержащий достаточно информации для объяснения msbuild.exe, как следует трансформировать файлы XAML в соответствующую кодовую базу С#: <Project DefaultTargets=MBuildM xmlns="http://schemas.microsoft.com/develeper/msbuild/2003"> <PropertyGroup> <RootNamespace>SimpleXamlApp</RootNamLLpiCL> <AssemblyName>SimpleXamlApp</AssemblyMame> <OutputType>winexe</OutputType> </PropertyGroup> <ItemGroup> <Reference Include=llSystem" /> <Reference Include=llWindowsBaseM /> <Reference Include="PresentationCore11 h <Reference Include="PresentationFramework11 /> </ItemGroup> <ItemGroup> <ApplicationDefinition Include="MyApp.xaml" /> <Page Include=llMainWindow.xaml" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" /> </Project> На заметку! Этот файл *.csproj не может быть загружен непосредственно в Visual Studio 2010, поскольку он содержит минимальный набор инструкций, необходимых для построения приложения в командной строке. Элемент <PropertyGroup> используется для спецификации некоторых базовых аспектов сборки, таких как корневое пространство имен, имя результирующей сборки и тип вывода (эквивалент опции /targetiwinexe программы csc.exe). Первый элемент <ItemGroup> указывает набор внешних сборок для ссылки из текущей сборки, которыми, как видно, являются основные сборки WPF, упомянутые ранее в этой главе. Второй элемент <ItemGroup> более интересен. Обратите внимание, что атрибуту Include элемента <ApplicationDefinition> присвоен файл *.xaml, определяющий объект приложения. Атрибут Include элемента <Раде> может использоваться для пере-
Глава 27. Введение в Windows Presentation Foundation и XAML 1023 числения всех остальных файлов *.xaml, в которых определены окна (и страницы, что часто применяется при построении браузерных приложений XAML), обрабатываемые объектом Application. Однако "магия" этого сценария сборки кроется в элементах <Import>. Здесь производится ссылка на два файла *.targets, каждый из которых содержит множество других инструкций, используемых во время процесса сборки. В файле Microsoft.WinFX.targets определены необходимые настройки для трансформации определений XAML в эквивалентные файлы кода С#, а в файле Microsoft. CSharp.targets содержатся данные для взаимодействия с самим компилятором С#. Теперь можно открыть окно командной строки Visual Studio 2010 и обработать данные XAML с помощью msbuild.exe. Для этого перейдите в каталог, в котором находятся файлы MainWindow.xaml, MyApp.xaml и SimpleXamlApp.csproj, и введите следующую команду: msbuild SimpleXamlApp.csproj После завершения процесса сборки в рабочем каталоге обнаружится подкаталог \bin\obj (как в проекте Visual Studio). В папке \bin\Debug находится новая сборка .NET по имени SimpleXamlApp.exe. Открыв эту сборку в ildasm.exe, вы увидите, что разметка XAML была трансформирована в исполняемое приложение (рис. 27.9.). /7 H:\My Books\C# Вос*\С# and the .NET Platform 5th ed\F.rst Dr_mChapter_28\Code. Wrf^yfcMMl File View Help ► MANIFEST Щ SirnpleXamlApp SimpleXaml App. Main Window ► .class public auto ansi beforeheldinit ► extends [PresentationFramework]System. Windows. Window ► implements [ WindowsBase]System. Windows. Markup. IComponentConnector v _contentLoaded : private bool V btnExitApp : assembly class [Presertatior^ramewwk]System.Windows.Controls.Button ■ .ctor:void() ■ InitializeComponent: voidO ■ System^Windows.Markup.IComponentConnector.Connect: void(int32,object) ■ btnExitApp_Clicked : void(object,class [PresentationCore]System.Windows.RoutedEventArgs) SirnpleXamlApp. My App ► .class public auto ansi beforefieldinit ► extends [PresentationFramework]Sy stem. Windows. Application ■ .ctor: void() ■ AppExit: void(object, class [Present _tionfr_mework]System. Windows. ExitE vent Args) ■ InitializeComponent: voidO Q Main: void() . . '" . .assembly SirnpleXamlApp Рис. 27.9. Трансформация разметки XAML в сборку .NET Запустив программу двойным щелчком на исполняемом файле, вы увидите на экране ее главное окно. Трансформация разметки в сборку .NET Чтобы полностью понять, каким образом разметка превратилась в сборку .NET, нужно немного углубиться в процесс msbuild.exe и изучить ряд сгенерированных компилятором файлов, включая определенный двоичный ресурс, встроенный в сборку во время компиляции. Первой задачей будет узнать, как файлы *.xaml трансформируются в кодовую базу С#.
1024 Часть VI. Построение настольных пользовательских приложений с помощью WPF Отображение XAML-данных окна на код С# Файлы *.targets, указанные в сценарии для msbuild.exe, содержат множество инструкций трансляции элементов XAML в код С#. Когда msbuild.exe обрабатывает файл *.csproj, создаются два файла *.g.cs ("g" означает автоматически сгенерированные), которые сохраняются в каталоге \obj\Debug. На основе имен файлов *.xaml полученные файлы С# получают названия MainWindow.g.cs и MyApp.g.cs. Открыв файл MainWindow.g.cs в текстовом редакторе, вы найдете там класс по имени MainWindow, расширяющий базовый класс Window. Имя этого класса — прямой результат действия атрибута х:Class открывающего дескриптора <Window>. Кроме того, в этом классе определена переменная-член типа System.Windows.Controls.Button с именем btnExitApp. В данном случае имя элемента управления основано на значении атрибута x:Name открывающего объявления <Button>. Этот класс также содержит обработчик события Click кнопки — btnExitApp_Clicked(). Ниже приведена часть листинга этого сгенерированного компилятором файла: public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector { internal System.Windows.Controls.Button btnExitApp; private void btnExitApp_Clicked(object sender, RoutedEventArgs e) { this.Close (); } } В классе определена приватная переменная-член типа bool (по имени named_ с on tent Loaded), которая не была напрямую представлена в разметке XAML. Этот член данных используется для того, чтобы гарантировать присваивание содержимого окна только один раз: public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector { internal System.Windows.Controls.Button btnExitApp; // Эту переменную-член мы поясним ниже. private bool _contentLoaded; } Обратите внимание, что сгенерированный компилятором класс также явно реализует WPF-интерфейс IComponentConnector, определенный в пространстве имен System. Windows.Markup. В этом интерфейсе определен единственный метод Connect(), который реализован для подготовки каждого элемента управления, определенного в разметке, и обеспечения логики событий, как указано в исходном файле MainWindow.xaml. Перед завершением этого метода переменная contentLoaded устанавливается в true. Вот как выглядит этот метод: void System.Windows.Markup.IComponentConnector.Connect(int connectionld, object target) { switch (connectionld) { case 1: this.btnExitApp = ((System.Windows.Controls.Button)(target)); this.btnExitApp.Click += new System.Windows.RoutedEventHandler(this.btnExitApp_Clicked);
Глава 27. Введение в Windows Presentation Foundation и XAML 1025 return; this. contentLoaded = true; } И, наконец, в-классе MainWindow также реализован метод InitializeComponent(). Можно было ожидать, что этот метод содержит код, устанавливающий внешний вид и поведение каждого элемента управления за счет присваивания различных свойств (Height, Width, Content и т.п.). Однако это не так! Как же эти элементы управления получают корректный пользовательский интерфейс? Логика InitializeComponentO определяет местоположение встроенного в сборку ресурса, имя которого совпадает с именем исходного файла *.xaml: public void InitializeComponentO { if (_contentLoaded) { return; } _contentLoaded = true; System.Uri resourceLocater = new System.Uri ("/SimpleXamlApp;component/mainwindow.xaml" , System.UriKind.Relative); System.Windows.Application.LoadComponent(this, resourceLocater); Здесь возникает вопрос: что такое встроенный ресурс? Роль BAML Когда утилита msbuild.exe обрабатывает файл *.csproj, она генерирует файл с расширением *.baml, именованный согласно начальному файлу MainWindow.xaml, так что в папке \obj\Debug должен появиться файл под названием MainWindow.baml. Как и можно было предположить, формат Binary Application Markup Language (BAML) — это компактное двоичное представление исходных данных XAML. Этот файл *.baml встраивается в виде ресурса (через сгенерированный файл *.д.resources) в скомпилированную сборку. В этом можно удостовериться, открыв сборку в reflector.exe (рис. 27.10). Ресурс BAML содержит все необходимые данные для настройки внешнего вида виджетов пользовательского интерфейса (т.е. свойств вроде Height и Width). Открыв файл *.baml в Visual Studio 2010, можно увидеть следы начальных атрибутов XAML (рис. 27.11). # Red Gates .NET Reflector IB File View Tools Help c# ffl чЭ PresentationFramework 3 -СЭ SimpleXamlApp ;+■ l\ SimplcXamlApp.exe S Cl Resources // public resource SimpleXamlApp.g.i Size: 1025 bytes al Disassembler Name Ш mainwindow.baml Value 0c 00 00 00 4d 00 53 00 ... G97 bytes) > Рис. 27.10 Просмотр встроенного ресурса *.baml в утилите .NET Reflector
1026 Часть VI. Построение настольных пользовательских приложений с помощью WPF MainWindow.baml X 00000160 00000170 00000180 00000190 000001а0 OOOOOlbO OOOOOlcO OOOOOldO OOOOOleO OOOOOlfO 00000200 00000210 00000220 00000230 00000240 00000250 00000260 00000270 36 61 70 ЗА 6F 73 32 30 74 61 00 00 ЗА 2F 73 6F 30 30 35 04_ 05 64 33 2F 2F 6F 66 30 36 74 69 00 03 2F 73 66 74 36 2F 00 00 36 34 73 63 74 2E 2F 78 6F 6E 00 00 63 68- 2E 63 78 61 00 03 65 33 68 65 63 6F 61 6D 03 00 00 14 65 6D 6F 6D 6D 6C 00 00 03 00 00 00 24 09 Dl FF 00 00 00 03 00 00 00 24 FE 36 10 00 00 00 IF 1С 6E 64 6F 77 53 74 61 72 69 6F 6E 24 12 01 00 ОС 35 14 44 00 39 68 74 74 6ad364e35 D.9htt 6D 61 73 2E 6D 69 63 72 p://schemas.micr 6D 2F 77 69 6E 66 78 2F osoft.com/winfx/ 6C 2F 70 72 65 73 65 6E 2006/xaml/presen 01 00 02 00 03 00 35 03 tation 5. 38 01 78 2C 68 74 74 70 e.x.http 61 73 2E 6D 69 63 72 6F ://schemas.micro 2F 77 69 6E 66 78 2F 32 soft com/winfx/2 03 00 01 00 02 00 03 00 006/xaml 00 IF ОС 00 00 ID FD 00 5. _ . jTitle$$. . A Win] ! = now built using 1 | 99 FD 35 05 00 00 00 llOO* XAMU. .5. . . . ! 03 32 30 30 A4 FE 35 06 ....$....200..5. 09 C7 FF 03 33 30 30 A4 $. . . .300. 01 00 ID FD 00 15 57 69 .6 Wi 74 75 70 4C 6F 63 61 74 ndowStartupLocat 43 65 6E 74 65 72 53 63 ion$....CenterSc : ш ~~ ~~ ~~ i ► Рис. 27.11. BAML представляет собой компактную двоичную версию исходных данных XAML Здесь важно понять, что WPF-приложение содержит внутри себя двоичное представление (BAML) разметки. Во время выполнения ресурс BAML будет извлечен из контейнера ресурсов и использован для настройки внешнего вида всех окон и элементов управления. Также помните, что имена этих двоичных ресурсов идентичны именам написанных автономных файлов *.xaml. Однако это вовсе не означает необходимость поставки файлов *.xaml вместе со скомпилированной программой WPF! Если только не строится WPF-приложение, которое должно динамически загружать и разбирать файлы *.xaml во время выполнения, поставлять исходную разметку никогда не придется. Отображение XAML-данных приложения на код С# Последняя часть автоматически сгенерированного кода, которую мы рассмотрим, находится в файле MyApp.g.cs. Здесь имеется производный от Application класс с соответствующей точкой входа — методом Main(). Реализация Main() вызывает метод InitializeComponentO на типе-наследнике Application, который, в свою очередь, устанавливает свойство StartupUri, позволяя каждому объекту делать корректные установки свойств на основе двоичного представления XAML. namespace SimpleXamlApp { public partial class MyApp : System.Windows.Application { void AppExit(object sender, ExitEventArgs e) { MessageBox.Show ("App has exited"); } [System.Diagnostics.DebuggerNonUserCodeAttribute()] public void InitializeComponentO { this.Exit += new System.Windows.ExitEventHandler(this.AppExit); this. StartupUri = new System. Uri ("MainWindow. xaml" , System. UriKind. Relative) ; [System.STAThreadAttribute()] [System.Diagnostics.DebuggerNonUserCodeAttribute()] public static void Main() { SimpleXamlApp.MyApp app = new SimpleXamlApp.MyApp(); app.InitializeComponent(); app.Run();
Глава 27. Введение в Windows Presentation Foundation и XAML 1027 Итоговые замечания о процессе трансформирования XAML в сборку Итак, к этому моменту получена полноценная сборка .NET с использованием только двух файлов XAML и связанного сценария сборки для msbuild.exe. Как было показано, для обработки файлов XAML (и генерации *.baml) утилита msbuild.exe в процессе сборки полагается на вспомогательные настройки, определенные внутри файла *.targets. Все эти детали происходят "за кулисами", а на рис. 27.12 показана общая картина, касающаяся обработки файлов *.xaml во время компиляции. Файлы MainWindow. xaml, MyApp.xmal, *.csproj SimpleXamlApp. exe ч Встроенный ресурс BAML Утилита msbuild.exe и необходимые цели С# и WPF Вывод в каталог \Obj\Debug MainWindow.g.cs МуАрр.g.cs MainWindow.baml SimpleXamlApp.g.resources Компилятор С# - Компиляция файлов С# - Внедрение *. g. resources в виде ресурса Рис. 27.12. Процесс трансформации XAML в сборку во время компиляции Теперь вы лучше представляете, как используются данные XAML для построения приложения .NET. Можно переходить к рассмотрению синтаксиса и семантики самого языка XAML. Исходный код. Проект Wpf AppAllXaml доступен в подкаталоге Chapter 27. Синтаксис XAML для WPF Приложения WPF производственного уровня используют специальные инструменты для генерации необходимой XAML-разметки. Как бы ни были хороши эти инструменты, все же понимание общей структуры XAML не помешает. Введение в Kaxaml Когда вы впервые приступаете к изучению грамматики XAML, очень полезно использовать бесплатный инструмент под названием Kaxaml Вы можете получить Этот популярный редактор/анализатор XAML доступен для загрузки на веб-сайте http:// www.kaxaml.com. Редактор Kaxaml хорош тем, что он не имеет никакого понятия об исходном коде С#, обработчиках ошибок или логике реализации, и предлагает намного более простой способ тестирования фрагментов XAML, чем применение полноценного шаблона проекта WPF Visual Studio 2010. К тому же Kaxaml имеет набор интегрированных инструментов, таких как средство выбора цвета и диспетчер фрагментов XAML, и даже опцию "XAML scrubber", которая форматирует XAML-разметку на основе заданных настроек.
1028 Часть VI. Построение настольных пользовательских приложений с помощью WPF Открыв Kaxaml в первый раз, вы найдете там простую разметку для элемента управления <Раде>: <Раде xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <!— Добавляйте XAML-разметку сюда! --> </Grid> </Page> Подобно Window, элемент Page содержит различные диспетчеры компоновки и элементы управления. Однако, в отличие от Window, объекты Page не могут выполняться как отдельные сущности. Вместо этого они должны помещаться внутри подходящего хоста, такого как NavigationWindow, Frame или веб-браузер (и в этом случае просто делается приложение ХВАР). Очень полезно то, что можно вводить идентичную разметку в области <Раде> или <Window>. На заметку! Если в окне разметки Kaxaml заменить элементы <Раде> и </Раде> на <Window> и </window>, то можно нажать клавишу <F5> для отображения нового окна на экране. Для выполнения простого теста введите следующую разметку в панели XAML, расположенной внизу окна Kaxaml: <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <!— Кнопка со специальным содержимым —> <Button Height=00" Width=,,100"> <Ellipse Fill="Green" Height=0" Width=0"/> </Button> </Grid> </Page> В верхней части окна Kaxaml появится визуализированная страница (рис. 27.13). Рис. 27.13. Редактор Kaxaml — очень удобный (и бесплатный) инструмент, применяемый для изучения грамматики XAML
Глава 27. Введение в Windows Presentation Foundation и XAML 1029 При работе в Kaxaml помните, что этот инструмент не позволяет писать разметку, которая влечет за собой какую-либо компиляцию кода (однако разрешено использовать x:Name). Сюда входит определение атрибута х:Class (для указания файла кода), ввод имен обработчиков событий в разметке или применение любых ключевых слов XAML, которые также вызывают крмпиляцию кода (вроде FieldModif ier или ClassModif ier). Попытка сделать это приведет к ошибке разметки. Пространства имен XAML XML и "ключевые слова" XAML Корневой элемент XAML-документа WPF (такой как <Window>, <Page>, <UserControl> или <Application>) почти всегда ссылается на два заранее определенных пространства имен XML: <Раде xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> </Grid> </Page> Первое пространство имен XML, http://schemas.microsoft.com/winfx/2006/xaml/ presentation, отображает множество связанных с WPF пространств имен .NET для использования в текущем файле *.xaml (System.Windows, System.Windows.Controls, System.Windows.Data, System.Windows.Ink, System.Windows.Media, System.Windows. Navigation). Это отображение "один ко многим" на самом деле жестко закодировано внутри сборок WPF (WindowsBase.dll, PresentationCore.dll и PresentationFramework.dll) с использованием атрибута [XmlnsDefinition] уровня сборки. Ниже показан пример импортирования System.Windows:: [assembly: XmlnsDefinition ( "http://schemas.microsoft.com/winfx/200 6/xaml/presentation", "System.Windows")] Загрузив эти сборки WPF в утилиту reflector.exe, можно увидеть отображения наглядно. Например, выбор сборки PresentationCore.dll с последующим нажатием клавиши пробела позволяет просмотреть многочисленные экземпляры атрибута [XmlnsDefinition] (рис. 27.14). jf Red Gates NET Reflector file View Tools О ©! * a I я / \сГ !й чЭ System.ServiceModel if 43 System.Workflow.ComponentModel ■i) -СЭ System.Workflow.Runtime assembly: XminsPrefix("httf^/schemas.microsoft.com/vvinf)c/2006/xaml/presentat(o*i", °av")] [assembly: XmlnsDefinitionChttp://schemas.microsoft conVwmfx/2006/xaml/presentation",' System.WindowsAutomation')] [£i -*3 System .Workflow-Activities I [assembly: XmfnsDefinitionrhttp://schemas.microsoft.com/winfx/'2006/xaml/presentation', ' System.Windows.Media.TextForr № чЭ WindowsBase [assembly: XmfnsDefinitior»("http://schemas.microsoft.com/)tps/2005/06", "System.Windows.Media.Imaging"}] -j [assembly: XmlnsDefinrtion(''httpV/schemas.microsoft.com/i«infx/r200б/xaml/presentation,'. "System.Wmdows.lnlc")] jgi [assembly: XmlnsDefinitiofi('http://schemas.m»crosoft.com/winfx/2006/xaml/presentation', "System.WindowsJnput")] IS "^ fjjdrew^^^ II [assembly: XmlnsOefinrttofi(°http://schemas.microsoft.com/fwrnfx/2006/xam^presentation",' System.Windows.Media.Effects'", * ЧЭ|Щ II [assembly: XmlnsDefinition("http://schemas.microsoft.com/netfx/'2009/xaml/presentatton", "System.Wlndows.Media.TextForr Щ -i_3 PresentationFramework ~ [assembly: XmlnsDefinitionrhttp://schemas.micrcKoft.cofTi/netfx/2007/xaml/presentation''( "System.WtndowsJnput"}] "—J [assembly: XmlnsDefinitk>n("httpi//schemas.microsoft.com/xps/2tXM,'/06", "System.Windows.Media.Media3D")] // Assembly PrcsentationCor* Version 4.0 0 0 * [assembly: XmlnsDefinit(on('Kttp://scr>emas.microsoft.com/netfx/'2007/xaml/presentation', ' System.WindowsAutomation')] J [assembly: XmlnsDettnit^on('Ъttp://schemas.micfosoft,com/fмtfx/2007/xaml/presentatiorl',, "System.Windows.Media.Aiiimati'i Location: %SystemRoot% I [aKemo|y: XmlnsDeftortion("http://scherras.microsoft.corTVn€tfx/2fJ07/xaml/presentatiori'', "System.Windows")] \Microsoft.net\Frameworlc\v4.0.2irj06  [ОТ5етЬ1у: XmlnsPrefixrhttp://schemas.microsoft.com/xps/2005/D6', "xps")] \WPPiPresentationCore.dll , J [assembly: XmlnsDeflпitton('■http:У/schemas.microsoft.com/xps/2005Л)б■,, "System.Windows.Input")] PresentationCore, Version=4.0.0.0, [assembly: XmlnsDefinitiorc("http://schemas.microsoft.com/'vwnfx/20067xam!/presentation", "System.Windows")] Curture= neutral. Рис. 27.14 Пространство имен http://schemas.microsoft.com/winfx/2006/xaml/ presentation отображается на ключевые пространства имен WPF
1030 Часть VI. Построение настольных пользовательских приложений с помощью WPF Второе пространство имен XML, http://schemas.microsoft.com/winfx/2006/xaml, используется для включения специфичных для XAML "ключевых слов" (этот термин выбран за неимением лучшего) вместе с пространством имен System.Windows.Markup: [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml", "System.Windows.Markup")] Одно из правил любого корректно сформированного XML-документа (вспомните, что XAML — это грамматика, основанная на XML) состоит в том, что он должен иметь открывающий корневой элемент, назначающий одно пространство имен XML в качестве пространства по умолчанию, которым обычно является пространство, содержащее наиболее часто используемые элементы. Если корневой элемент требует включения дополнительных вторичных пространств имен (как здесь показано), они должны быть определены с уникальным префиксом (чтобы разрешить возможные конфликты имен). В качестве префикса принято применять просто х, тем не менее, это может быть любой уникальный маркер по вашему выбору, например, XamlSpecif icStuf f: <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:XamlSpecificStuff="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <!-- Кнопка со специальным содержимым --> <Button XamlSpecificStuff:Name="buttonl" Height=00" Width=00"> <Ellipse Fill="Green" Height=0" Width=0"/> </Button> </Grid> </Page> Очевидный недостаток определения длинных префиксов пространств имен XML связан с тем, что XamlSpecificStuff придется набирать всякий раз, когда в файле XAML нужно сослаться на один из типов, определенных в этом пространстве, связанном с XAML. Поскольку набирать каждый раз префикс XamlSpecificStuff слишком утомительно, давайте ограничимся х. В любом случае, помимо ключевых слов x:Name, x:Class и x:Code, включение пространства имен XML http://schemas.microsoft.com/winfx/2006/xaml также предоставит доступ к дополнительным ключевым словам XAML, наиболее часто используемые из которых перечислены в табл. 27.9. Таблица 27.9. Ключевые слова XAML Ключевое слово XAML Назначение х: Array Представляет тип массива .NET на XAML x:ClassModif ier Позволяет определять видимость типа класса (internal или public), обозначенного ключевым словом Class x:FieldModif ier Позволяет определять видимость члена типа (internal, public, private или protected) для любого именованного элемента корня (например, <Button> внутри элемента <Window>). Именованный элемент определяется с использованием ключевого слова XAML Name х:Key Позволяет устанавливать значение ключа для элемента XAML, которое должно быть помещено в элемент словаря x:Name Позволяет указывать сгенерированное С# имя заданного элемента XAML x:Null Представляет null-ссылку х:Static Позволяет ссылаться на статический член типа
Глава 27. Введение в Windows Presentation Foundation и XAML 1031 Окончание табл. 27.9 Ключевое слово XAML Назначение х:Туре XAML-эквивапент операции С# typeof (выдает System.Type на основе указанного имени) x:TypeArguments Позволяет устанавливать элемент как обобщенный тип с определенным параметром типа (например, List<int> или List<bool>) В дополнение к этим двум необходимым объявлениям пространств имен XML можно, а иногда и необходимо, определить дополнительные префиксы дескрипторов в открывающем элементе XAML-документа. Обычно это делается, когда нужно описать в XAML класс .NET, определенный во внешней сборке. Например, предположим, что вы построили несколько специальных элементов управления WPF и упаковали их в библиотеку под названием MyControls.dll. Теперь, если необходимо создать новый экземпляр Window, который использует эти элементы, можно установить специальное пространство имен XML, отображаемое на библиотеку MyControls.dll, с использованием лексем clr-namespace и assembly. Ниже приведен пример разметки, создающей префикс дескриптора по имени myCtrls, который может применяться для доступа к членам этой библиотеки: <Window x:Class="WpfApplicationl.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml" xmlns:myCtrls="clr-namespace:MyControls;assembly=MyControls" Title="MainWindow" Height=,,350" Width=25"> <Grid> <myCtrls:MyCustomControl /> </Grid> </Window> Лексеме clr-namespace присваивается название пространства имен .NET в сборке, в то время как лексема assembly устанавливается в дружественное имя внешней сборки *.dll. Такой синтаксис можно использовать для любой внешней библиотеки .NET, которой необходимо манипулировать внутри разметки. Управление объявлениями классов и переменных-членов Многие из этих ключевых полей вы увидите в действии там, где они понадобятся. Давайте в качестве простого примера рассмотрим следующее определение XAML <Window>, в котором используются ключевые слова ClassModifier и FieldModifier, а также x:Name и х:Class (вспомните, что редактор Kaxaml не позволяет использовать ключевые слова XAML, вызывающие компиляцию, такие как x:Code, x:FieldModifier или x:ClassModifier): <!— Этот класс теперь будет объявлен как internal в файле *.g.cs --> <Window x:Class="MyWPFApp.MainWindow" xrClassModifler ="internal11 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml"> <!— Эта кнопка будет объявлена как public в файле *.g.cs --> <Button x:Name ="myButton" x:FieldModifier ="public" Content = K"/> </Window> По умолчанию все определения типов C#/XAML являются общедоступными (public), а члены — внутренними (internal). Однако на основе показанного определения XAML результирующий автоматически сгенерированный файл содержит тип класса internal с public-членом Button:
1032 Часть VI. Построение настольных пользовательских приложений с помощью WPF internal partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector { public System.Windows.Controls.Button myButton; } Элементы XAML, атрибуты XAML и преобразователи типов После установки корневого элемента и необходимых пространств имен XML следующей задачей будет наполнение корня дочерним элементом. Как уже упоминалось, в реальных WPF-приложениях дочерним элементом будет диспетчер компоновки (вроде Grid или StackPanel), который, в свою очередь, содержит любое количество элементов, определяющих пользовательский интерфейс. Эти диспетчеры компоновки подробно рассматриваются в следующей главе, а пока предположим, что элемент <Window> будет содержать единственный элемент Button. Как было показано ранее в главе, элементы XAML отображаются на типы классов или структур внутри заданного пространства имен .NET, в то время как атрибуты в открывающем дескрипторе элемента отображаются на свойства и события конкретного типа. Для целей иллюстрации введите следующее определение <Button> в редакторе Kaxaml: <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <!-- Настроить внешний вид элемента Button --> <Button Height=,,50" Width=,,100" Content="OK! " FontSize=,,20" Background="Green" Foreground="Yellow,,/> </Grid> </Page> Обратите внимание, что значения, присвоенные свойствам, являются строковыми. Это может показаться полным несоответствием типам данных, поскольку после создания такого элемента Button в коде С# этим свойствам будут присваиваться не строковые объекты, а значения специфических типов данных. Например, ниже показано, как та же кнопка описана в коде: public void MakeAButton () { Button myBtn = new Button (); myBtn.Height = 50; myBtn.Width = 100; myBtn.FontSize = 20; myBtn.Content = "OK!"; myBtn.Background = new SolidColorBrush(Colors.Green); myBtn.Foreground = new SolidColorBrush(Colors.Yellow); } Оказывается, WPF поставляется с множеством классов преобразователей типов, которые применяются для трансформации простых текстовых значений в корректные типы данных. Этот процесс происходит прозрачно (и автоматически). Тем не менее, нередко возникает потребность присвоить атрибуту XAML намного более сложное значение, которое не может быть выражено простой строкой. Например, предположим, что необходимо построить специализированную кисть для установки свойства Background элемента Button. При создании этой кисти в коде все достаточно просто:
Глава 27. Введение в Windows Presentation Foundation и XAML 1033 public void MakeAButton () { // Забавная кисть для фона. LinearGradientBrush fancyBruch = new LinearGradientBrush(Colors.DarkGreen, Colors.LightGreen, 45); myBtn.Background = fancyBruch; myBtn.Foreground = new SolidColorBrush(Colors.Yellow); } Но как представить эту сложную кисть в виде строки? Да никак! К счастью, в XAML предусмотрен специальный синтаксис, который можно применять всякий раз, когда нужно присвоить сложный объект в качестве значения свойства, и этот синтаксис называется "свойство-элемент" (property-element). Понятие синтаксиса XAML "свойство-элемент" Синтаксис свойство-элемент позволяет присваивать свойству сложные объекты. Вот как выглядит XAML-описание элемента Button, свойство Background которого установлено в кисть LinearGradientBrush: <Button Height=,,50" Width=00" Content="OK' " FontSize=011 Foreground="Yellow"> <Button.Background> <LinearGradientBrush> <GradientStop Color=,,DarkGreen" Offset=,,0"/> <GradientStop Color=,,LightGreen" Offset="l,,/> </LinearGradientBrush> </Button.Background> </Button> Обратите внимание, что внутри контекста дескрипторов <Button> и </Button> определен вложенный элемент по имени <Button.Background>. Внутри него определен специальный элемент <LinearGradientBrush>. (Пока не беспокойтесь о коде кисти; графика WPF будет рассматриваться в главе 30.) В общем случае, любое свойство может быть установлено с использованием синтаксиса "свойство-элемент", что всегда сводится к следующему шаблону: <ОпределяющийКласс> <ОпределяющийКласс.СвойствоОпределяющегоКласса> <!-- Значение для СвойствоОпределяющегоКласса --> </ОпределяющийКласс.СвойствоОпределяющегоКласса> </ОпределяющийКласс > Хотя любое свойство может быть установлено с использованием этого синтаксиса, указание значения в виде простой строки экономит время на ввод. Например, ниже показано, как мог бы выглядеть более громоздкий способ установки свойства Width элемента Button: <Button Height=,,50" Content="OK ' " FontSize=ll20" Foreground="Yellow"> <Button.Width> 100 </Button.Width> </Button>
1034 Часть VI. Построение настольных пользовательских приложений с помощью WPF Понятие присоединяемых свойств XAML В дополнение к синтаксису "свойство-элемент" в XAML поддерживается специальный синтаксис, используемый для установки значения присоединяемого свойства. По сути, присоединяемые свойства позволяют дочернему элементу устанавливать значение какого-то свойства, которое в действительности определено в родительском элементе. Общий шаблон, которому нужно следовать, выглядит так: <РодительскийЭлемент> <ДочернийЭлемент РодительскийЭлемент.СвойствоРодительскогоЭлемента = "Значение"> </Родитель скийЭлемент> Наиболее распространенное применение синтаксиса присоединяемых свойств состоит в позиционировании элементов пользовательского интерфейса внутри одного из классов диспетчеров компоновки (Grid, DockPanel и т.п.). Эти диспетчеры компоновки более подробно рассматриваются в следующей главе, а пока введите в редакторе Kaxaml такую разметку: <Раде xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Canvas Height=,,200" Width=00" Background=,,LightBlue"> <Ellipse Canvas.Top=0" Canvas.Left=0" Height=0" Width=0" Fill="DarkBlue"/> </Canvas> </Page> Здесь определен диспетчер компоновки Canvas, содержащий в себе элемент Ellipse. Обратите внимание, что с помощью синтаксиса присоединяемого свойства Ellipse может информировать свой родительский элемент (Canvas) о том, где его следует разместить по отношению к левому верхнему углу. В отношении присоединяемых свойств следует помнить несколько моментов. Прежде всего, это не универсальный синтаксис, который может применяться к любому свойству любого родительского элемента. Например, следующий XAML содержит ошибку: <!-- Ошибка! Нельзя устанавливать свойство Background в Canvas через присоединяемое свойство --> <Canvas Height=,,200" Width=00"> <Ellipse Canvas .Background="LightBlue11 Canvas.Top= 0" Canvas.Left="90" Height=0" Width=0" Fill="DarkBlue"/> </Canvas> В действительности присоединяемые свойства представляют собой специализированную форму связанной с WPF концепции, которая называется свойством зависимости Если свойство не было реализовано в очень специфической манере, его значение не может быть установлено с использованием синтаксиса присоединяемых свойств. Более подробно свойства зависимости рассматриваются в главе 31. На заметку! Инструменты Kaxaml, Visual Studio 2010 и Expression Blend оснащены средством IntelliSense, отображающим действительные присоединяемые свойства, которые могут быть установлены для каждого элемента. Понятие расширений разметки XAML Как уже объяснялось, значения свойств чаще всего представляются в виде простой строки или через синтаксис "свойство-элемент". Однако есть и другой способ указать значение атрибута XAML — с использованием расширений разметки Расширения раз-
Глава 27. Введение в Windows Presentation Foundation и XAML 1035 метки позволяют анализатору XAML получать значение свойства из выделенного внешнего класса. Это может быть очень выгодно, учитывая, что некоторые значения свойств требуют выполнения множества операторов кода для поиска значения. Расширения разметки предлагают способ ясного расширения грамматики XAML новой функциональностью. Расширение разметки внутренне представлено как класс, унаследованный от Mark up Extension. Следует подчеркнуть: шансы, что когда-либо придется строить специальное расширение разметки, невелики. Тем не менее, подмножество ключевых слов XAML (таких как х: Array, x:Null, х:Static и х:Туре)— это именно расширения разметки. Расширение разметки заключается в фигурные скобки, как показано ниже: Олемент УстанавливаемоеСвойство = "{РасширениеРазметки}"/> Чтобы увидеть расширение разметки в действии, введите следующий код в редакторе Kaxaml: <Page xmlns="http://schemas.microsoft.com/winfx/20 0 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:CorLib="clr-namespace:System;assembly=mscorlib"> <StackPanel> <!-- Расширение разметки Static позволяет получать значение статического члена класса —> <Label Content ="{x:Static CorLib:Environment.OSVersion}"/> <Label Content ="{x:Static CorLib:Environment.ProcessorCount}"/> <•— Расширение разметки Type — это XAML-версия оператора С# typeof --> <Label Content ="{х:Туре Button}" /> <Label Content ="{x:Type CorLib:Boolean}" /> <!—Наполнение элемента ListBox массивом строк --> <ListBox Width=00" Height=0"> <ListBox.ItemsSource> <x:Array Type="CorLib:String"> <CorLib:String>Sun Kil Moon</CorLib:String> <CorLib:String>Red House Painters</CorLib:String> <CorLib:String>Besnard Lakes</CorLib:String> </x:Array> </ListBox.ItemsSource> </ListBox> </StackPanelx/Page> Прежде всего, обратите внимание, что определение <Раде> имеет новое объявление пространства имен XML, что позволяет получать доступ к пространству имен System сборки mscorlib.dll. Имея это пространство имен, сначала с помощью расширения разметки x:Static извлекаются значения OSVersion и ProcessorCount класса System. Environment. Расширение разметки х:Туре позволяет получить доступ к описанию метаданных указанного элемента. Здесь просто назначаются полностью квалифицированные имена типов WPF Button и System.Boolean. Наиболее интересная часть показанной выше разметки связана с элементом ListBox. Его свойство ItemSource устанавливается в массив строк, полностью объявленный в разметке. Обратите внимание, что расширение разметки х: Array позволяет указывать набор под элементов внутри своего контекста: <х:Array Type="CorLib:String"> <CorLib:String>Sun Kil Moon</CorLib:String> <CorLib:String>Red House Painters</CorLib:String> <CorLib:String>Besnard Lakes</CorLib:String> </x:Array>
1036 Часть VI. Построение настольных пользовательских приложений с помощью WPF На заметку! Приведенный выше пример XAML служит только для иллюстрации расширения разметки в действии. Как будет показано в главе 28, существуют намного более легкие способы наполнения элементов управления ListBox На рис. 27.15 показана разметка этого <Раде> в редакторе Kaxaml. Рис. 27.15. Расширения разметки позволяют устанавливать значения через функциональность выделенного класса Итак, вы ознакомились с многочисленными примерами, демонстрирующими основные аспекты синтаксиса XAML. Наверняка вы согласитесь, что XAML — очень интересный язык в том плане, что позволяет описать дерево объектов .NET в декларативной манере. Хотя это исключительно полезно для конфигурирования графических пользовательских интерфейсов, следует помнить, что XAML может описать любой тип из любой сборки, если только этот тип не является абстрактным и имеет конструктор по умолчанию. Построение приложений WPF с использованием файлов отделенного кода В первых двух примерах этой главы демонстрировались крайние случаи построения приложения WPF с использованием только кода, или только XAML. Рекомендованный способ построения любого приложения WPF предусматривает применение подхода на основе файла кода. Согласно этой модели, файлы XAML проекта не содержат ничего кроме разметки, которая описывает общее состояние классов, в то время как файлы кода содержат детали реализации. Добавление файла кода для класса MainWindow Для иллюстрации сказанного модифицируем пример WpfAppAllXaml, добавив к нему файль! кода. Скопируйте всю папку предыдущего примера и назовите ее Wpf AppCodeFiles. Создайте в этой папке новый файл кода С# по имени MainWindow. xaml.cs (по существующему соглашению имя файла отделенного кода С# имеет форму *.xaml.cs). Поместите в этот новый файл показанный ниже код:
Глава 27. Введение в Windows Presentation Foundation и XAML 1037 // MainWindow.xaml.es using System; using System.Windows; using System.Windows.Controls; namespace SimpleXamlApp { public partial class MainWindow : Window { public MainWindow() { // Помните! Этот метод определен внутри // сгенерированного файла MainWindow. g. cs . InitializeComponent(); } private void btnExitApp_Clicked(object sender, RoutedEventArgs e) { this.Close(); } } } Здесь определен частичный класс, содержащий логику обработки событий, которая может быть объединена с определением частичного класса того же типа в файле *.g.cs. Учитывая, что InitializeComponent () определен внутри файла MainWindow.g.cs, конструктор окна выполняет вызов для загрузки и обработки встроенного ресурса BAML. Файл MainWindow. xaml также должен быть обновлен; это означает просто удаление в нем всех следов прежнего кода С#: <Window х:CIass="SimpleXamlApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="A Window built using Code Files!" Height=00" Width=00" WindowstartupLocation ="CenterScreen"> <'—The event handler is now in your code file --> <Button x:Name="btnExitApp" Width=33" Height=4" Content = "Close Window" Click ="btnExitApp_Clicked"/> </Window> Добавление файла кода для класса МуАрр При необходимости можно было бы также построить файл отделенного кода для типа, унаследованного от Application. Поскольку большая часть действий происходит в файле MyApp.g.cs, код внутри файла MyApp.xaml.cs выглядит следующим образом: / / МуАрр.xaml.сs using System; using System.Windows; using System.Windows.Controls; namespace SimpleXamlApp { public partial class MyApp : Application { private void AppExit(object sender, ExitEventArgs e) { MessageBox.Show("App has exited"); } } }
1038 Часть VI. Построение настольных пользовательских приложений с помощью WPF В файле MyApp.xaml теперь содержится такая разметка: <Application x:Class="SimpleXamlApp.MyApp11 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml" Exit ="AppExit"> </Application> Обработка файлов кода с помощью msbuild.exe Прежде чем мы перекомпилировать файлы с использованием msbuild.exe, понадобится обновить файл *.csproj для учета новых файлов С#, подлежащих включению в процесс компиляции, с помощью элементов <Compile> (выделены полужирным): <Project DefaultTargets="Build" xmlns= "http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <RootNamespace>SimpleXamlApp</RootNamespace> <AssemblyName>SimpleXamlApp</AssemblyName> <OutputType>winexe</OutputType> </PropertyGroup> <ItemGroup> <Reference Include="System" /> <Reference Include="WindowsBase" /> <Reference Include="PresentationCore11 /> <Reference Include="PresentationFramework11 /> </ItemGroup> <ItemGroup> <ApplicationDefinition Include="MyApp.xaml" /> <Compile Include = "MainWindow.xaml.es" /> <Compile Include = "MyApp.xaml.es" /> <Page Include="MainWindow.xaml" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" /> </Project> Передав этот сценарий сборки утилите msbuild.exe, вы получите ту же исполняемую сборку, что и в приложении Wpf AppAllXaml. Однако в том, что касается разработки, теперь имеется четкое отделение представления (XAML) от программной логики (С#). Поскольку это — предпочтительный метод разработки WPF, в приложениях WPF, созданных с использованием Visual Studio 2010, всегда применяется модель отделенного кода. Исходный код. Проект Wpf AppCodeFiles доступен в подкаталоге Chapter 27. Построение приложений WPF с использованием Visual Studio 2010 На протяжении этой главы примеры создавались с использованием простых текстовых редакторов, компилятора командной строки и редактора Kaxaml. Тогда важно было сосредоточить внимание на основном синтаксисе приложений WPF, не замутненном дополнительными элементами графического конструктора. Однако теперь, зная, как строятся WPF-приложения с помощью простейших средств, давайте посмотрим, каким образом Visual Studio 2010 может упростить процесс разработки.
Глава 27. Введение в Windows Presentation Foundation и XAML 1039 На заметку! Хотя Visual Studio 2010 обеспечивает некоторую поддержку написания сложной XAML- разметки с использованием интегрированных визуальных конструкторов, Expression Blend представляет собой намного лучшую альтернативу для построения WPF-приложений. Среда Expression Blend рассматривается в главе 28. Шаблоны проектов WPF В диалоговом окне New Project (Новый проект) среды Visual Studio 2010 определен набор рабочих пространств проектов WPF, и все они расположены в узле Windows корня Visual C#. На выбор доступны следующие варианты: WPF Application (Приложение WPF), WPF User Control Library (Библиотека пользовательских элементов управления WPF), WPF Custom Control Library (Библиотека специальных элементов управления WPF) и WPF Browser Application (Браузерное приложение WPF, т.е. ХВАР). Для начала создадим новое приложение WPF по имени MyXamlPad (рис. 27.16). Рис. 27.16. Шаблоны проектов WPF в Visual Studio 2010 Помимо установки ссылок на все сборки WPF (PresentationCore.dll, Presentation Foundation.dll и WindowsBase.dll), вы также получаете начальные классы-наследники Window и Application и возможность использования файлов кода и связанный XAML-файл. На рис. 27.17 показано окно Solution Explorer для этого нового проекта WPF. Знакомство с инструментами визуального конструктора WPF В Visual Studio 2010 предусмотрена панель инструментов flbolbox), доступная через пункт меню View (Вид) и содержащая множество элементов управления WPF (рис. 27.18). Используя стандартное перетаскивание с помощью мыши, можно поместить любой из этих элементов управления на поверхность визуального конструктора элемента Window. При этом соответствующий XAML будет сгенерирован автоматически. Тем не менее, разметку можно также вводить и вручную, с использованием интегрированного редактора XAML.
1040 Часть VI. Построение настольных пользовательских приложений с помощью WPF I Solution Explorer f*3 Solution 'MyXamlPad' A project) л ip MyXamlPad t> <fM Properties d ■ £3t References 4Э Microsoft.CSharp -■O PresentationCore -C2 PresentationFramework *GJ System НЭ System.Core •O System.Data нЭ System.Data.DataSetExtensions *€3 System.Xaml >Э SystemJKml -СЭ System.Xml.Linq -СЭ WindowsBase j Й Appjcaml S§^ App,xaml.cs 4 • MainWindcw.xaml **) MainWindow.xaml.cs ^ Solution Explorer I Рис. 27.17. Начальные файлы проекта типа WPF Application JToolbo ,-IV~, bi A Ш »I2 a 1 ss ею 0 □ m \ m i ■ GroupBox Image Label ListBox LrstView MedraElement Menu PasswordBox ProgressBar RadioButton Rectangle RichTextBox ScrollBar ScrollViewer * п x] * 1 -1 Рис. 27.18. Панель инструментов содержит элементы управления WPF, которые могут быть помещены на поверхность визуального конструктора Как показано на рис. 27.19, при этом обеспечивается поддержка средства IntelliSense, что может упростить написание разметки. На заметку! Расположение панелей визуального конструктора легко изменять с помощью кнопок, встроенных в разделитель — Swap Panels (Поменять панели), которая помечена стрелками вверх/вниз, Horizontal Split (Разделить горизонтально) и Vertical Split (Разделить вертикально) и т.д. Уделите время на удобную для себя организацию панелей. ЕЗ XAML ; | x:Cias,s="«yXe»lPad.«ainWindow" xinlns^http://schemes,microsoft .com/winfx/2006/xarel/preb< xmlns:xV*http://schemas.microsoft.соя/winfx/20в6/ха»1" Title«"MainWindow" Height-50" Width«25" (> id> 1 f Activated Iff AIlowDrop ■ff AllowsTransparency 0\ JO Binding ; iff BindingGroup iff BitmapEffect iff BitmapEffectfnput jiff BorderBmsh iff BorderThickness Iff CacheMode {) Calendar Iff a* Рис. 27.19. Визуальный конструктор элемента Window
Глава 27. Введение в Windows Presentation Foundation и XAML 1041 После помещения элементов управления на поверхность визуального конструктора можете использовать окно Properties (Свойства) для установки значений свойств выбранного элемента управления, а также для создания обработчиков событий для выбранного элемента. Для примера перетащите элемент управления — кнопку на любое место поверхности визуального конструктора. В результате Visual Studio сгенерирует XAML-разметку вроде показанной ниже: <Button Content="Button" Height=3" HorizontalAlignment="Left" Margin=2,12,0,0" Name="buttonl" VerticalAlignment=,,Top" Width=5" /> Теперь с помощью окна Properties измените цвет фона (Background) элемента Button с использованием встроенного редактора кистей (рис. 27.20). На заметку! Редактор кистей в Expression Blend очень похож на редактор кистей в Visual Studio и будет детально рассматриваться в главе 29. Рис. 27.20. Окно Properties позволяет конфигурировать пользовательский интерфейс элемента управления WPF Завершив упражнение с редактором кистей, взгляните на сгенерированную разметку. Она может выглядеть примерно так: <Button Content="Button" Height=3" HorizontalAlignment="Left" Margin=2,12,0,0" Name="buttonl" VerticalAlignment="Top" Width= 5"> <Button.Background> <LinearGradientBrush EndPoint="l,0.5" StartPoint=,0.5"> <GradientStop Color="#FF7488CE" Offset=" /> <GradientStop Color="#FFCllElE" Offset=.837"J> </LinearGradientBrush> </Button.Background> </Button> Для организации обработки событий, связанных с определенным элементом управления, также может использоваться окно Properties, но на этот раз нужно перейти на вкладку Events (События). Удостоверьтесь, что на поверхности визуального конструктора выбрана кнопка, в окне Properties перейдите на вкладку Events и найдите событие Click. Среда Visual Studio 2010 автоматически построит обработчик событий со следующим именем ИмяЭлементаУправления_ИмяСобытия. Пока кнопка не переименована, в окне Properties отображается сгенерированный обработчик событий по имени buttonl Click (рис. 27.21).
1042 Часть VI. Построение настольных пользовательских приложений с помощью WPF I Button buttonl I _jf Properties 4 Events I |МШ j Search Context MenuClos... Context MenuOpe... DataContextChan... DragEnter DragLeave DragOver Drop FocusabteC ha nged Ш buttonl_Click □ □ □ □ □ a D □ ДН —I "pll w ч Рис. 27.21. Обработка событий с использованием визуального конструктора К тому же Visual Studio 2010 сгенерирует соответствующий код С# в файле кода для окна. Здесь можно добавить любой код, который должен выполняться по щелчку на кнопке: public partial class MainWindow : Window { public MainWindow () { InitializeComponent (); } private void buttonl_Click(object sender, RoutedEventArgs e) { } } Обрабатывать событие можно также непосредственно в редакторе XAML. Для примера поместите курсор мыши внутрь элемента <Button> и введите имя события MouseEnter, а за ним — знак равенства. Visual Studio отобразит все совместимые обработчики из файла кода, а также опцию <New Event Handler> (Новый обработчик события). Двойной щелчок на <New Event Handler> приводит к тому, что IDE-среда сгенерирует соответствующий обработчик в файле кода С#. Теперь, когда вы ознакомились с базовыми инструментами, применяемыми внутри Visual Studio 2010 для манипуляций приложениями WPF, давайте воспользуемся этой IDE-средой для построения примера программы, которая проиллюстрирует процесс разбора XAML во время выполнения. Прежде, чем начать, полностью удалите только что созданную разметку, описывающую Button, а также удалите код обработчика события С#. Проектирование графического интерфейса окна API-интерфейс WPF поддерживает возможность программной загрузки, разбора и сохранения XAML-описаний. Это может быть полезно во многих ситуациях. Например, предположим, что есть пять разных файлов XAML, описывающих внешний вид и поведение типа Window. До тех пор, пока имена каждого элемента (и всех необходимых обработчиков событий) идентичны внутри каждого файла, можно динамически менять "обложки" окна (возможно, на основе аргумента, передаваемого приложению при запуске).
Глава 27. Введение в Windows Presentation Foundation и XAML 1043 Взаимодействие с XAML во время выполнения вращается вокруг типов X ami Reader и XamlWriter — оба они определены в пространстве имен System.Windows.Markup. Чтобы проиллюстрировать, как программно наполнить объект Window из внешнего файла *.xaml, создадим приложение, который имитирует базовую функциональность редактора Kaxaml. Хотя наше приложение определенно не будет столь же развитым, как Kaxaml, все же оно предоставит возможность вводить разметку XAML, просматривать результаты и сохранять XAML во внешнем файле. Для начала обновите начальное XAML-определение элемента <Window>, как показано ниже. На заметку! В следующей главе мы погрузимся в работу с элементами управления и панелями, так что пока не беспокойтесь о деталях объявления элементов управления. <Window x:Class="MyXamlPad.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title=MMy Custom XAML Editor" Height=38" Width=041" Loaded=llWindow_LoadedM Closed="Window_Closed11 WindowStartupLocation="CenterScreen'"* <!— Используйте DockPanel, а не Grid —> <DockPanel LastChildFill="True" > <!— Эта кнопка запустит окно с определенным XAML --> <Button DockPanel.Dock=MTopn Name = "btnViewXaml" Width=,,100" Height=0" Content ="View Xaml" Click="btnViewXaml_Click11 /> <!-- Это будет область для ввода --> <TextBox AcceptsReturn ="True" Nam = = "t::tXamlData" FontSize = 4" Background="Black" Foreground="Yellow11 BorderBrush ="Blue" VerticalScrollBarVisibility="Auto11 Accept sTab="True"/4 </DockPanel> </Window> Прежде всего, обратите внимание на то, что первоначальный тип <Grid> заменен типом <DockPanel>, содержащим Button (по имени btnViewXaml) и TextBox (по имени txtXamlData), а также на то, как обрабатывается событие Click типа Button. Кроме того, события Loaded и Closed самого типа Window были обработаны внутри открывающего элемента <Window>. Если вы применяли визуальный конструктор для обработки событий, то должны найти следующий код в файле MainWindow.Xaml.cs: public partial class MainWindou : Window { public MainWindow() InitializeComponent(); private void btnViewXaml_Clirk (ob]e -1- sender, PoutedEventArgs e) private void Window_Closed (rt jf it -r, Ev.i t'rj: e) private void Window Loaded(object r* nder, FcutedEventArgs p) }
1044 Часть VI. Построение настольных пользовательских приложений с помощью WPF Прежде чем продолжить, не забудьте импортировать следующие пространства имен в файл MainWindiw.xaml.cs: using System.10; using System.Windows.Markup; Реализация события Loaded Событие Loaded главного окна отвечает за определение наличия файла YourXaml. xaml в папке, содержащей приложение. Если этот файл есть, вы прочитаете его данные и поместите в Text В ох главного окна. Если же нет, то заполните Text В ох начальным XAML- описанием по умолчанию пустого окна (это описание в точности совпадает с разметкой, полученной при начальном определении окна, за исключением того, что для неявной установки свойства Content из Window вместо <Grid> используется <StackPanel>). На заметку! Строку, которая строится для представления ключевых пространств имен XML, набирать довольно утомительно, учитывая потребность в символах отмены, необходимых для внутренних кавычек, поэтому будьте внимательны. private void Window_Loaded(object sender, RoutedEventArgs e) { // При загрузке главного окна приложения поместить // некоторый базовый текст XAML в текстовый блок. if (File.Exists(System.Environment.CurrentDirectory + "\\YourXaml.xaml") ) { txtXamlData.Text = File.ReadAllText("YourXaml.xaml"); } else { txtXamlData.Text = ' "<Window xmlns=\"http://schemas.microsoft.com/winfx/200£/xaml/presentation\"\nM +"xmlns:x=\"http://schemas.microsoft.com/winfx/2 00 6/xaml\"" +" Height =\00\" Width =\00\" WindowStartupLocation=\"CenterScreen\">\n" +"<StackPanel>\n" +"</StackPanel>\n" + "</Window>"; } } Используя этот подход, приложение сможет загружать XAML, введенный в предыдущем сеансе, или при необходимости предлагать блок разметки по умолчанию. Сейчас можно запустить программу и увидеть внутри объекта Text В ох окно, показанное на рис. 27.22. Рис. 27.22. Первый запуск MyXamlPad.exe
Глава 27. Введение в Windows Presentation Foundation и XAML 1045 Реализация события Click объекта Button При щелчке на объекте Button сначала сохраняются текущие данные из TextBox в файле YourXaml.xaml. Сохраненные данные читаются через File.0pen() для получения типа-наследника Stream. Это необходимо, поскольку метод XamlReader.LoadO требует типа-наследника Stream (вместо простого System.String) для представления XAML-разметки, подлежащей разбору. После загрузки описания XAML в <Window>, который будет конструироваться, создайте экземпляр System.Windiows.Window на основе находящегося в памяти XAML и отобразите Window в виде модального диалогового окна: private void btnViewXaml_Click(object sender, RoutedEventArgs e) { // Записать данные из текстового блока в локальный файл *.xaml. File.WriteAllText("YourXaml.xaml", txtXamlData.Text); // Это окно, к которому будет динамически применяться XAML-раэметка. Window myWindow = null; // Открыть локальный файл *.xaml. try { using (Stream sr = File.Open("YourXaml.xaml", FileMode.Open)) { // Присоединить XAML-раэметку к объекту Window. myWindow = (Window)XamlReader.Load(sr); // Отобразить диалоговое окно и выполнить очистку. myWindow.ShowDialog(); myWindow.Close(); myWindow = null; } } catch (Exception ex) { MessageBox.Show(ex.Message); } } Обратите внимание, что большая часть логики помещена в блок try/catch. Таким образом, если файл YourXaml.xaml содержит неверно сформированную разметку, в результирующем окне сообщения отобразится сообщение об ошибке. Например, запустите программу и намеренно внесите ошибку в <StackPanel>, скажем, добавив лишнюю букву Р в открывающий элемент. После щелчка на кнопке отобразится сообщение об ошибке вроде показанного на рис. 27.23. Рис. 27.23. Перехват ошибок разметки
1046 Часть VI. Построение настольных пользовательских приложений с помощью WPF Реализация события Closed И, наконец, событие Closed типа Window гарантирует, что данные, введенные в TextBox, сохранятся в файле YourXaml.xaml: private void Window_Closed(object sender, EventArgs e) { // Записать данные из текстового блока в локальный файл *.xaml. File.WriteAllText("YourXaml.xaml", txtXamlData.Text); } Тестирование приложения Запустите приложение и ведите некоторую XAML-разметку в текстовой области. Имейте в виду, что (подобно редактору Kaxaml) это приложение не позволяет указывать атрибуты XAML, связанные с кодом (такие как Class или обработчики событий). Для целей тестирования введите следующий код XAML внутри <Window>: <StackPanel> <Rectangle Fill = "Green" Height = 0" Width = 00" /> <Button Content = "OK!" Height = 0" Width = 00" /> <Label Content ="{x:Type Label}" /> </StackPanel> После щелчка на кнопке появится окно, визуализирующее определение XAML (или, возможно, окно с сообщением об ошибке разбора — будьте внимательны при вводе). На рис. 27.24 показан возможный вывод. Рис. 27.24. Приложение MyXamlPad.exe в действии На этом пример завершен. Наверняка вы сразу же придумаете массу возможных усовершенствований для этого приложения, однако сначала нужно научиться работать с элементами управления WPF и диспетчерами компоновки, содержащими их. Этим мы займемся в следующей главе. Исходный код. Проект MyXamlPad доступен в подкаталоге Chapter 27.
Глава 27. Введение в Windows Presentation Foundation и XAML 1047 Резюме Windows Presentation Foundation (WPF) — это набор инструментов для построения пользовательских интерфейсов, появившийся в версии .NET 3.0. Основная цель WPF состоит в интеграции и унификации множества ранее разрозненных настольных технологий (двух- и трехмерная графика, разработка окон и элементов управления и т.п.) в единую унифицированную программную модель. Помимо этого, в WPF-приложениях обычно используется расширяемый язык разметки приложений (Extendable Application Markup Language —<XAML), который позволяет определять внешний вид и поведение элементов WPF через разметку. Вспомните, что XAML позволяет описывать деревья объектов .NET с применением декларативного синтаксиса. Во время исследований XAML в настоящей главе вы узнали несколько новых частей синтаксиса, включая синтаксис "свойство-элемент" и присоединяемые свойства, а также роль преобразователей типов и расширений разметки XAML. Хотя XAML — ключевой аспект любого профессионального WPF-приложения, в первом примере этой главы было показано, как построить программу WPF исключительно с помощью кода С#. Затем вы узнали, как строить программу WPF из одного XAML (что, однако, не рекомендуется; это был всего лишь учебный пример). Наконец, было продемонстрировано применение "файлов кода", которые позволяют отделять поведение от функциональности. В последнем примере этой главы строилось WPF-приложение, позволяющее программно взаимодействовать с определениями XAML с использованием классов XamlReader и XamlWriter. Кроме того, вы ознакомились с визуальными конструкторами WPF среды Visual Studio 2010. ;
ГЛАВА 28 Программирование с использованием элементов управления WPF В предыдущей главе был заложен фундамент модели программирования WPF, включая рассмотрение классов Window и Application, грамматику XAML и использование файлов кода. Вы также ознакомились с процессом построения WPF-приложений посредством визуальных конструкторов среды Visual Studio 2010. Здесь мы углубимся в конструирование более сложных пользовательских интерфейсов с применением нескольких новых элементов управления и диспетчеров компоновки. В этой главе также будут рассмотрены некоторые связанные с элементами управления WPF темы, такие как программная модель привязки данных и использование команд управления. Вы узнаете, как работать с интерфейсами Ink API и Documents API, позволяющие получать ввод от пера (или мыши) и создавать RTF-документы, используя XML Paper Specification. Наконец, в главе будет показано применение Expression Blend для построения пользовательских интерфейсов для WPF-приложений. Использование Expression Blend в проектах WPF не является обязательным, но эта IDE-среда может значительно упростить разработку, поскольку генерирует необходимый код XAML, используя множество интегрированных визуальных конструкторов, редакторов и мастеров. Обзор библиотеки элементов управления WPF Если только вы не новичок в области построения графических интерфейсов пользователя (что нормально), общее назначение элементов управления WPF не должно вызывать вопросы. Независимо от того, какой набор инструментов для построения графических интерфейсов вы применяли в прошлом (MFC, Java AWT/Swing, Windows Forms, VB 6.0, Mac OS X (Cocoa) или GTK+/GTK#), распространенные элементы управления, представленные в табл. 28.1, скорее всего, покажутся знакомыми.
Глава 28. Программирование с использованием элементов управления WPF 1049 Таблица 28.1. Основные элементы управления WPF Категория элементов управления WPF Примеры членов Назначение Основные пользовательские элементы управления Элементы украшения окон и элементов управления Элементы мультимедиа Элементы управления компоновкой Button, RadioButton, ComboBox, CheckBox, Calendar, DatePicker, Expander, DataGrid, ListBox, ListView, Slider, ToggleButton, TreeView, ContextMenu, ScrollBar, Slider, TabControl, TextBlock, TextBox, RepeatButton, RichTextBox, Label Menu, ToolBar, StatusBar, ToolTip, ProgressBar Image, MediaElement, SoundPlayerAction Border, Canvas, DockPanel, Grid, GridView, GridSplitter, GroupBox, Panel, TabControl, StackPanel, Viewbox, WrapPanel WPF предлагает полное семейство элементов управления, которые можно использовать для построения пользовательских интерфейсов Эти элементы пользовательского интерфейса служат для декорирования рамки объекта Window компонентами для ввода (наподобие Menu) и элементами информирования пользователя (StatusBar, ToolTip и т.п.) Эти элементы управления предоставляют поддержку воспроизведения аудио/видео и визуализации изображений WPF предлагает множество элементов управления, которые позволяют группировать и организовывать другие элементы для управления компоновкой Большинство этих стандартных элементов управления WPF упаковано в пространство имен System. Windows.Controls сборки PresentationFramework.dll. При построении приложения WPF в Visual Studio 2010 большинство этих элементов находится в панели инструментов (ТЪоШох), когда в активном окне открыт визуальный конструктор WPF (рис. 28.1). Как и при создании приложений Windows Forms, эти элементы можно перетаскивать на поверхность визуального конструктора WPF и конфигурировать их в окне Properties (Свойства), о котором вы узнали в предыдущей главе. Хотя Visual Studio 2010 сгенерирует значительный объем XAML автоматически, нет ничего необычного в ручном редактировании разметки. Давайте обратимся к основам. Работа с элементами управления WPF в Visual Studio 2010 Вы можете вспомнить из главы 27, что после помещения элемента управления WPF на поверхность л Common WPF Controls * В (S в ш № СП *л А S3 © □ т и Ж |*ы1 Pointer Border Button CheckBox ComboBox DataGrid Grid Image Label ListBox RadioButton Rectangle StackPanel TabControl TextBlock TextBox л АН WPF Controls * D @ ^ S3 Pointer Border Button Calendar Canvas Рис. 28.1. Панель инструментов Visual Studio 2010 представляет встроенные элементы управления WPF
1050 Часть VI. Построение настольных пользовательских приложений с помощью WPF визуального конструктора Visual Studio 2010 необходимо установить свойство x:Name в окне Properties, поскольку оно позволяет обращаться к объекту в связанном файле кода С#. Кроме того, для генерации обработчиков событий выбранного элемента управления можно использовать вкладку Events (События) окна Properties. Таким образом, с помощью Visual Studio можно сгенерировать следующую разметку для простого элемента управления Button: Button x:Xame=,,btnMyButton" Content="Click lie1" Height=,,23" Width=,,140" Click=,,btnMyButton_Click" /> Здесь свойство Content элемента Button устанавливается в простую строку. Однако, благодаря модели содержимого элементов управления WPF, можно оформить Button, включающий следующее сложное содержимое: <Button x:Name=,,btnMyButtOD" Height=,,121" Width=56" Click=,,btnShowDlg_Click"> <Button.Content^ <StackPanel Height=,,95" Width=28" Orientation=,,Vertical"> <Ellipse Fill = "Red" Width=,,52" Height = ,,45" Margin=,7> <Label Width=9" FontSize=,,20" Content = "Click! " Height=,,36" /> </StackPanel> </Button.Content> </Button> Вы можете также вспомнить, что непосредственный дочерний элемент унаследованного от ContentControl класса — это неявное содержимое, поэтому при указании сложного содержимого определять контекст <Button.Context> явно не нужно. В таком случае достаточно написать следующую разметку: <Button x:Name=,,btnMyButton" Height=21" Wiath=,,156" Click=MbtnShowDlg_ClickM> <StackPanel Height=,,95" Width=,,128" Orientarion=,,Vertical"> <Ellipse Fill = ,,Red" Width=,,52" Height=5" Margin="/> <Label Width=,,59" FontSize=,,20" Content="Clickl " Height=6" /> </StackPanel> </Button> В любом случае вы устанавливаете свойство Content кнопки B<StackPanel> связанных элементов. Создавать такого рода сложное содержимое можно также с использованием визуального конструктора Visual Studio 2010. После определения диспетчер компоновки можно выбирать в визуальном конструкторе в качестве целевого компонента для перетаскивания внутренних элементов управления. Каждый из них можно конфигурировать в окне Properties. Вы должны помнить, что окно Document Outline (Эскиз документа) в Visual Studio 2010 (которое можно открыть, выбрав пункт меню View1^ Other Windows (Вид1^Другие окна)} удобно для проектирования элемента управления WPF со сложным содержимым. Обратите внимание, как на рис. 28.2 показано логическое дерево XAML для созданного элемента Window. Для предварительного просмотра выбранного элемента щелкните на любом из этих узлов. В любом случае, после определения первоначальной разметки можно открыть связанный файл кода С# для добавления логики реализации: private void btnShowDlg_Click(object sender, RoutedEventArgs e) { MessageBox.Show("You clicked the button1"); } В первой части этой главы для построения пользовательских интерфейсов в примерах проектов используется Visual Studio 2010. Здесь мы будем отмечать дополнительные средства по мере необходимости, однако стоит потратить некоторое время на исследование опций окна Properties и функциональности визуального конструктора.
Глава 28. Программирование с использованием элементов управления WPF 1051 Document Outline л Window л Grid л 'Button (btnShowDtg) л L Content л StackPanel „ [-Ellipse к I-Label ^ d Design ti EXAML <6rid> <Button Height=21" HorizontalAlign«ent»"Left" Margin» Naa»e="btnShowOlg" VerticalAlignment»"Top" Width El <Button.Content> В <StackPanel Height»"95" Width=28" Orientation^] <Ellipse Fili-"Red" Width«5" Height»6" <Label Width-9" FontSize«0" Content«"Cl </StaekPanel> </Button-Content> </Button> | ! </6rid> : </Window> 1Q0% , «fij^ ». '.^^Д I Л.г, Ihpse Рис. 28.2. Окно Document Outline в Visual Studio 2010 помогает в навигации по сложному содержимому После этого в главе для построения пользовательских интерфейсов будет применяться Expression Blend. Как будет показано, Expression Blend включает множество встроенных инструментов, генерирующих большую часть необходимой XAML-разметки автоматически. Элементы управления Ink API В дополнение к обычным элементам управления WPF, перечисленным в табл. 28.1, в WPF определены дополнительные элементы для работы с API-интерфейсом "цифровых чернил" (digital Ink API). Этот API-интерфейс полезен при разработке приложений для планшетных ПК (Tablet PC), поскольку позволяет получать ввод от пера. Однако это не означает, что стандартные настольные приложения не могут пользоваться Ink API, так как некоторые определенные в нем элементы управления могут получать ввод от мыши. Пространство имен System.Windows.Ink сборки PresentationCore.dll содержит разнообразные типы, поддерживающие Ink API (например, Stroke и StrokeCollection); однако большинство элементов управления Ink API (такие как InkCanvas и InkPresenter) упакованы в общие элементы управления WPF из пространства имен System.Windows.Controls сборки PresentationFramework.dll. Более подробно Ink API рассматривается далее в главе. Элементы управления документами WPF В WPF предлагаются элементы управления для обработки расширенных документов, позволяя строить приложения, которые поддерживают функциональность в стиле Adobe PDF Используя типы из пространства имен System.Windows.Documents (также из сборки PresentationFramework.dll), можно создавать готовые к печати документы, поддерживающие масштабирование, поиск, пользовательские аннотации ("клейкие" заметки) и прочие развитые средства работы с текстом.
1052 Часть VI. Построение настольных пользовательских приложений с помощью WPF Однако "за кулисами" элементы управления документов не используют API- интерфейсы Adobe PDF, а вместо этого работают с API-интерфейсом XML Paper Specification. Конечные пользователи никакой разницы не заметят, поскольку документы PDF и документы XPS имеют почти идентичный вид и поведение. В действительности доступно множество бесплатных утилит, которые позволяют преобразовывать эти форматы друг в друга на лету. В последующих примерах мы будем иметь дело с некоторыми аспектами элементов управления документами. Общие диалоговые окна WPF В WPF также предоставляются несколько диалоговых окон, таких как OpenFileDialog и SaveFileDialog. Эти диалоговые окна определены внутри пространства имен Microsoft.Win32 сборки PresentationFramework.dll. Работа с каждым из этих диалоговых окон сводится к созданию объекта и вызову метода ShowDialogO: using Microsoft.Win32; namespace WpfControls { public partial class MainWindow : Window { public MainWindow() { InitializeComponent (); } private void btnShowDlg_Click (object sender, RoutedEventArgs e) { // Отобразить диалоговое окно сохранения файла. SaveFileDialog saveDlg = new SaveFileDialog (); saveDlg.ShowDialog (); } } } Как и следовало ожидать, в этих классах определены различные члены, позволяющие устанавливать фильтры файлов и пути каталогов и получать доступ к выбранным пользователем файлам. Некоторые диалоговые окна будут использоваться в последующих примерах; кроме того, будет показано, как строить специальные диалоговые окна для получения пользовательского ввода. Подробные сведения находятся в документации Целью этой главы не является обзор всех членов каждого элемента управления WPF. Вместо этого будет дан обзор различных элементов управления с упором на лежащую в основе программную модель и ключевые службы, общие для большинства элементов управления WPF. Вы также узнаете, как строить пользовательские интерфейсы с помощью Expression Blend и Visual Studio 2010. Чтобы получить полное представление о конкретной функциональности определенного элемента управления, обращайтесь в документацию .NET Framework 4.0 SDK. В частности, элементы управления описаны в разделе Control Library (Библиотека элементов управления) справочной системы, ссылка на который находится ниже ссылки Windows Presentation Foundation Controls (Элементы управления Windows Presentation Foundation), как показано на рис. 28.3.
Глава 28. Программирование с использованием элементов управления WPF 1053 Library Home Viiual Studio 2010 .NET Framework 4 Windows Presentation Foundation Controls Control Library Border BuIletDecorator Button Canvas ChecfcBo* ComboBox ContextMenu DataGiid Doer. Panel Document Viewer Expander Flow/Document PageViewer FlowDocumentReader FlowDocumentScroUVtewer Frame Grid The Windows Presentation Foundation (WPF) control library contains information on the controls provided by Windows Presentation Foundation (WPF), listed alphabetically. In This Section Border BuIletDecorator Button Canvas Checkfiox ComboBox ContextMenu DockPanel DocumentViewer fr Internet | Protected Mode Off /i » ^95% Рис. 28.3. Детальная информация по каждому элементу управления WPF доступна по нажатию клавиши <F1> Управление компоновкой содержимого с использованием панелей Реальное приложение WPF неизбежно содержит изрядное количество элементов пользовательского интерфейса (элементов ввода, графического содержимого, систем меню, строк состояния и т.п.), которые должны быть хорошо организованы внутри содержащего их окна. Кроме того, виджеты пользовательского интерфейса должны вести себя адекватно в случае изменения конечным пользователем размеров всего окна или его части (как в случае окна с разделителем). Для гарантирования того, что элементы управления WPF сохранят свое положение в принимающем окне, предусмотрено значительное количество типов панелей. При объявлении элемента управления непосредственно внутри окна, не имеющего панелей, он размещается в центре окна. Рассмотрим следующее простое объявление окна, содержащего единственный элемент типа Button. Независимо от того, как вы будете изменять размеры окна, виджет пользовательского интерфейса всегда окажется на равном удалении от всех четырех границ клиентской области. <•-- Эта кнопка всегда находится в центре окна --> <Window x:Class="MyWPFApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Fun with Panels'" Height=85" Width=25"> <Button x:Name="btnOK" Height = 00" Width="80" Content="OK"/> </Window> Также вспомните, что попытка разместить несколько элементов прямо в контексте <Window> приводит к ошибке разметки во время компиляции. Причина в том, что свойству Content окна (или любого наследника ContentControl) может быть присвоен только один объект: <!-- Ошибка! Свойство Content неявно устанавливается более одного раза! --> <Window x:Class="MyWPFApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
1054 Часть VI. Построение настольных пользовательских приложений с помощью WPF xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Fun with Panels1" Height=,,285" Width=,,325"> <■-- Два непосредственных дочерних элемента <Window>! --> <Label x:Name="lblInstructions" Width=28" Height=7" FontSize=5" Content="Enter Information"/> <Button x:Name="btnOK" Height = 00" Width="8 0" Content="OK"/> </Window> Понятно, что от окна, которое может содержать единственный элемент, толку мало. Поэтому, когда в окно требуется поместить множество элементов, это делается с помощью панелей. Панель будет содержать все элементы пользовательского интерфейса, представляющие окно, после чего сама панель используется как единственный объект, присваиваемый свойству Content окна. Пространство имен System.Windows.Controls предлагает многочисленные панели, каждая из которых по-своему обслуживает внутренние элементы. Используя панели, вы сможете устанавливать поведение элементов управления при изменении размеров окна пользователем — будут они оставаться в том же месте, куда были помещены во время проектирования, будут располагаться свободным потоком слева направо или сверху вниз, и т.д. В одних панелях допускается помещать другие панели (вроде DockPanel, которая содержит StackPanel с другими элементами), чтобы обеспечить еще более высокую гибкость и степень управления. В табл. 28.2 перечислены некоторые из наиболее часто используемых панелей WPF. Таблица 28.2. Основные панели WPF Панель Назначение Canvas Предлагает классический режим размещения содержимого. Элементы остаются в точности там, где были размещены во время проектирования DockPanel Привязывает содержимое к определенной стороне панели (Тор (верхняя), Bottom (нижняя), Left (левая) или Right (правая)) Grid Располагает содержимое внутри серии ячеек, расположенных в табличной сетке StackPanel Складывает содержимое по вертикали или горизонтали, в зависимости от значения свойства Orientation WrapPanel Позиционирует содержимое слева направо, перенося на следующую строку по достижении границы панели. Последовательность размещения происходит сначала сверху вниз или сначала слева направо, в зависимости от значения свойства Orientation В следующих нескольких разделах будет показано, как использовать эти типы панелей, для чего предопределенные данные XAML будут копироваться в приложение MyXamlPad.exe, созданное в главе 27 (при желании данные можно также загружать и в редактор Kaxaml). Необходимые файлы XAML находятся в подкаталоге PanelMarkup каталога Chapter 28 (рис. 28.4). Позиционирование содержимого внутри панелей Canvas Панель Canvas поддерживает абсолютное позиционирование содержимого пользовательского интерфейса. Если конечный пользователь изменяет размер окна, делая его меньше, чем компоновка, обслуживаемая панелью Canvas, ее внутреннее содержимое становится невидимым до тех пор, пока контейнер вновь не увеличится до размера, равного или больше начального размера области Canvas.
Глава 28. Программирование с использованием элементов управления WPF 1055 Л Favorites ■ Desktop Ш Recent Places м* Libraries [*] Documents л Musk Й GridWithSplitterjtam! *, ScrollViewenxaml {j*j SimpleCanvasjcaml •: SimpleDockPanel.xaml £5 SimpleGrid.xarnl \шттштШтт il, 9/2Я 9/2Я 9/2s[ 9.25 9/25 Рис. 28.4. Для тестирования различных компоновок эти ХАМL-данные будут загружаться в приложение MyXamlPad.exe Для добавления содержимого к Canvas определите необходимые элементы управления внутри области между открывающим (<Canvas>) и закрывающим (</Canvas>) дескрипторами. Затем для каждого элемента управления укажите левый верхний угол с использованием свойств Canvas .Top и Canvas .Left; здесь должна начинаться визуализация. Нижняя правая область для каждом элементе управления может быть задана неявно, за счет установки свойств Canvas.Height и Canvas.Width, либо явно — через свойства Canvas.Right и Canvas.Bottom. Чтобы увидеть Canvas в действии, откройте файл SimpleCanvas.xaml (входящий в состав примеров кода для этой главы) в текстовом редакторе и скопируйте его содержимое в приложение MyXamlPad.exe (или Kaxaml). Вы должны увидеть следующее определение Canvas: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Fun with Panels!" Height=85" Width=25"> <Canvas Background="LightsteelBlue"> <Button x:Name="btnOK" Canvas.Left=12" Canvas.Top=03" Width="80" Content=,,OK"/> <Label x:Name="lblInstructions" Canvas.Left=7" Canvas.Top=4" Width=28" Height=7" FontSize=,,15" Content="Enter Car Information"/> <Label x:Name="lblMake" Canvas.Left=7" Canvas.Top=0" Content="Make"/> <TextBox x:Name="txtMake" Canvas.Left="94" Canvas.Top=0" Width=93" Height=5"/> <Label x:Name="lblColor" Canvas.Left=7" Canvas.Top=09" Content="Color"/> <TextBox x:Name="txtColor" Canvas.Left="94" Canvas.Top=07" Width=93" Height=5"/> <Label x:Name="lblPetName" Canvas.Left=7" Canvas.Top=55" Content="Pet Name"/> <TextBox x:Name="txtPetName" Canvas.Left="94" Canvas.Top=53" Width=93" Height=5"/> </Canvas> </Window> I Щелчок на кнопке View Xaml (Показать Xaml) приведет к отображению окна, показанного на рис. 28.5.
1056 Часть VI. Построение настольных пользовательских приложений с помощью WPF Рис. 28.5и Диспетчер компоновки Canvas обеспечивает абсолютное позиционирование содержимого Обратите внимание, что порядок, в котором объявляются элементы содержимого внутри Canvas, для вычисления местоположения не используется; вместо этого оно базируется на размерах элемента и свойствах Canvas.Top, Canvas.Bottom, Canvas.Left и Canvas.Right. На заметку! Если подэлементы внутри Canvas не определяют специфическое местоположение с использованием синтаксиса присоединяемых свойств (например, Canvas.Left и Canvas.Top), они автоматически прикрепляются к верхнему левому углу Canvas. Хотя применение типа Canvas может показаться предпочтительным способом расположения содержимого (поскольку выглядит столь знакомым), этому подходу присущи некоторые ограничения. Во-первых, элементы внутри Canvas не изменяют себя динамически при применении стилей или шаблонов (например, их шрифты не изменяются). Во-вторых, Canvas не пытается сохранять элементы видимыми, когда пользователь изменяет размер окна в меньшую сторону. ■ Возможно, наилучшим применением типа Canvas является позиционирование графического содержимого. Например, при построении изображения с использованием XAML определенно понадобится, чтобы все линии, фигуры и текст оставались именно там, где они были размещены, а не позволять им динамически перемещаться при изменении размеров окна. Мы еще вернемся к Canvas в следующей главе, когда речь пойдет о службах визуализации графики WPF. Позиционирование содержимого внутри панелей WrapPanel Wrap Panel позволяет определить содержимое, которое обтекает панель при изменении размеров окна. При позиционировании элементов в WrapPanel вы не указываете положение относительно верхней, левой, нижней и правой границ, как это обычно делается с Canvas. Однако для каждого подэлемента могут быть определены значения Height и Width (наряду с другими значениями свойств) для управления общим размером контейнера. Поскольку содержимое внутри WrapPanel не стыкуется к определенной стороне панели, порядок определения элементов важен (содержимое визуализируется от первого элемента к последнему). В файле SimpleWrapPanel.xaml имеется следующая разметка (заключенная в определении <Window>): <WrapPanel Background=llLightSteelBlue"> <Label x:Name=,,lblInstructionM Width=M328M Height=,,27" FontSize=,,15" Content="Enter Car Information"^
Глава 28. Программирование с использованием элементов управления WPF 1057 <Label x:Name="lblMake" Content="Make"/> <TextBox Name="txtMake" Width=93" Height=M25n/> <Label x:Name=,,lblColor" Content="Color,,/> <TextBox Name=,,txtColor" Width=93" Height=M25"/> <Label x:Name=,,lblPetName" Content="Pet Name"/> <TextBox x,:Name=,,txtPetNameM Width=,,193" Height=M25"/> <Button x:Name=,,btnOK" Width=n80" Content=MOKn/> </WrapPanel> Загрузив эту разметку и попробовав изменить ширину окна, содержимое будет перетекать внутри окна слева направо (рис. 28.6). И j Fun with Panels! Sli&i] Enter Car Information Рис. 28.6. Содержимое WrapPanel ведет себя подобно традиционной HTML-странице По умолчанию содержимое WrapPanel перетекает слева направо. Однако если изменить значение свойства Orientation на Vertical, то содержимое будет располагаться сверху вниз: <WrapPanel Orientation ="Vertical"> Панель WrapPanel (как и некоторые другие типы панелей) может быть объявлена с указанием значений ItemWidth и ItemHeight, которые управляют размером по умолчанию каждого элемента. Если подэлемент предоставляет собственные значения Height и/или Width, он позиционируется относительно размера установленного для него панелью. Рассмотрим следующую разметку: <WrapPanel Background="LightSteelBlue" ItemWidth =00" ItemHeight =0"> <Label x:Name="lblInstruction" FontSize=5" Content="Enter Car Information"/> <Label x:Name="lblMake" Content="Make"/> <TextBox x:Name="txtMake"/> <Label x:Name="lblColor" Content="Color"/> <TextBox x:Name="txtColor"/> <Label x:Name="lblPetName" Content="Pet Name"/> <TextBox x:Name="txtPetName"/> <Button x:Name="btnOK" Width ="80" Content="OK"/> </WrapPanel> После визуализации получается окно, показанное на рис. 28.7 (обратите внимание на размер и положение элемента управления Button, для которого было задано уникальное значение Width). Рис. 28.7. Панель WrapPanel может устанавливать ширину и высоту каждого заданного элемента
1058 Часть VI. Построение настольных пользовательских приложений с помощью WPF Взглянув на рис. 28.7, трудно не согласиться с тем, что Wrap Panel — обычно не лучший выбор для непосредственного расположения содержимого в окне, поскольку его элементы могут перемешиваться, когда пользователь изменяет размеры окна. В большинстве случаев WrapPanel будет подэлементом панели другого типа, позволяя небольшой области окна заворачивать свое содержимое при изменении размера (как, например, элемент управления Toolbar). Позиционирование содержимого внутри панелей StackPanel Подобно WrapPanel, элемент управления StackPanel организует содержимое в одну строку, которая может быть ориентирована горизонтально или вертикально (по умолчанию), в зависимости от значения, присвоенного свойству Orientation. Однако отличие между ними состоит в том, что StackPanel не пытается переносить содержимое при изменении размера окна пользователем. Вместо этого элементы внутри StackPanel просто растягиваются (в зависимости от ориентации), приспосабливаясь к размеру самой панели StackPanel. Например, в файле SimpleStackPanel.xaml содержится следующая разметка, которая дает вывод, показанный на рис. 28.8: <StackPanel Background=,,LightSteelBlue"> <Label x:Name="lblInstruction11 FontSize=511 Content="Enter Car Information"/> <Label x:Name=,,lblMake" Content="Make,,/> <TextBox x:Name="txtMake,,/> <Label x:Name=,,lblColor" Content=,:Color"/> <TextBox Name="txtColor,,/> <Label x:Name=,,lblPetName" Content="Pet Name"/> <TextBox x:Name=,,txtPetName"/> <Button x:Name=,,btnOK" Content="OK,,/> </StackPanel> Если присвоить свойству Orientation значение Horizontal, то визуализированный вывод станет таким, как показано на рис. 28.9: <StackPanel Orientation ="Horizontalll> Рис. 28.8. Вертикальное расположение содержимого f \ > Fun with Panels1 л Enter Car Information rtake This will resize as I typej Color Pet Name _ 1 OK _J Рис. 28.9. Горизонтальное расположение содержимого Опять-таки, как и WrapPanel, использовать StackPanel для прямого расположения содержимого окна редко когда придется. Вместо этого StackPanel больше подходит в качестве подпанели какой-то главной панели.
Глава 28. Программирование с использованием элементов управления WPF 1059 Позиционирование содержимого внутри панелей Grid Из всех панелей, предлагаемых API-интерфейсом WPF, наиболее гибкой является Grid. Подобно HTML-таблице, Grid может состоять из набора ячеек, каждая из которых имеет свое содержимое. При определении Grid выполняются следующие шаги. 1. Определение и конфигурирование каждого столбца. 2. Определение и конфигурирование каждой строки. 3. Назначение содержимого каждой ячейке сетки с использованием синтаксиса присоединяемых свойств. На заметку! Если не определить никаких строк и столбцов, то по умолчанию <Grid> будет состоять из одной ячейки, занимающей всю поверхность окна. Кроме того, если не указать ячейку для подэлемента <Grid>, он автоматически разместится в столбце 0 и строке 0. Первые два шага (определение столбцов и строк) выполняются за счет использования элементов <Grid.ColumnDef initions> и <Grid.RowDef initions>, которые содержат коллекции элементов <ColumnDefinition> и <RowDefinition>, соответственно. Поскольку каждая ячейка внутри сетки является настоящим объектом .NET, можно должным образом конфигурировать внешний вид и поведение каждого элемента. Ниже приведено простое определение <Grid> (из файла SimpleGrid.xaml), которое организует содержимое пользовательского интерфейса, как показано на рис. 28.10: <Grid ShowGridLines =MTrue" Background ="AliceBluell> <!— Определить строки/столбцы --> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <!-- Добавить элементы в ячейки сетки --> <Label x:Name="lblInstruction11 Grid.Column =" Grid.Row =" FontSize=,,15" Content=MEnter Car Information"/^ <Button x:Name=MbtnOKM Height =0M Grid.Column =" Grid.Row =" Content=,,0K,7> <Label x:Name=,,lblMake" Grid.Column =" Grid.Row =" Content=,,Make"/> <TextBox x:Name=,,txtMake" Grid.Column =" Grid.Row =" Width=93" Height=,,25,7> <Label x:Name=,,lblColorM Grid.Column =" Grid.Row =" Content="Color,,/> <TextBox x:Name=,,txtColor" Width=93" Height=5" Grid.Column =" Grid.Row =" /> <!— Чтобы сделать картину интереснее, добавим цвет ячейке имени --> <Rectangle Fill ="LightGreen" Grid.Column ="l" Grid.Row ="l" /> <Label x:Name=,,lblPetNameM Grid.Column =" Grid.Row =" Content="Pet Name"/> <TextBox x:Name=MtxtPetNameM Grid.Column ="l" Grid.Row ="l" Width=93" Height=5,,/> </Grid> Обратите внимание, что каждый элемент (включая элемент Rectangle светло-зеленого цвета) прикрепляется к ячейке сетки, используя присоединяемые свойства Grid.Row и Grid.Column. Порядок ячеек по умолчанию начинается с левой верхней, которая указана с помощью Grid.Column=" и Grid.Row=". Учитывая, что сетка состоит всего из четырех ячеек, нижняя правая ячейка может быть идентифицирована как Grid.Column=,,l" и Grid.Row="l".
1060 Часть VI. Построение настольных пользовательских приложений с помощью WPF Рис. 28.10. Панель Grid в действии Панели Grid с типами GridSplitter Панели Grid также могут поддерживать разделители (splitters). Как известно, разделители позволяют конечному пользователю изменять размеры столбцов и строк сетки. При этом содержимое каждой изменяемой в размере ячейки реорганизует себя на основе содержащихся в ней элементов. Добавить разделители к Grid довольно просто: для этого необходимо определить элемент управления <GridSplitter> и с использованием синтаксиса присоединяемых свойств указать строку или столбец, к которому он относится. Имейте в виду, что для обеспечения видимости на экране разделителя потребуется указать значение Width или Height (в зависимости от вертикального или горизонтального разделения). Ниже показана простая панель Grid с разделителем на первом столбце (Grid.Column=") из файла GridWithSplitter.xaml: <Grid Background =llAliceBlue"> <!-- Определить столбцы --> <Grid.ColumnDefinitions> <ColumnDefmition Width =llAuto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <!-- Добавить метку в ячейку 0 --> <Label x:Name="lblLeft11 Background ="GreenYellow11 Grid.Column=M0M Content =MLeft|,,/> <!— Определить разделитель —> <GridSplitter Grid.Column =" Width ="/> <!— Добавить метку в ячейку 1 --> <Label x:Name=,,lblRight" Grid. Column =" Content ="Right! "/> </Grid> Прежде всего, обратите внимание, что столбец с разделителем имеет свойство Width, значение которого установлено в Auto. Кроме того, элемент <GridSplitter> использует синтаксис присоединяемых свойств для установки столбца, с которым он работает. Если вы взглянете на вывод, то увидите там 5-пиксельный разделитель, который позволяет изменять размер каждого элемента Label. Поскольку для каждого элемента Label не было задано свойство Height или Width, они заполняют всю ячейку (рис. 28.11). Позиционирование содержимого внутри панелей DockPanel Обычно DockPanel используется в качестве главной панели, содержащей любое количество дополнительных панелей для группирования их в связанные контейнеры. Панели DockPanel применяют синтаксис присоединяемых свойств (как было показано в типах Canvas и Grid) для управления тем, куда будет стыкован каждый элемент внутри DockPanel.
Глава 28. Программирование с использованием элементов управления WPF 1061 Рис. 28.11. Панель Grid с разделителями В файле SimpleDockPanel.xaml определена следующая простая панель DockPanel, которая дает результат, показанный на рис. 28.12: <DockPanel LastChildFill ="True"> <!-- Прикрепить элементы к панели --> <Label x:Name="lblInstruction11 DockPanel. Dock ="Top" FontSize=511 Content="Enter Car Information"/> <Label x:Name="lblMake11 DockPanel. Dock ="Left" Content="Makell/> <Label x:Name=MlblColorM DockPanel.Dock ="Right" Content=MColorM/> <Label x:Name=,,lblPetName" DockPanel. Dock =,,Bottom" Content="Pet Name'7> <Button x:Name="btnOK" Content="OK"/> </DockPanel> lji0 Fun with Panels! Enter Car Information Make Color Рис. 28.12. Простая панель DockPanel На заметку! В случае добавления множества элементов к одной стороне DockPanel, они выстраиваются вдоль указанной грани в порядке объявления. Преимущество использования типов DockPanel состоит в том, что при изменении размеров окна пользователем каждый элемент остается прикрепленным к указанной (в DockPanel.Dock) стороне панели. Также обратите внимание, что в открывающем дескрипторе <DockPanel> атрибут LastChildFill устанавливается в true. Учитывая, что элемент Button не задает никакого значения DockPanel.Dock, он будет растянут, чтобы занять все оставшееся место. Включение прокрутки в типах панелей В WPF предусмотрен класс <ScrollViewer>, которые обеспечивает возможность прокрутки данных внутри объектов панелей. Ниже показан фрагмент разметки из файла ScrollViewer.xaml: <ScrollViewer> <StackPanel> <Button Content="First11 Background="Green11 Height=ll40"/> <Button Content=MSecondM Background=,,Red" Height=0"/>
1062 Часть VI. Построение настольных пользовательских приложений с помощью WPF <Button Content=,,Third" Background=,,Pink" Height=0,,/> <Button Content=MFourthM Background=,,YellowM Height=0M/> <Button Content=MFifth" Background=MBlueM Height=M40M/> </StackPanel> </ScrollViewer> Результат приведенного определения XAML можно видеть на рис. 28.13. Рис. 28.13. Работа с классом ScrollViewer Как и следовало ожидать, каждая панель предоставляет множество членов, позволяющих настраивать расположение содержимого. В частности, элементы управления WPF поддерживают два таких свойства (Padding и Margin), которые позволяют самому элементу информировать панель о том, как с ним следует обращаться. В частности, свойство Padding контролирует свободное место, которое должно окружать внутренний элемент управления, а свойство Margin — пространство, окружающее элемент управления извне. На этом экскурс в основные типы панелей WPF и различные способы позиционирования их содержимого завершен. Далее мы перейдем к рассмотрению примера, в котором используются вложенные панели для создания системы компоновки главного окна. Построение главного окна с использованием вложенных панелей Как упоминалось ранее, в типичном окне WPF для получения нужной системы компоновки применяется не одиночный элемент управления типа панели, а одни панели вкладываются в другие. Ниже будет показано, как вкладывать одни панели в другие, а также как использовать множество новых элементов управления WPF. Изучение будет проходить на примере приложения простого текстового процессора. Откройте Visual Studio 2010 и создайте новое приложение WPF по имени MyWordPad. Наша цель состоит в том, чтобы получить компоновку, в которой главное окно имеет расположенную в верхней части систему меню, под ней — панель инструментов и нижней части окна — строка состояния. Строка состояния будет содержать область для хранения текстовых сообщений, отображаемых при выборе пункта меню (или кнопки в панели инструментов), а система меню и панель инструментов предоставят триггеры пользовательского интерфейса для закрытия приложения и отображения вариантов правописания в элементе Expander. На рис. 28.14 показана начальная компоновка, в которой отображаются подсказки по правописанию для "ХАМЕ'. Обратите внимание, что две кнопки панели инструментов не содержат в себе ожидаемых изображений, а только текстовые значения. Хотя этого явно не достаточно для профессионального приложения, установка изображений для кнопок панели инструментов обычно предполагает использование встроенных ресурсов, и эта тема рассматривается в главе 30 (пока обойдемся текстом). При наведении на кнопку Check (Проверить) курсор мыши изменяется, и в единственной панели строки состояния отображается полезное сообщение пользовательского интерфейса.
Глава 28. Программирование с использованием элементов управления WPF 1063 CALM AXEL UXMAL BALM PALM AM AMYL EXAM CAMEL Show Spelling Suggestions Яй^Ц, is a very useful tool when building WPP applications. Рис. 28.14. Использование вложенных панелей для построения пользовательского интерфейса окна Чтобы приступить к построению описанного пользовательского интерфейса, модифицируйте начальное определение XAML типа Window для использования в качестве дочернего элемента <DockPanel> вместо <Grid> по умолчанию: <Window x:Class="MySpellChecker.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MySpellChecker" Height=31" Width=08" WindowstartupLocation ="CenterScreen"> <!-- Эта панель устанавливает содержимое окна —> <DockPanel> </DockPanel> </Window> Построение системы меню Системы меню в WPF представлены классом Menu, который поддерживает коллекцию объектов Menu Item. При построении системы меню в XAML каждый Menu Item может обрабатывать различные события, из которых в первую очередь следует упомянуть Click, происходящее, когда пользователь выбирает подэлемент. В рассматриваемом примере будут созданы два пункта меню верхнего уровня (File (Файл) и Tools (Сервис); меню Edit (Правка) строится позже), которые будут содержать в себе подэлементы Exit (Выход) и Spelling Hints (Подсказки по правописанию), соответственно. В дополнение к обработке события Click для каждого подэлемента будет также организована обработка событий MouseEnter и MouseExit, которые используются для установки текста в строке состояния. Добавьте следующую разметку в контекст элемента <DockPanel> (используйте окно Properties (Свойства) среды Visual Studio 2010, чтобы сократить объем ручного ввода): <'— Закрепить систему меню вверху —> <Menu DockPanel.Dock ="Top" HorizontalAlignment="Left" Background="White" BorderBrush ="Black"> <MenuItem Header="_File"> <Separator/> <MenuItem Header ="_Exit" MouseEnter ="MouseEnterExitArea" MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/> </MenuItem> <MenuItem Header="_Tools"> <MenuItem Header ="_Spelling Hints" MouseEnter ="MouseEnterToolsHintsArea" MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click"/> </MenuItem> </Menu>
1064 Часть VI. Построение настольных пользовательских приложений с помощью WPF Обратите внимание, что система меню стыкована к верхней части DockPanel. Кроме того, элемент <Separator> используется для добавления тонкой горизонтальной линии в систему меню, непосредственно перед пунктом Exit. Значения Header для каждого Menu Item содержат символ подчеркивания (например, Exit). Это указывает символ, который станет подчеркнутым, когда пользователь нажмет клавишу <Alt> (для ввода клавиатурного сокращения). После построения системы меню необходимо реализовать различные обработчики событий. Прежде всего, понадобится обработчик пункта меню File^Exit по имени FileExit_Click(), который просто закрывает окно, что, в свою очередь, приводит к завершению приложения, поскольку это окно самого высшего уровня. Обработчики событий MouseEnter и MouseExit для каждого из подэлементов должны обновлять строку состояния; однако пока мы просто оставим их пустыми. И, наконец, обработчик ToolsSpellingHintsClickO для пункта меню Tools«=>Spelling Hints также пока оставим пустым. Ниже показаны текущие обновления файла отделенного кода: public partial class MainWindow : System.Windows.Window { public MainWindow() { InitializeComponent (); } protected void FileExit_Click(object sender, RoutedEventArgs args) { // Закрыть это окно. this.Close (); } protected void ToolsSpellingHints_Click(object sender, RoutedEventArgs args) { } protected void MouseEnterExitArea(object sender, RoutedEventArgs args) { } protected void MouseEnterToolsHintsArea(object sender, RoutedEventArgs args) { } protected void MouseLeaveArea(object sender, RoutedEventArgs args) { Построение панели инструментов Панели инструментов (представленные в WPF классом ToolBar) обычно предлагают альтернативный способ активизации пунктов меню. Добавьте следующую разметку непосредственно после закрывающего дескриптора определения <Menu>: <!-- Разместить панель инструментов ниже области меню --> <ToolBar DockPanel.Dock ="Top" > <Button Content ="Exit" MouseEnter ="MouseEnterExitArea" MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/> <Separator/> <Button Content ="Check" MouseEnter ="MouseEnterToolsHintsArea" MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click" Cursor="Help" /> </ToolBar> Элемент управления ToolBar состоит из двух элементов Button, которые предназначены для обработки тех же самых событий теми же методами из файла кода. С по-
Глава 28. Программирование с использованием элементов управления WPF 1065 мощью этой техники можно дублировать обработчики для обслуживания как пунктов меню, так и кнопок панели управления. Хотя в этой панели используются типичные нажимаемые кнопки, имейте в виду, что ToolBar "является" ContentControl, и потому на его поверхность можно помещать любые типы (раскрывающиеся списки, изображения, графику и т.п.). Единственное, что еще может интересовать здесь — это то, что кнопка Check поддерживает специальный курсор мыши через свойство Cursor. На заметку! Элемент ToolBar может быть дополнительно помещен в элемент <Тоо1ВагТгау>, который управляет компоновкой, стыковкой и перетаскиванием набора объектов ToolBar. За подробной информацией обращайтесь в документацию .NET Framework 4.0 SDK. Построение строки состояния Элемент управления StatusBar прикрепляется к нижней части <DockPanel> и содержит единственный элемент управления <TextBlock>, который пока еще в этой главе не использовался. Элемент TextBlock может применяться для хранения текста с добавлением форматирования, такого как полужирный текст, подчеркнутый текст, разрывы строк и т.д. Поместите следующую разметку непосредственно после предшествующего определения ToolBar: <!-- Разместить строку состояния внизу --> <StatusBar DockPanel. Dock ="Bottom11 Background="Beige" > <StatusBarItem> <TextBlock Name=,,statBarText" Text=,,Ready"/> </StatusBarItem> </StatusBar> Завершение дизайна пользовательского интерфейса Финальный аспект дизайна разрабатываемого пользовательского интерфейса заключается в определении элемента Grid с разделителями и двумя столбцами. Слева будет элемент управления Expander, который отобразит список подсказок по правописанию, помещенный в <StackPanel>, а справа — элемент TextBox с поддержкой многострочного текста, линеек прокрутки и включенной проверкой правописания. Элемент <Grid> целиком может быть размещен в левой части родительской панели <DockPanel>. Чтобы завершить определение пользовательского интерфейса окна, добавьте следующую XAML-разметку непосредственно под разметкой, описывающей StatusBar: <Grid DockPanel. Dock ="Left" Background ="AliceBlueII> <i-- Определить строки и столбцы --> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <GridSplitter Grid.Column =" Width =" Background ="Gray" /> <StackPanel Grid. Column=11 VerticalAlignment ="Stretch11 > <Label Name=,,lblSpellingInstructions" FontSize=,,14" Margin=0, Ю, 0, 0"> Spelling Hints </Label> <Expander Name=IIexpanderSpelling" Header ="Try these!" Margin=0,10,10,10"> <!— Будет заполняться программно --> <Label Name =,,lblSpellingHints" FontSize =2,,/> </Expander> </StackPanel> <i— это будет областью для ввода --> <TextBox Grid.Column ="
1066 Часть VI. Построение настольных пользовательских приложений с помощью WPF SpellCheck.IsEnabled =IITrue" AcceptsReturn ="True11 Name =IItxtData" FontSize =4" BorderBrush'="Blue" VerticalScrollBarVisibility=,,Auto" HorizontalScrollBarVisibility=llAuto"> </TextBox> </Grid> Реализация обработчиков событий MouseEnter/MouseLeave К этому моменту пользовательский интерфейс окна готов. Осталось предоставить реализацию остальных обработчиков событий. Начните с обновления файла кода С# так, чтобы каждый из обработчиков MouseEnter и MouseLeave устанавливал в текстовой панели строки состояния соответствующий текст сообщения для конечного пользователя: public partial class MainWindow : System.Windows.Window { protected void MouseEnterExitArea(object sender, RoutedEventArgs args) statBarText.Text = "Exit the Application"; protected void MouseEnterToolsHintsArea(object sender, RoutedEventArgs args) statBarText.Text = "Show Spelling Suggestions"; protected void MouseLeaveArea(object sender, RoutedEventArgs args) statBarText.Text = "Ready"; } Теперь приложение можно запустить. Обратите внимание, что текст в строке состояния меняется в зависимости от выбранного пункта меню или кнопки панели инструментов. Реализация логики проверки правописания API-интерфейс WPF имеет встроенную поддержку проверки правописания, которая не зависит от продуктов Microsoft Office. Это значит, что использовать уровень взаимодействия с СОМ для обращения к функции проверки правописания Microsoft Word не придется, а вместо этого данная функциональность добавляется всего несколькими строками кода. Вспомните, что при определении элемента управления <TextBox> свойство SpellCheck.IsEnabled было установлено в true. После этого неправильно написанные слова будут подчеркиваться красной волнистой линией, как это делается в Microsoft Office. Более того, лежащая в основе программная модель предоставляет доступ к механизму проверки правописания, который позволяет получить список предполагаемых слов, написанных с ошибкой. Добавьте в метод ToolSpellingHints_Click() следующий код: protected void ToolsSpellingHints_Click(object sender, RoutedEventArgs args) { string spellingHints = string.Empty; // Попытка получить ошибку правописания в текущем положении курсора ввода. SpellingError error = txtData.GetSpellingError(txtData.Caretlndex);
Глава 28. Программирование с использованием элементов управления WPF 1067 if (error != null) { // Построить строку с подсказками по правописанию. foreach (string s in error.Suggestions) { spellingHints += string.Format("{0}\n", s) ; } // Отобразить подсказки и раскрыть элемент Expander. lblSpellingHints.Content = spellingHints; expanderSpelling.IsExpanded = true; } } Приведенный код достаточно прост. С помощью свойства Caretlndex определяется текущее положение курсора ввода в текстовом поле и извлекается объект SpellingError. Если в указанном месте есть ошибка (т.е. значение не равно null), производится проход в цикле по списку подсказок с использованием свойства Suggestions. После получения все подсказки по правописанию помещаются в метку Label внутри элемента Expander. Вот и все! С помощью всего нескольких строк процедурного кода (и приличной порции XAML-разметки) заложен фундамент для будущего текстового процессора. После освоения управляющих команд можно будет добавить несколько новых возможностей. Понятие управляющих команд WPF В Windows Presentation Foundation предлагается поддержка независимых от элементов управления событий через управляющие команды (control commands). Обычное событие .NET определяется внутри некоторого базового класса и может быть использовано этим классом и его наследниками. Таким образом, нормальные события .NET очень тесно связаны с классом, в котором они определены. В отличие от этого, управляющие команды WPF представляют собой подобные событиям сущности, которые независимы от конкретного элемента управления и во многих отношениях могут успешно применяться к многочисленным (и внешне несвязанным) типам элементов управления. Приведем несколько примеров: WPF поддерживает команды Сору (Копировать), Paste (Вставить) и Cut (Вырезать), которые могут применяться к широкому разнообразию элементов пользовательского интерфейса (пунктам меню, кнопкам панели инструментов, специальным кнопкам), а также клавиатурные комбинации (<Ctrl+C>, <Ctrl+V> и т.д.). Хотя другие наборы инструментов для построения пользовательских интерфейсов (вроде Windows Forms) предлагают для этих целей стандартные события, в результате получается избыточный и трудный в сопровождении код. В модели WPF команды могут использоваться в качестве альтернативы. В результате получается более компактный и гибкий код. Внутренние объекты управляющих команд WPF поставляется с множеством встроенных управляющих команд, каждая из которых может быть ассоциирована в соответствующей горячей клавишей (или другим источником ввода). Говоря языком программиста, управляющая команда WPF — это любой объект, поддерживающий свойство (часто называемое Command), которое возвращает объект, реализующий интерфейс ICommand: public interface ICommand { // Возникает, когда происходит изменение режима,
1068 Часть VI. Построение настольных пользовательских приложений с помощью WPF // должна или нет выполняться данная команда. event EventHandler CanExecuteChanged; // Задает метод, определяющий, может ли // команда выполняться в ее данном состоянии. bool CanExecute (object parameter); // Определяет метод, вызываемый для выполнения команды. void Execute(object parameter); } Хотя можно предоставить собственную реализацию этого интерфейса для создания управляющей команды, шансы делать это довольно незначительны, учитывая, что функциональность пяти классов WPF предлагает около сотни готовых объектов команд. В этих классах определены многочисленные свойства, представляющие специфические объекты команд, каждый из которых реализует интерфейс ICommand. В табл. 28.3 описаны некоторые стандартные объекты команд (более подробную информацию ищите в документации .NET Framework 4.0 SDK). Таблица 28.3. Стандартные объекты команд WPF Класс WPF Объекты команд Назначение ApplicationCommands ComponentCommands MediaCommands NavigationCommands EditingCommands Close, Copy, Cut, Delete, Find, Open, Paste, Save, SaveAs, Redo, Undo MoveDown, MoveFocusBack, MoveLeft, MoveRight, ScrollToEnd, ScrollToHome BoostBase, ChannelUp, ChannelDown, FastForward, NextTrack, Play, Rewind, Select,Stop BrowseBack, BrowseForward, Favorites, LastPage, NextPage, Zoom AlignCenter, CorrectSpellingError, DecreaseFontSize, EnterLineBreak, EnterParagraphBreak, MoveDownByLine, MoveRightByWord Разнообразные команды уровня приложения. Разнообразные команды, которые являются общими для компонентов пользовательского интерфейса. Разнообразные команды, связанные с мультимедиа. Разнообразные команды, связанные с навигационной моделью WPF. Разнообразные команды, связанные с API- интерфейсом документов WPF. Подключение команд к свойству Command Для подключения любой из команд к элементу пользовательского интерфейса, поддерживающему свойство Command (такому как Button или Menultem), прнадобится сделать совсем немного. Для примера модифицируем текущую систему меню, добавив новый пункт верхнего уровня по имени Edit (Правка) с тремя подэлементами, позволяющими копировать, вставлять и вырезать текстовые данные: <Menu DockPanel.Dock ="Top" HorizontalAlignment="Left" Background="White" BorderBrush ="Black"> <MenuItem Header="_File" Click ="FileExit_Click" > <Separator/>
Глава 28. Программирование с использованием элементов управления WPF 1069 <MenuItem Header ="_Exit" MouseEnter ="MouseEnterExitArea" MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/> </MenuItem> <!— Новые пункты меню с командами --> <MenuItem Header="_Edit"> <MenuItem Command ="ApplicationCommands.Copy"/> <MenuItem Command ="ApplicationCommands.Cut"/> <MenuItem Command ="ApplicationCommands.Paste"/> </MenuItem> <MenuItem Header="_Tools"> <MenuItem Header ="_Spelling Hints" MouseEnter ="MouseEnterToolsHintsArea" MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click"/> </MenuItem> </Menu> Обратите внимание, что каждый подэлемент в меню Edit имеет значение, присвоенное его свойству Command. За счет этого пункт меню автоматически получает корректное имя и горячую клавишу (например, <Ctrl+C> для операции вырезания) в интерфейсе меню, и приложение теперь знает, как копировать, вырезать и вставлять, без написания процедурного кода. Запустив приложение и выделив некоторую часть текста, сразу же можно пользоваться новыми пунктами меню. Вдобавок приложение также сможет реагировать на стандартную операцию щелчка правой кнопкой мыши, предлагая пользователю те же самые опции (рис. 28.15). Рис. 28.15. Объекты команд предлагают полезный набор встроенной функциональности Подключение команд к произвольным действиям Если необходимо подключить объект команды к произвольному событию (специфичному для приложения), то для этого понадобится написать процедурный код. Это несложно, но требует чуть больше логики, чем можно видеть в XAML. Например, предположим, что все окно должно реагировать на нажатие клавиши <F1>, активизируя связанную справочную систему. Пусть в файле кода для главного окна определен новый метод по имени SetFlCommandBindingO, который вызывается в конструкторе после вызова InitializeComponent(): public MainWindow() { InitializeComponent (); SetFlCommandBindingO ;
1070 Часть VI. Построение настольных пользовательских приложений с помощью WPF Этот новый метод программно создает новый объект CommandBinding, который можно использовать всякий раз, когда нужно привязать объект команды к заданному обработчику событий приложения. Сконфигурируем объект CommandBinding для работы с командой ApplicationCommands.Help, которая автоматически доступна по нажатию <F1>: private void SetFlCommandBinding () { CommandBinding helpBinding = new CommandBinding(ApplicationCommands.Help); helpBinding.CanExecute += CanHelpExecute; helpBinding.Executed += HelpExecuted; CommandBindings.Add(helpBinding) ; } Большинство объектов CommandBinding будет обрабатывать событие CanExecute (которое позволяет указать команду на основе операций программы) и событие Executed (в котором определяется то, что должно произойти в ответ на команду). Добавьте показанные ниже обработчики событий к типу-наследнику Window (обратите внимание на формат каждого метода, которого требуют ассоциированные делегаты): private void CanHelpExecute(object sender, CanExecuteRoutedEventArgs e) { // Если нужно предотвратить выполнение команды, // следует установить CanExecute в false. е.CanExecute = true; } private void HelpExecuted(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("Look, it is not that difficult. Just type something1", "Help!"); } В предыдущем фрагменте кода метод CanHelpExecute() реализован так, чтобы справка по нажатию <F1> всегда работала, для чего просто возвращается true. Если в каких-то ситуациях справочная система отображаться не должна, тогда нужно вернуть false. Наша "справочная система", отображаемая внутри HelpExecuteO — это всего лишь простое окно сообщения. Теперь можно запустить приложение. После нажатия <F1> отображается не слишком полезная справочная система (рис. 28.16). Рис. 28.16. Пример специальной справочной системы
Глава 28. Программирование с использованием элементов управления WPF 1071 Работа с командами Open и Save В завершение текущего примера добавим функциональность сохранения текстовых данных во внешнем файле и открытия файлов *.txt для редактирования. Можно пойти длинным путем, вручную добавив программную логику, которая включает и отключает пункты меню в зависимости от того, есть ли данные внутри TextBox. Однако для сокращения затрат можно воспользоваться командами. Начнем с обновления элемента <MenuItem>, который представляет меню File высшего уровня, добавив два новых подменю, использующих объекты Save и Open класса ApplicationCommands: <MenuItem Header="_File"> <MenuItem Command ="ApplicationCommands.Open"/> <MenuItem Command =llApplicationCommands . Save"/> <Separator/> <MenuItem Header ="_Exit" MouseEnter ="MouseEnterExitArea" MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/> </MenuItem> Как уже упоминалось, все объекты команд реализуют интерфейс ICommand, определяющий два события (CanExecute и Executed). Теперь нужно позволить окну выполнять эти команды. Это делается наполнением коллекции CommandBindings, поддерживаемой окном. Чтобы сделать это в XAML, необходимо воспользоваться синтаксисом "свойство-элемент" для определения области <Window.CommandBindings>, куда будут помещены определения <CommandBinding>. Модифицируйте определение <Window>, как показано ниже: <Window х:Class="MyWordPad.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MySpellChecker" Height=31" Width=08" WmdowStartupLocation ="CenterScreen" > <!— Это информирует Window, какие обработчики вызывать при проверке команд Open и Save. --> <Window.CommandBindings> <CommandBinding Command="ApplicationCommands.Open" Executed="OpenCmdExecuted" CanExecute="OpenCmdCanExecute"/> <CommandBinding Command="ApplicationCommands.Save" Executed="SaveCmdExecuted" CanExecute="SaveCmdCanExecute"/> </Window.CommandBindings> < ' — Эта панель устанавливает содержимое окна —> <DockPanel> </DockPanel> </Window> Теперь щелкните правой кнопкой мыши на каждом из атрибутов Executed и CanExecute в редакторе XAML и выберите в контекстном меню пункт Navigate to Event Handler (Перейти к обработчику события). Как было описано в главе 27, это автоматически сгенерирует заготовку кода для обработчика события. Теперь в файле кода С# для окна имеются пустые обработчики событий. * Реализация обработчиков событий CanExecute должна сообщить окну, что можно инициировать соответствующие события Executed в любой момент, для чего нужно установить свойство CanExecute входящего объекта CanExecuteRoutedEventArgs:
1072 Часть VI. Построение настольных пользовательских приложений с помощью WPF private void OpenCmdCanExecute (object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void SaveCmdCanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } Соответствующие обработчики Executed выполняют действительную работу по отображению диалоговых окон открытия и сохранения файла; кроме того, они передают данные из TextBox в файл. Начните с импорта пространства имен System. 10 и Microsoft.Win32 в файл кода. ГЪтовый код прост и показан ниже: private void OpenCmdExecuted(object sender, ExecutedRoutedEventArgs e) { // Создать диалоговое окно открытия файла, отображающее только текстовые файлы. OpenFileDialog openDlg = new OpenFileDialog (); openDlg.Filter = "Text Files |*.txt"; // Был ли щелчок на кнопке OK? if (true == openDlg.ShowDialog ()) { // Загрузить содержимое выбранного файла. string dataFromFile = File.ReadAllText(openDlg.FileName); // Отобразить строку в TextBox. txtData.Text = dataFromFile; } } private void SaveCmdExecuted(object sender, ExecutedRoutedEventArgs e) { SaveFileDialog saveDlg = new SaveFileDialog(); saveDlg.Filter = "Text Files |*.txt"; // Был ли щелчок на кнопке OK? if (true == saveDlg.ShowDialog() ) { // Сохранить данные из TextBox в указанном файле. File.WriteAllText(saveDlg.FileName, txtData.Text); } } На этом пример и начальное знакомство с работой с элементами управления WPF завершены. Вы узнали, как работать с системой меню, строкой состояния, панелью инструментов, вложенными панелями и несколькими базовыми элементами пользовательского интерфейса, такими как TextBox и Expander. В следующем примере будут задействованы более экзотические элементы управления, и параллельно вы ознакомитесь с несколькими важными службами WPF. Кроме того, будет показано, как строить пользовательские интерфейсы с помощью Expression Blend. Исходный код. Проект MyWordPad доступен в подкаталоге Chapter 28. Построение пользовательского интерфейса WPF с помощью Expression Blend Среда Visual Studio 2010 предлагает развитые средства редактирования WPF, и при желании эту IDE-среду можно использовать для решения всех задач по редактированию XAML. Однако для сложных приложений объем работ можно значительно сократить,
Глава 28. Программирование с использованием элементов управления WPF 1073 если применять Expression Blend для генерации разметки, а затем открывать тот же проект в Visual Studio, чтобы при необходимости подкорректировать полученную разметку и написать код. Кстати говоря, Expression Blend 3.0 поставляется с простым редактором кода; хотя он все равно не дотягивает до мощи Visual Studio, этот встроенный редактор С# можно использовать для быстрого добавления кода обработчиков событий пользовательского интерфейса во время разработки. Ключевые аспекты IDE-среды Expression Blend Чтобы приступить к знакомству с Blend, загрузите продукт и щелкните на кнопке New Project (Создать проект) в диалоговом окне приветствия (если оно не отображается, выберите пункт меню File^New project (Файл"^Создать проект)). В диалоговом окне New Project (Новый проект) создайте новое приложение WPF по имени WpfControlsAndAPIs, как показано на рис. 28.17. Открыв проект, перейдите на вкладку Project (Проект), находящуюся в верхней левой части IDE-среды (если вы не видите ее, выберите пункт меню Window^ Projects (Окна1^Проект)). Как видно на рис. 28.18, приложение WPF, сгенерированное Blend, идентично проекту WPF, сгенерированному Visual Studio. Теперь найдите вкладку Properties (Свойства) в правой части IDE-среды Blend. Подобно Visual Studio 2010, здесь можно присваивать значения свойствам элемента, выбранного в визуальном конструкторе. Однако есть некоторые отличия от окна Properties в Visual Studio 2010. Например, обратите внимание, что подобные свойства сгруппированы вместе в сворачиваемых панелях (рис. 28.19). Возможно, наиболее очевидной частью IDE-среды Blend является визуальный конструктор окон, который позволяет выполнить большую часть работы. В верхней правой области визуального конструктора окон видны три маленькие кнопки; они позволяют открыть сам визуальный конструктор, лежащую в его основе XAML-разметку или разделить оба представления. На рис. 28.20 показан визуальный конструктор с разделенным видом. Рис. 28.17. Создание нового приложения WPF Рис. 28.18. Проект WPF в в Expression Blend Expression Blend идентичен проекту WPF в Visual Studio
1074 Часть VI. Построение настольных пользовательских приложений с помощью WPF Рис. 28.19. Редактор свойств в Expression Blend 1 2 3 4 5 б 7 8 садштш^си ^^ <Window xmlns-"http://schemes.microsoft.com/winfx/2006/xaml/presentation" xmlns:х-"http://schemes.microsoft.com/winfx/2006/xaml" x:Class-"WpfControlsAndAPIs.MainWindowH x: Name"MWindowM Title-"MainWindow" Width-59" Height-88"> Рис. 28.20. Визуальный конструктор Blend позволяет просматривать лежащую в основе XAML-разметку
Глава 28. Программирование с использованием элементов управления WPF 1075 Редактор XAML довольно насыщен средствами. Например, когда начинает вводиться код, активизируется средство IntelliSense, обеспечивающее поддержку авто-завершения, кроме того, имеется удобная справочная система. В рассматриваемом примере не придется вводить много XAML-разметки, но на рис. 28.21 показан редактор в действии. 2 xeJns-"http://scheeas.«icrosoft.coe/winfx/2ee6/xa«l/presentBtion" 3, xjilns:x«"http://schemes.microsoft.ссж/winfх/2вв6/ха«1* 4 x:Cldr.s-"UpfControlsAndAPIs.MainWin<W S' x:NaJne-"Window" 6; TitU«"MeinWindow" 7; Width-59" Height-88" > i ^ AllowDrop 9! <6rid ч: Name-" LayoutRoot"/» iej </Window> : «f AHowTranspenency | {} Application {) AutomationProperties i 2§* Background <> Binding AHowOrop boo! Gets or sets a vaJue indicating whether this element can be used as the target of a drag-and-drop operation. Рис. 28.22. Дерево разметки XAML в окне Objects and Timeline Рис. 28.21. Редактор XAML в Expression Blend столь же многофункционален, как и аналогичный редактор в Visual Studio Еще одним ключевым аспектом Blend, о котором следует помнить, является область Objects and Timeline (Объекты и шкала времени), расположенная в нижней левой области окна Blend. В главе 30 вы узнаете, что временная шкала этого инструмента позволяет фиксировать последовательности анимации. А пока достаточно будет сказать, что этот аспект IDE- среды позволяет видеть логическое дерево разметки, подобно тому, как это делает Visual Studio 2010. В настоящее время окно Objects and Timeline должно выглядеть, как показано на рис. 28.22. Здесь видно, что узлом верхнего уровня является сам элемент Window, имеющий элемент LayoutRoot в качестве содержимого. Просмотрев XAML-разметку для начального окна, легко заметить, что это имя назначено диспетчеру компоновки <Grid> по умолчанию: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WpfControlsAndAPIs.MainWindow" x:Name="Window" Title="MainWindow" Width=,,559" Height=,,288" > <Grid x:Name="LayoutRoot"/> </Window> Если такой диспетчер компоновки по умолчанию не подходит, можно отредактировать XAML-разметку напрямую. Кроме того, можно щелкнуть правой кнопкой мыши на значке LayoutRoot в окне Objects and Timeline и выбрать в контекстном меню пункт Change Layout Type (Изменить тип компоновки). На заметку! Чтобы изменить внешний вид элемента пользовательского интерфейса в редакторе свойств, выберите этот элемент в дереве Objects and Timeline. Это автоматически приведет к выбору корректного элемента в визуальном конструкторе.
1076 Часть VI. Построение настольных пользовательских приложений с помощью WPF И заключительный аспект Blend, о котором следует знать на данный момент — это окно Tools (Инструменты), расположенное в левой части IDE-среды. В нем находятся элементы управления WPF для выбора и размещения в визуальном конструкторе. На рис. 28.23 видно, что некоторые из этих кнопок снабжены маленьким треугольником в нижней правой области. Щелкая и удерживая такую кнопку, можно выбирать связанные варианты. На заметку! Если окно Tools смонтировано в верхней части IDE-среды Blend, то элементы управления располагаются в горизонтальной, а не вертикальной ленте. Любое окно Blend можно стыковать простым перетаскиванием с помощью мыши. Выбор пункта меню Window^Reset Current Workplace (Окно^Сбросить текущую рабочую область) позволяет восстановить стандартное местоположение всех окон Blend. Рис. 29.23. Окно Tools позволяет выбирать элемент управления WPF для помещения в визуальный конструктор Также обратите внимание, что одна из кнопок в окне Tools помечена символом ». Щелчок ней приводит к открытию окна Assets Library (Библиотека активов), показанного на рис. 28.24. Здесь доступно множество дополнительных элементов управления WPF, которые по умолчанию в окне Tools не отображаются. Выбор элементов в Assets Library вызывает их перемещение в область Tools с целью дальнейшего использования. Рис. 28.24. Окно Assets Library предоставляет доступ к дополнительным элементам управления WPF
Глава 28. Программирование с использованием элементов управления WPF 1077 В * * MainVl Selection (V) | Рис. 28.25. Кнопка Selection позволяет изменять размер и расположение выбранного элемента в визуальном конструкторе Другая важная тема, связанная с окном Tools, часто вводит в заблуждение новичков, впервые имеющих дело с Blend; так, многие часто не понимают разницы между кнопками Selection (Выбор) и Direct Selection (Направить выбор). Когда в визуальном конструкторе необходимо выбрать элемент (вроде Button) для изменения его размеров или местоположения в контейнере, используется кнопка Selection, которая помечена черной стрелкой (рис. 28.25). Кнопка Direct Selection (со стрелкой белого цвета) служит для углубления в содержимое включающего элемента. Таким образом, выбрав элемент Button, можно углубиться в его содержимое, чтобы построить сложное визуальное представление (например, добавить StackPanel и графические элементы). Использование кнопки Direct Selection еще будет рассматриваться в главе 31, когда речь пойдет о построении специальных элементов управления. А пока при построении графических интерфейсов пользователя выбирайте кнопку Select вместо Direct Select. Использование элемента TabControl Проектируемое окно будет содержать элемент TabControl с четырьмя вкладками, каждая из которых отображает набор связанных элементов управления и/или API- интерфейсов WPF. Для начала удостоверьтесь, что в окне Objects and TimeLine выбран узел LayoutRoot. Затем щелкните на кнопке » (Assets Library) и найдите элемент управления TabControl. Выберите этот элемент управления в окне Tools и перетащите на поверхность визуального конструктора, чтобы он занял большую ее часть. В окне появится система вкладок с двумя вкладками по умолчанию. Выберите новый элемент TabControl в окне Objects and TimeLine и в окне Properties назначьте ему имя myTabSystem, используя поле редактирования Name (Имя) в верхней части редактора свойств. Щелкните правой кнопкой мыши на TabControl в окне Objects and TimeLine и выберите в контекстном меню пункт Add Tabltem (Добавить вкладку), как показано на рис. 28.26. Рис. 28.26. Визуальное добавление вкладок
1078 Часть VI. Построение настольных пользовательских приложений с помощью WPF Добавьте к элементу управления еще одну вкладку и найдите в окне Properties область Common Properties (Общие свойства). Измените свойство Header каждой вкладки, указав названия Ink API, Documents, Data Binding и DataGrid (рис. 28.27). Рис. 28.27. Визуальное редактирование элементов управления Tabltem Теперь поверхность визуального конструктора окна должна выглядеть, как показано на рис. 28.28. Последовательно щелкните на вкладках и в окне Properties назначьте каждой из них уникальное подходящее имя (вкладки также можно выбирать в окне Object and Timeline). Имейте в виду, что после выбора вкладки с помощью любого из этих двух подходов, она становится активной, и на нее можно перетаскивать элементы управления из области Tools. Перед тем, как заняться проектированием каждой вкладки, взгляните на XAML- разметку, сгенерированную IDE-средой Blend, щелкнув на кнопке XAML. Разметка будет выглядеть примерно так, как показано ниже: <Window xmlns="http://schemas.microsoft.com/winfx/2 0 0 6/xaml/presentation"
Глава 28. Программирование с использованием элементов управления WPF 1079 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x: Class="WpfControlsAndAPIs .MainWindow" x :Name="Window11 Title="MainWindow" Width=28" Height=,,383" > <Grid x:Name=llLayoutRoot"> <TabControl x:Name="myTabSystem11 Margin=ll8"> <TabItem x:Name="tabInk" Header="Ink API"> <Grid/> </TabItem> <TabItem x:Name="tabDocuments11 Header="Documentsll> <Grid/> </TabItem> <TabItem x:Name="tabDataBinding11 Header="Data Binding"> <Grid/> </TabItem> <TabItem x :Name="tabDataGrid11 Header=MDataGrid"> <Grid/> </TabItem> </TabControl> </Grid> </Window> Построение вкладки ink API На этой вкладке будет отображаться общее назначение интерфейса Ink API, который позволяет встраивать в программу функциональность рисования. Конечно, его применение не обязательно ограничивается приложениями для рисования; этот API можете использовать для решения широкого круга задач, включая фиксацию рукописного ввода посредством пера на Tablet PC. Начните с поиска в области Objects and Timeline узла, представляющего вкладку Ink API, и раскройте его. Диспетчером компоновки по умолчанию для этого элемента Tabltem является <Grid>. Щелкните правой кнопкой мыши на нем и замените диспетчер компоновки на StackPanel (рис. 28.29). Рис. 28.29. Замена диспетчера компоновки
1080 Часть VI. Построение настольных пользовательских приложений с помощью WPF Проектирование элемента ToolBar Удостоверьтесь, что узел StackPanel в данный момент выбран в окне Objects and Timeline, и воспользуйтесь Assets Library для вставки нового элемента управления ToolBar по имени inkToolbar. Выберите узел inkToolbar в Objects and Timeline, найдите раздел Layout (Компоновка) в окне Properties и установите свойство Height элемента Toolbar равным 60 (для свойства Width оставьте значение Auto). Отыщите область Common Properties в окне Properties и щелкните на кнопку с многоточием возле свойства Items (Collection), как показано на рис. 28.30. После щелчка по кнопке откроется диалоговое окно, которое позволяет выбрать элементы управления для добавления к ToolBar. Щелкните на раскрывающемся списке кнопки Add another item (Добавить другой элемент) и добавьте три элемента управления RadioButton (рис. 28.31). Воспользуйтесь встроенным редактором свойств в этом диалоговом окне, чтобы установить для каждого элемента RadioButton значение свойства Height в 50 и Width — в 100 (эти свойства находятся в разделе Layout). Также установите свойства Content (в разделе Common Properties) элементов RadioButton в Ink Mode!, Erase Mode! и Select Mode!, соответственно (рис. 28.32). После добавления трех элементов управления RadioButton добавьте элемент Separator, используя раскрывающийся список Add another item. Теперь остается добавить финальный элемент управления ComboBox (не ComboBoxItem); однако этот элемент в раскрывающемся списке Add another item отсутствует. Когда необходимо вставить нестандартные элементы управления в диалоговом окне Items (Collection) (Элементы (Коллекция)), просто щелкните на области Add another item, как если бы это была кнопка. В результате откроется редактор Select Object (Выберите объект), в котором можно ввести имя нужного элемента управления (рис. 28.33). Установите свойство Width элемента ComboBox в 100 и добавьте три объекта ComboBoxItem к ComboBox, используя свойство Items (Collection) в разделе Common Properties редактора свойств. Установите свойство Content каждого ComboBoxItem в строки Red, Green и Blue. Закройте редактор для возврата в визуальный конструктор окна. Последней задачей настоящего раздела будет использование свойства Name для назначения имен перемен- Рис. 28.30. Первый шаг в до- Рис. 28.31. Добавление трех элементов RadioButton к бавлении элементов управле- ToolBar ния к ToolBar с помощью окна Properties
Глава 28. Программирование с использованием элементов управления WPF 1081 ных для новых элементов. Назовите три элемента RadioButton следующим образом: inkRadio, selectRadio и eraseRadio. Элемент ComboBox назовите comboColors. Рис. 28.32. Конфигурирование элементов RadioButton Рис. 28.33. Использование редактора Select Object для добавления уникальных элементов к Toolbar
1082 Часть VI. Построение настольных пользовательских приложений с помощью WPF На заметку! При построении панели инструментов с помощью Blend вы поймете, насколько быстрее все можно было бы сделать, если иметь возможность просто вручную редактировать XAML. Это действительно так! Не забывайте, что инструмент Expression Blend предназначен для дизайнеров, которые могут и не уметь вводить код разметки вручную. Программисты С# всегда имеют возможность применить встроенный в Blends редактор XAML; тем не менее, полезно освоить работу внутри IDE-среды, особенно если впоследствии придется объяснять дизайнерам, как использовать этот инструмент. Элемент управления RadioButton В данном примере требуется, чтобы эти три элемента управления RadioButton были взаимно исключающими. В других платформах для построения графических интерфейсов пользователя такие элементы должны были бы помещаться в групповую рамку. В WPF этого делать не нужно. Взамен им просто назначается одинаковое групповое имя. Это полезно потому, что связанные элементы не обязательно должны быть физически объединены в общую область, а могут располагаться в любом месте окна. Сделайте это, выбрав каждый элемент RadioButton в визуальном конструкторе (чтобы выбрать все три элемента, щелкайте при нажатой клавише <Shift>) и затем установив для свойства GroupName (находящегося в разделе Common Properties окна Properties) значение InkMode. Когда элемент управления RadioButton не помещен внутрь родительского элемента-панели, он принимает вид, идентичный элементу управления Button. Тем не менее, в отличие от Button, класс RadioButton включает в себя свойство IsChecked, которое переключается между true и false, когда конечный пользователь щелкает на элементе. Более того, RadioButton поддерживает два события (Checked и Unchecked), которые можно использовать для перехвата изменения этого состояния. Для конфигурирования элементов управления RadioButton, чтобы они выглядели, как типичные переключатели, выберите их в визуальном конструкторе, последовательно щелкая при нажатой клавише <Shift>, а затем щелкните правой кнопкой мыши и выберите в контекстном меню пункт Group In to ^Border (Сгруппировать внутри^ Элемент Border), как показано на рис. 28.34. Рис. 28.34. Группирование элементов внутри элемента Border
Глава 28. Программирование с использованием элементов управления WPF 1083 Теперь все готово к тестированию программы, для чего нужно нажать клавишу <F5>. В окне отображаются три связанных переключателя и раскрывающийся список с тремя элементами (рис. 28.35). Рис. 28.35. Готовая система панели инструментов Для вкладки Ink API осталось обработать событие Click каждого элемента управления RadioButton. Подобно Visual Studio, окно Properties в Blend содержит кнопку с изображением молнии для ввода имен обработчиков событий (на рис. 28.36 она находится рядом со свойством Name). Привяжите событие Click каждой кнопки к одному и тому же обработчику по имени RadioButtonClicked (см. рис. 28.36). Рис. 28.36. Обработка события Click каждого элемента RadioButton Откроется редактор кода Blend. Обработав все три события Click, обработайте событие SelectionChanged элемента управления ComboBox посредством обработчика по имени ColorChanged. В результате должен получиться следующий код С#: public partial class MainWindow : Window { public MainWindow () { this.InitializeComponent () ; // Добавьте код, необходимый при создании объекта. } private void RadioButtonClicked(object sender, System.Windows.RoutedEventArgs e) { // TODO: Сюда добавьте реализацию обработчика событий. }
1084 Часть VI. Построение настольных пользовательских приложений с помощью WPF private void ColorChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { // TODO: Сюда добавьте реализацию обработчика событий. } } Эти обработчики будут реализованы позже, а пока оставьте их пустыми. Элемент управления inkCanvas Для завершения пользовательского интерфейса этой вкладки необходимо поместить элемент управления InkCanvas в StackPanel, чтобы он появился под только что созданным элементом Toolbar. Выберите StackPanel для объекта tablnk в окне Object and Timeline и затем воспользуйтесь Assets Library для добавления InkCanvas по имени mylnkCanvas. Затем щелкните на инструменте Selection в окне Tools (можно также нажать клавишу <V>) и растяните новый элемент InkCanvas, чтобы он занял большую часть области вкладки. С помощью редактора Brushes (Кисти) можно задать для InkCanvas уникальный цвет (более подробно редактор кистей рассматривается в следующей главе). Сделав все это, запустите программу, нажав <F5>. Вы увидите, что поверхность холста теперь позволяет рисовать графические данные по нажатию левой кнопки и перемещению мыши (рис. 28.37). Рис. 28.37. Элемент InkCanvas в действии Элемент InkCanvas позволяет делать нечто большее, чем просто рисование линий мышью (или пером); он также поддерживает множество уникальных режимов редактирования, управляемых свойством EditingMode. Этому свойству можно присвоить любое значение из связанного перечисления InkCanvasEditingMode. В данном примере интересует режим Ink, принятый по умолчанию, который только что использовался; режим Select, позволяющий пользователю выбирать с помощью мыши область для последующего перемещения или изменения размера; и режим EraseByStroke, который удаляет предыдущий след, нарисованный мышью. На заметку! Штрих (stroke) — это визуализация, которая происходит при одиночной операции нажатия и отпускания кнопки мыши. InkCanvas сохраняет все штрихи в объекте StrokeCollection, который доступен через свойство Strokes.
Глава 28. Программирование с использованием элементов управления WPF 1085 Обновите обработчик RadioButtonClickedO следующей логикой, которая переводит InkCanvas в правильный режим в зависимости от выбранного переключателя RadioButton: private void RadioButtonClicked(object sender, System.Windows.RoutedEventArgs e) { // В зависимости от того, какая кнопка отправила событие, //' переключить InkCanvas в нужный режим работы, switch((sender as RadioButton) .Content.ToString ()) { // Эти строки должны совпадать со значениями Content // каждого элемента RadioButton. case "Ink Mode1": this.mylnkCanvas.EditingMode = InkCanvasEditingMode.Ink; break; case "Erase Mode1": this.mylnkCanvas.EditingMode = InkCanvasEditingMode.EraseByStroke; break; case "Select Mode1": this.mylnkCanvas.EditingMode = InkCanvasEditingMode.Select; break; } } Также установите режим в Ink по умолчанию в конструкторе окна. Там же установите выбор по умолчанию для ComboBox (более подробно этот элемент рассматривается в следующем разделе): public MainWindowO { this.InitializeComponent(); // Установить по умолчанию режим Ink. this.mylnkCanvas.EditingMode = InkCanvasEditingMode.Ink; this.inkRadio.IsChecked = true; this.comboColors.SelectedIndex=0; } Теперь снова запустите программу нажатием <F5>. Выберите режим Ink и нарисуйте что-нибудь. Затем выберите режим Erase и сотрите ранее нарисованное (курсор мыши автоматически примет вид стирающей резинки). Наконец, переключитесь в режим Select, выберите несколько линий, используя мышь как "лассо". Охватив элемент, вы сможете перемещать его по поверхности холста и изменять его размеры. На рис. 28.38 показан результат работы в разных режимах. Рис. 28.38. Элемент InkCanvas в действии, с разными режимами редактирования.
1086 Часть VI. Построение настольных пользовательских приложений с помощью WPF Элемент управления ComboBox После заполнения элемента управления ComboBox (или ListBox) есть три способа определения выбранного в них элемента. Во-первых, если необходимо найти числовой индекс выбранного элемента, необходимо использовать свойство Selectedlndex (отсчет начинается с 0; значение -1 означает отсутствие выбора). Во-вторых, если требуется получить объект, выбранный внутри списка, подойдет свойство Selectedltem. В-третьих, SelectedValue позволяет получить значение выбранного объекта (обычно через вызов его метода ToStringO). И последний фрагмент кода, который понадобится добавить для данной вкладки, отвечает за изменение цвета линий, нарисованных в InkCanvas. Свойство DefaultDrawingAttributes элемента InkCanvas возвращает объект DrawingAttributes, который позволяет конфигурировать различные аспекты пера, включая его размер и цвет (помимо прочего). Обновите код С# следующей реализацией метода ColorChanged(): private void ColorChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { // Получить выбранный элемент в раскрывающемся списке. string colorToUse = (this.comboColors.Selectedltem as ComboBoxItem) .Content.ToString() ; // Изменить цвет, используемый для визуализации линий. this.mylnkCanvas.DefaultDrawingAttributes.Color = (Color)ColorConverter.ConvertFromString(colorToUse); } Вспомните, что ComboBox имеет коллекцию ComboBoxItems. В сгенерированной XAML-разметке имеется следующее определение: <ComboBox x:Name="comboColors11 Width=0011 SelectionChanged="ColorChangedll> <ComboBoxItem Content=llRed"/> <ComboBoxItem Content=llGreen"/> <ComboBoxItem Content="Bluell/> </ComboBox> В результате вызова Selectedltem получается выбранный ComboBixItem, который хранится как элемент общего типа Object. После приведения Object к ComboBixItem получится значение Content, представленное строкой Red, Green или Blue. Эта строка затем преобразуется в объект Color с помощью удобного служебного класса ColorConverter. Снова запустите программу. Теперь появилась возможность переключать цвета при визуализации изображения. Обратите внимание, что элементы управления ComboBox и ListBox могут также включать сложное содержимое, а не только списки текстовых данных. Чтобы получить пред- ' ставление о некоторых возможностях, откройте редактор XAML для окна и измените определение ComboBox, чтобы он имел набор элементов <StackPanel>, каждый из которых содержит <Ellipse> и <Label> (значение свойства Width элемента ComboBox равно 200): <ComboBox x:Name="comboColors11 Width=0011 SelectionChanged="ColorChangedll> <StackPanel Orientation ^'Horizontal" Tag=,,Red"> <Ellipse Fill =,,Red" Height = 0" Width =,,50,7> <Label FontSize =,,20" HorizontalAlignment="Center" VerticalAlignment="Center" Content=,,Red,,/> </StackPanel> <StackPanel Orientation ^'Horizontal11 Tag=llGreen"> <Ellipse Fill ="Green" Height = 0" Width =0'7> <Label FontSize =ll20" HorizontalAlignment="Center11 VerticalAlignment=,,Center" Content="Green,7> </StackPanel>
Глава 28. Программирование с использованием элементов управления WPF 1087 <StackPanel Orientation ^'Horizontal11 Tag=llBlue"> <Ellipse Fill =,,BlueM Height = 0" Width =,,50,,/> <Label FontSize =0" HorizontalAlignment="Center" VerticalAlignment="Center" Content="Blue"/> </StackPanel> </ComboBox>" Теперь свойству Tag каждого элемента StackPanel присвоено какое-то значение; это представляет собой быстрый и удобный способ определения стека элементов, выбранных пользователем (существуют и лучшие способы делать это, но пока такого достаточно). С этой поправкой потребуется изменить реализацию метода ColorChangedO, как показано ниже: private void ColorChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { // Получить свойство Tag выбранного элемента StackPanel. string colorToUse = (this.comboColors.Selectedltem as StackPanel).Tag.ToString(); } Запустите программу и обратите внимание на уникальный ComboBox (рис. 28.39). шШШШШШвШШШШШШШШШШШщ | п L??comem*! ^ta Si™in9_L??*!r_nd.._; Рис. 28.39. Специальный элемент ComboBox, реализованный благодаря модели содержимого WPF Сохранение, загрузка и очистка данных inkCanvas Последняя часть этой вкладки позволит сохранять и загружать данные полотна, а также очищать его содержимое. Вы уже должны довольно уверенно чувствовать себя в проектировании пользовательских интерфейсов с помощью Blend, так что инструкции будут краткими и по существу. Начните с импорта пространств имен System. 10 и System.Windows.Ink в файл кода. Затем добавьте в Tool Bar три новых элемента Button с именами btnSave, btnLoad и btnClear. После этого обработайте событие Click для каждого элемента управления и реализуйте обработчики следующим образом: private void SaveData(object sender, System.Windows.RoutedEventArgs e) { // Сохранить все данные InkCanvas в локальном файле. using (FileStream fs = new FileStream("StrokeData.bin", FileMode.Create)) { this.mylnkCanvas.Strokes.Save(fs);
1088 Часть VI. Построение настольных пользовательских приложений с помощью WPF fs.Close (); } } private void LoadData(object sender, System.Windows.RoutedEventArgs e) { // Наполнить StrokeCollection из файла. using(FileStream fs = new FileStream("StrokeData.bin", FileMode.Open, FileAccess.Read)) { StrokeCollection strokes = new StrokeCollection(fs); this.mylnkCanvas.Strokes = strokes; } } private void Clear (object sender, System.Windows.RoutedEventArgs e) { // Удалить все штрихи. this.mylnkCanvas.Strokes.Clear(); I После этого появляется возможность сохранять данные в файле, загружать из файла и очищать InkCanvas от всех данных. На этом работа с первой вкладкой TabControl завершена, равно как и рассмотрение интерфейса Ink API. По правде говоря, еще много чего можно рассказать об этой технологии, однако теперь вы должны обладать достаточными знаниями, чтобы продолжить исследование этой темы самостоятельно. Далее мы приступаем к рассмотрению интерфейса Documents API в WPF. Введение в интерфейс Documents API WPF поставляется с множеством элементов управления, которые позволяют вводить или отображать тщательно сформатированные текстовые данные, подобные тем, что можно найти в файлах Adobe PDF Интерфейс Documents API из WPF поддерживает такую функциональность, однако в нем используется формат XML Paper Specification (XPS) вместо PDF Documents API позволяет конструировать готовые для печати документы, используя несколько классов из пространства имен System.Windows.Documents. В этом пространстве имен доступно множество типов, представляющих части документа XPS: List, Paragraph, Section, Table, LineBreak, Figure, Floater и Span. Блочные элементы и встроенные элементы Формально элементы, которые добавляются к документу XPS, относятся к одной из двух категорий: блочные элементы (block elements) и встроенные элементы (inline elements). Первая категория, блочные элементы, состоит из классов, которые расширяют базовый класс System.Windows.Documents.Block. Примерами блочных элементов могут служить List, Paragraph, BlockUIContainer, Section и Table. Классы из этой категории применяются для группирования вместе другого содержимого (например, список, содержащий данные абзаца, и абзац, содержащий подабзацы для разного форматирования текста). Вторая категория, встроенные элементы, состоит из классов, расширяющих базовый класс System.Windows.Documents.Inline. Встроенные элементы вкладываются внутрь блочного элемента (или, возможно, внутрь другого встроенного элемента внутри блочного элемента). К общим встроенным элементам относятся Run, Span, LineBreak, Figure и Floater.
Глава 28. Программирование с использованием элементов управления WPF 1089 Эти классы имеют имена, которые можно встретить при построении форматированного документа в профессиональном редакторе. Как и любой другой элемент управления WPF, эти классы можно конфигурировать в XAML-разметке или в коде. Таким образом, можно объявить пустой элемент <Paragraphs который наполняется во время выполнения (в следующем примере будет показано, как это делается). Диспетчеры компоновки документа Может показаться, что встроенные и блочные элементы следует размещать непосредственно в панели-контейнере, такой как Grid, но на самом деле они должны быть упакованы в элементы <FlowDocument> или <FixedDocument>. Помещать элементы в FlowDocument идеально, когда нужно позволить конечному пользователю изменять способ представления данных. Пользователь может изменять масштаб текста или способ представления данных (например, в виде одной длинной страницы или в виде пары столбцов). FixedDocument лучше применять для представления готовых к печати (WYSIWYG), неизменяемых документов. В рассматриваемом примере мы будем иметь дело только с контейнером FlowDocument. После вставки встроенных и блочных элементов в FlowDocument этот объект будет помещен в один из поддерживающих XPS диспетчеров компоновки, которые перечислены в табл. 28.4. Таблица 28.4. Диспетчеры компоновки XPS Элемент управления типа панели Назначение FlowDocumentReader Отображает данные в FlowDocument и добавляет поддержку масштабирования, поиска и компоновки содержимого в разнообразных формах FlowDocumentScrollViewer Отображает данные в FlowDocument; однако данные представлены как единый документ, просматриваемый с использованием линеек прокрутки. Этот контейнер не поддерживает масштабирование, поиск или альтернативные режимов компоновки RichTextBox Отображает данные в FlowDocument и добавляет поддержку редактирования со стороны пользователей FlowDocumentPageViewer Отображает документ постранично, т.е. одну страницу за раз. Данные можно масштабировать, но поиск не предусмотрен Наиболее богатый средствами способ отображения FlowDocument состоит в том, чтобы поместить его в диспетчер FlowDocumentReader. В результате пользователь получает возможность изменять компоновку, искать слова в документе и масштабировать отображение данных. Одно из ограничений этого контейнера (а также FlowDocumentScrollViewer и FlowDocumentPageViewer) связано с тем, что отображаемое содержимое доступно только для чтения. Если же необходимо позволить конечному пользователю вводить новую информацию в FlowDocument, его можно упаковать в элемент управления RichTextBox. Построение вкладки Documents Щелкните на вкладке Documents элемента Tabltem и откройте ее в редакторе Blend. Здесь уже имеется элемент управления по умолчанию <Grid>, который является прямым дочерним элементом Tabltem; с помощью окна Objects and Timeline замените его StackPanel. На вкладке Documents будет отображаться элемент FlowDocument, кото-
1090 Часть VI. Построение настольных пользовательских приложений с помощью WPF рый позволит выделять текст и добавлять аннотации, используя API-интерфейс Sticky Notes ("клейкие" заметки). Начните с определения следующего элемента управления ToolBar, который включает в себя три простых (неименованных) элемента управления Button. Позднее к этим элементам управления будут привязаны команды, поэтому ссылаться на них в коде не понадобится. <TabItem x:Name="tabDocuments" Header="Documents" VerticalAlignment="Bottom" Height=0"> <StackPanel> <ToolBar> <Button BorderBrush="Green" Content="Add Sticky Note"/> <Button BorderBrush="Green" Content="Delete Sticky Notes"/> <Button BorderBrush="Green" Content="Highlight Text"/> </ToolBar> </StackPanel> </TabItem> Откройте Assets Library и найдите элемент управления FlowDocumentReader в категории All (Все) узла Conrtols (Элементы управления). Поместите этот элемент управления в StackPanel, переименуйте в myDocumentReader и растяните на всю поверхность StackPanel (удостоверившись, что выбран инструмент Selection). Компоновка должна выглядеть подобно показанной на рис. 28.40. Теперь выберите элемент управления FlowDocumentReader в окне Objects and Timeline и найдите категорию Miscellaneous (Прочие) в окне Properties. Щелкните на кнопке New (Создать) рядом со свойством Document. В XAML-разметку добавляется пустой элемент <FlowDocument>: <FlowDocumentReader x:Name="myDocumentReader" Height=69.4"> <FlowDocument/> </FlowDocumentReader> Теперь можно добавлять к элементу классы документов (например, List, Paragraph, Section, Table, LineBreak, Figure, Floater и Span).
Глава 28. Программирование с использованием элементов управления WPF 1091 Resources Data Properties > Name < No Name > < / Type FtowDocumentReadef * Document (FtowDocu AllowDrop Background BindmgGroup Blocks (Collection] CotumnGap Auto CoiumnRuleBrush CokimnRuJeWidtn 0 ColumnWidth Auto ment) No Brush I I No Brush П Ш* New . New ^ Рис. 28.41. Элемент FlowDocument наполняется с использованием свойства Blocks (Collection) Наполнение FlowDocument с использованием Blend Сразу после добавления нового документа к контейнеру документов свойство Document в окне Properties станет раскрываемым, отображая массу новых свойств, которые позволяют формировать дизайн документа. В рассматриваемом примере единственным свойством, о котором следует позаботиться, будет Blocks (Collection), как показано на рис. 28.41. Щелкните на кнопке с многоточием справа от Blocks (Collection) и в открывшемся диалоговом окне с помощью кнопки Add another Item (Добавить новый элемент) вставьте в документ элементы Section, List и Paragraph. Каждый из этих элементов можно редактировать дальше, используя для этого редактор Blocks (Блоки), причем каждый блок может содержать в себе подблоки. Например, выбрав элемент Section, можно добавить в него подблок Paragraph. Для примера сконфигурируйте элемент Section с определенным цветом фона, переднего плана и размером шрифта. Кроме того, вставьте подблок Paragraph. Элемент Section можно конфигурировать как угодно, однако оставьте элементы List и исходный Paragraph пустыми, поскольку они будут заполняться в коде. Ниже показан один из возможных вариантов конфигурации FlowDocument: <FlowDocumentReader x:Name="myDocumentReader" Height=69.4"> <FlowDocument> <Section Foreground = "Yellow" Background = "Black"> <Paragraph FontSize = 0"> Here are some fun facts about the WPF Document API! </Paragraph> </Section> <List/> <Paragraph/> </FlowDocument> </FlowDocumentReader> Если теперь запустить программу (нажав <F5>), можно масштабировать отображение документа (используя ползунок в нижнем правом углу), искать ключевые слова (с помощью редактора поиска, расположенного внизу слева) и отображать данные в одном из трех режимов (используя кнопки компоновки). На рис. 28.42 показан поиск слова "WPF"; обратите внимание, что содержимое отображается в увеличенном масштабе. Прежде чем приступить к следующему шагу, отредактируйте XAML-разметку, чтобы вместо FlowDocumentReader использовать другой контейнер FlowDocument, например, такой как FlowDocumentScrollViewer или RichTextBox. После этого снова запустите приложение и обратите внимание на отличия в обработке данных документа. По завершении этого эксперимента верните тип FlowDocumentReader. Наполнение FlowDocument с помощью кода Теперь давайте построим блок List и оставшийся блок Paragraph в коде. В этом важно разобраться, поскольку часто требуется наполнять документ FlowDocument на основе пользовательского ввода, внешних файлов, информации из базы данных или любого другого источника.
1092 Часть VI. Построение настольных пользовательских приложений с помощью WPF 1 !^|Р] Documents Datafcndmg j Ca'.aG-ю i |Add Sticky Note| Delete Sticky Notes|Highlight Tertj Here are some fun facts about the WPF Document API! ' ItaiwPF «>-] Б 1 L , , 1 Hi - *. 4. l l Рис. 28.42. Манипулирование FlowDocument с помощью FlowDocumentReader Воспользуйтесь редактором XAML-разметки в Blend, чтобы назначить элементам List и Paragraph подходящие имена и тем самым получить возможность обращаться к ним из кода: <List x:Name="listOfFunFacts"/> <Paragraph x:Name="paraBodyText"/> В файле кода определите новый приватный метод по имени PopulateDocument(). Этот метод сначала добавит набор элементов Listltem к List, каждый из которых имеет элемент Paragraph с единственным элементом Run. Кроме того, вспомогательный метод динамически построит сформатированный абзац, используя три отдельных объекта Run, как показано ниже: private void PopulateDocument () { // Добавить некоторые данные в элемент List. this.listOfFunFacts.FontSize = 14; this.listOfFunFacts.MarkerStyle = TextMarkerStyle.Circle; this.listOfFunFacts.Listltems.Add(new Listltem( new Paragraph(new Run("Fixed documents are for WYSIWYG print ready docs1")))); this.listofFunFacts.Listltems.Add(new Listltem( new Paragraph(new Run("The API supports tables and embedded figures!")))); this.listOfFunFacts.Listltems.Add(new Listltem( new Paragraph(new Run("Flow documents are read only1")))); this.listOfFunFacts.Listltems.Add(new Listltem(new Paragraph(new Run ("BlockUIContainer allows you to embed WPF controls in the document1") ))); // Добавить некоторые данные в элемент Paragraph. Первая часть абзаца. Run prefix = new Run("This paragraph was generated ") ; // Середина абзаца. Bold b = new Bold () ; Run infix = new Run("dynamically") ; infix.Foreground = Brushes.Red; infix.FontSize = 30; b. Inlmes .Add (infix) ; // Последняя часть абзаца. Run suffix = new Run(" at runtime1"); // Добавить все части в коллекцию встроенных элементов Paragraph. this.paraBodyText.Inlines.Add(prefix); this.paraBodyText.Inlines.Add(infix); this.paraBodyText.Inlines.Add(suffix) ;
Глава 28. Программирование с использованием элементов управления WPF 1093 Вызовите этот метод в конструкторе окна. После этого запустите приложение и обратите внимание на новое, динамически сгенерированное содержимое документа. Включение аннотаций и "клейких" заметок Теперь можно строить документ с интересными данными, используя XAML-разметку и код С#, однако нужно еще что-то сделать с тремя кнопками в панели инструментов на вкладке Documents. В составе WPF доступен набор команд, используемых специально с интерфейсом Documents API. Эти команды позволяют пользователю выделять часть документа, а также добавлять "клейкие" заметки — аннотации. Все это делается с помощью всего нескольких строк кода (и небольшого объема разметки). Командные объекты для Documents API находятся в пространстве имен System.Windows. Annotations сборки PresentationFramework.dll. Таким образом, понадобится определить специальное пространство имен XML в открывающем элементе <Window> для использования таких объектов в XAML (обратите внимание, что префиксом дескриптора является а): <Window xmlns:а= "clr-namespace:System.Windows.Annotations;assembly=PresentationFramework" x:Class="WpfControlsAndAPIs.MainWindow" x:Name="Window" Title="MainWindow" Width="856" Height=83" mc:Ignorable="d" WindowStartupLocation="CenterScreen" > </Window> Обновите три определения <Button>, установив свойства Command в соответствующие команды аннотаций: <Тоо1Ваг> <Button BorderBrush="Green" Content="Add Sticky Note" Command="a:AnnotationService.CreateTextStickyNoteCommand"/> <Button BorderBrush="Green" Content="Delete Sticky Notes" Command="a:AnnotationService.DeletestickyNotesCommand"/> <Button BorderBrush="Green" Content="Highlight Text" Command="a:AnnotationService.CreateHighlightCommand"/> </ToolBar> Последнее, что потребуется делать — это включить службы аннотации для объекта FlowDocumentReader, который был назван myDocumentReader. Добавьте в класс еще один приватный метод по имени EnableAnnotationsO, который будет вызываться в конструкторе окна. Затем импортируйте-следующие пространства имен: using System.Windows.Annotations; using System.Windows.Annotations.Storage; Теперь реализуйте этот метод: private void EnableAnnotationsO { // Создать объект AnnotationService, работающий с FlowDocumentReader. AnnotationService anoService = new AnnotationService(myDocumentReader); // Создать MemoryStream, который будет содержать аннотации. MemoryStream anoStream = new MemoryStream(); // Создать основанное на XML хранилище на базе MemoryStream. Этот объект можно // использовать для программного добавления, удаления или поиска аннотаций. AnnotationStore store = new XmlStreamStore(anoStream); // Включить службы аннотаций. anoService.Enable(store); }
1094 Часть VI. Построение настольных пользовательских приложений с помощью WPF Класс AnnotationService позволяет заданному диспетчеру компоновки документа получить поддержку аннотаций. Прежде чем вызвать метод Enable () этого объекта, необходимо предоставить местоположение объекта для хранения аннотированных данных, которые в данном примере являются областью памяти, представленной объектом MemoryStream. Обратите внимание, что объект AnnotationService соединяется с объектом Stream, используя AnnotationStore. Запустите приложение. Выделите некоторый текст, щелкните на кнопке Add Sticky Note (Добавить "клейкую" заметку) и введите некоторую информацию. Выделенный текст также можно подсвечивать (по умолчанию желтым цветом). Наконец, можно удалять заметки, выбирая их и щелкая на кнопке Delete Stricky Note (Удалить "клейкую" заметку). На рис. 28.43 показан результат тестового запуска. r^t^il"Documents i Deto Binding j DatoGnd | {Add Sticky NoteJDetete Sticky Notes] Highfcght Tcxt| Here are some fan tacts about the WPF Document .API! О Fixed documents are for WYSIWYG print ready docs! О The API supports tables and embedded figures! О Flow documents are read only! О BlockUIContainer allows you to embed WPF control in the document! This paragraph was generated dynamically )at runtime! Рис. 28.43. Использование "клейких" заметок Сохранение и загрузка потокового документа Экскурс в интерфейс Documents API завершается рассмотрением простого процесса сохранения документа в файл, а также чтения документа из файла. Вспомните, что если объект FlowDocument не упакован в RichTextBox, конечный пользователь не сможет редактировать документ; кроме того, часть документа создавалась динамически во время выполнения, так что может понадобиться сохранить его для последующего использования. Возможность сохранения документа в стиле XPS также может быть полезна во многих приложениях WPF, так как она позволяет определить пустой документ и загружать его на лету. В следующем фрагменте разметки предполагается, что к элементу Toolbar вкладки Documents были добавлены два новых элемента Button, объявленные следующим образом (обратите внимание, что в разметке никакие события не обрабатываются): <Button х:Name="btnSaveDoc" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width=5" Content="Save Doc"/> <Button x:Name="btnLoadDoc" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width=5" Content="Load Doc"/> В конструкторе окна напишите следующие лямбда-выражения для сохранения и загрузки данных FlowDocument (для получения доступа к классам XamlReader и XamlWriter понадобится импортировать пространство имен System.Windows.Markup): public MainWindow() {
Глава 28. Программирование с использованием элементов управления WPF 1095 // Построить обработчики событий Click для сохранения и загрузки // документа нефиксированного формата. btnSaveDoc.Click += (о, s) => { using(FileStream fStream = File.Open( "documentData.xaml", FileMode.Create)) { XamlWriter.Save(this.myDocumentReader.Document, fStream); } }; btnLoadDoc.Click += (o, s) => { using(FileStream fStream = File.Open("documentData.xaml", FileMode.Open)) { try { FlowDocument doc = XamlReader.Load(fStream) as FlowDocument; this.myDocumentReader.Document = doc; } catch(Exception ex) {MessageBox.Show(ex.Message, "Error Loading Doc1");} } }; } Это все, что потребуется сделать для сохранения документа (следует отметить, что при этом не сохраняются аннотации; тем не менее, это можно сделать с помощью служб аннотаций). После щелчка на кнопке Save Doc (Сохранить документ) в папке \bin\ Debug появится новый файл *.xaml, который содержит данные текущего документа. На этом рассмотрение интерфейса Document API завершено. В этом API-интерфейсе есть еще много такого, что здесь не было показано, но вы получили достаточное представление об основах. В завершение главы мы коснемся нескольких тем, связанных с привязкой данных, и завершим текущее приложение. Введение в модель привязки данных WPF Элементы управления часто служат целью для различных операций привязки данных. Просто говоря, привязка данных (data binding) — это акт подключения свойств элемента управления к значениям данных, которые могут изменяться на протяжении жизненного цикла приложения. Это позволяет элементу пользовательского интерфейса отображать состояние переменной в коде. Например, привязку данных можно использовать для выполнения следующих действий: • отмечать флажок элемента управления Checkbox на основе булевского свойства заданного объекта; • отображать в элементах Text Box информацию, извлеченную из реляционной базы данных; • подключать элемент Label к целому числу, представляющему количество файлов в папке. При использовании встроенного в WPF механизма привязки данных следует помнить о разнице между источником и местом назначения операции привязки. Как и можно было ожидать, источником операции привязки данных являются сами данные (булевское свойство, реляционные данные и т.п.), в то время как местом назначения (или целью) является свойство элемента управления пользовательского интерфейса, который использует содержимое данных (Checkbox, TextBox и т.д.).
1096 Часть VI. Построение настольных пользовательских приложений с помощью WPF По правде говоря, использование инфраструктуры привязки данных WPF никогда не является обязательным. Если разработчик пожелает самостоятельно реализовать собственную логику привязки данных, то подключение между источником и местом назначения обычно потребует обработки различных событий и написания процедурного кода для соединения с источником и целью. Например, если в окне имеется элемент ScrollBar, который должен отобразить свое значение в Label, можно обработать событие ValueChanged элемента ScrollBar и соответствующим образом обновить содержимое Label. Однако привязку данных WPF можно применять для соединения источника и цели непосредственно в XAML-разметке (или в файле кода С#) без необходимости в обработке различных событий или жесткого кодирования соединений между источником и целью. К тому же, на основе построения логики привязки данных можно обеспечить постоянную синхронизацию целевого объекта с изменяющимися значениями данных. Построение вкладки Data Binding С помощью окна Objects and Timeline замените Grid в третьей вкладке на StackPanel. Затем воспользуйтесь Assets Library и редактором Properties среды Blend для построения следующей начальной компоновки: <TabItem x:Name="tabDataBinding" Header="Data Binding"> <StackPanel Width=50"> <Label Content="Move the scroll bar to see the current value"/> • <!-- Значение линейки прокрутки является источником привязки данных —> ^ScrollBar x:Name="mySB" Orientation="Horizontal" Height=0" Minimum = " Maximum = 00" LargeChange="l" SmallChange="l"/> <■-- Содержимое Label будет привязано к линейке прокрутки —> <Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness=" Content = "/> </StackPanel> </TabItem> Обратите внимание, что объект <ScrollBar> (названный здесь mySB) сконфигурирован с диапазоном от 1 до 100. Цель заключается в том, чтобы при изменении положения ползунка линейки прокрутки (или по щелчку на стрелке влево или вправо) элемент Label автоматически обновлялся текущим значением. Установка привязки данных с использованием Blend Механизм, обеспечивающий определение привязки в XAML — это расширение разметки {Binding}. Установка привязки между элементами управления в Blend осуществляется легко. В рассматриваемом примере найдите свойство Content объекта Label (в области Common Properties окна Properties) и щелкните на очень маленьком белом квадрате рядом со свойством для открытия окна расширенных свойств (Advanced Properties). Выберите элемент Data Binding (Привязка данных), как показано на рис. 28.44. Здесь нас интересует вкладка Element Property (Свойство элемента), потому что на ней представлен список всех элементов в файле XAML, которые можно выбирать в качестве источника операции привязки данных. Перейдите на эту вкладку и в окне списка Scene elements (Элементы сцены) найдите объект ScrollBar (по имени mySB). В окне списка Properties (Свойства) выберите значение Value (рис. 28.45). Щелкните на кнопке ОК.
Глава 28. Программирование с использованием элементов управления WPF 1097 Рис. 28.44. Конфигурирование Рис. 28.45. Выбор объекта-источника и его свойства операции привязки данных в Blend Запустив программу, вы обнаружите, что содержимое метки обновляется при перемещении ползунка. Теперь взглянем на XAML-разметку, сгенерированную инструментом привязки: <Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness=" Content = "(Binding Value, ElementName=mySB, Mode=Default}"/> Обратите внимание на значение, присвоенное свойству Content элемента Label. Значение ElementName представляет источник операции привязки данных (объект ScrollBar), а первый элемент после ключевого слова Binding (Value) представляет (в данном случае) свойство элемента, который нужно получить. Если вам приходилось ранее работать с привязкой данных WPF, то вы можете ожидать увидеть использование лексемы Path для установки наблюдаемого свойства объекта. Например, следующая разметка будет также корректно обновлять Label: <Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness=" Content = "{Binding Path=Value, ElementName=mySB, Mode=Default} "/> По умолчанию Blend пропускает аспект Path= операции привязки данных, если только свойство не является подсвойством другого объекта (например, myObject. MyProper ty. Object 2. Proper ty2). Свойство DataContext Для определения операции привязки данных в XAML может применяться альтернативный формат, при котором допускается разбивать значения, заданные расширением разметки {Binding}, за счет явной установки свойства DataContext в источник операции привязки:
1098 Часть VI. Построение настольных пользовательских приложений с помощью WPF <!-- Разбиение пары "объект/значение" через DataContext —> <Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness=" DataContext = "{Binding ElementName=mySB}" Content = "{Binding Path=Value}" /> В этом примере вывод будет идентичным. С учетом этого может возникнуть вопрос: когда необходимо устанавливать свойство DataContext явно? Это полезно в ситуации, когда подэлементы наследуют свое значение в дереве разметки. Подобным образом можно легко устанавливать один и тот же источник данных для семейства элементов управления, вместо того, чтобы повторять избыточные фрагменты XAML "{Binding ElementName=X, Path=Y}" во множестве элементов управления. Например, предположим, что в контейнер <S tack Panel > этой вкладки добавлен новый элемент Button: <Button Content="Click" Height=40"/> Для генерации привязок данных к множеству элементов управления можно было бы использовать Blend, но вместо этого давайте попробуем ввести модифицированную разметку в редакторе XAML: <!-- В StackPanel устанавливается свойство DataContext —> <StackPanel Width=50" DataContext = "{Binding ElementName=mySB}"> <Label Content="Move the scroll bar to see the current value"/> <ScrollBar Orientation="Horizontal" Height=0" Name="mySB" Maximum = 00" LargeChange=" SmallChange="l"/> <!-- Теперь оба элемента пользовательского интерфейса работают со значением линейки прокрутки уникальным образом --> <Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness=" Content = "{Binding Path=Value}"/> <Button Content="Click" Height=00" FontSize = "{Binding Path=Value}"/> </StackPanel> Здесь свойство DataContext в <StackPanel> устанавливается напрямую. В результате при перемещении ползунка не только отображается текущее значение в элементе Label, но также увеличивается размер шрифта элемента Button в соответствие с тем же значением. На рис. 28.46 показан возможный вывод. Рис. 28.46. Привязка значения ScrollBar к Label и Button
Глава 28. Программирование с использованием элементов управления WPF 1099 Преобразование данных с использованием iValueConverter Вместо ожидаемого целого числа для представления положения ползунка тип ScrollBar использует значение типа double. Поэтому при перемещении ползунка в элементе Label будут отображаться различные значения с плавающей точкой (вроде 61.0576923076923), которые выглядят не слишком интуитивно понятными для конечного пользователя, который ожидает целые числа (такие как 61, 62, 63 и т.д.). Одним из возможных способов преобразования значения из операции привязки данных в альтернативный формат является создание специального класса, реализующего интерфейс IValueCVonverter, доступный в пространстве имен System.Windows.Data. В этом интерфейсе определены два члена, которые позволяют осуществлять преобразование между источником и целью (в случае двунаправленной привязки). После определения такой класс можно использовать для последующего уточнения процесса привязки данных. Исходя из того, что в элементе управления Label необходимо отображать целые числа, показанный ниже класс можно построить с помощью Expression Blend. Выберите пункт меню Project^Add New Item (ПроектеДобавить новый элемент) и добавьте класс по имени MyDoubleConverter. Затем поместите в него следующий код: class MyDoubleConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { // Преобразовать double в int. double v = (double)value; return (int)v; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { // Поскольку заботиться о "двунаправленной" привязке / / не нужно, просто вернуть значение. return value; } } Метод Convert() вызывается при передаче значения от источника (ScrollBar) к цели (свойство Content элемента Label). Хотя получается множество входных аргументов, для этого преобразования понадобится манипулировать только входным object, который представляет текущее значение double. Этот тип можно использовать для приведения к целому и возврата нового числа. Метод ConvertBack() будет вызван, когда значение передается от цели к источнику (если включен двунаправленный режим). Здесь мы просто возвращаем значение. Это, например, позволяет вводить в TextBox значение с плавающей точкой (например, 99.9) и автоматически преобразовать его в целочисленное значение (99), когда производится выход из элемента управления. Такое "свободное" преобразование происходит потому, что метод Convert() будет вызван еще раз после вызова ConvertBack(). Если просто вернуть null из ConvertBackO, то синхронизация привязки будет нарушена, потому что TextBox все еще будет отображать число с плавающей точкой. Установка привязок данных в коде Имея этот класс, можно зарегистрировать специальный преобразователь с любым элементом управления, который желает использовать его. Это допускается сделать
1100 Часть VI. Построение настольных пользовательских приложений с помощью WPF исключительно в XAML-разметке, но тогда потребуется определить ряд специальных объектных ресурсов, которые рассматриваются в следующей главе. А пока класс преобразователя данных можно зарегистрировать в коде. Начните с очистки текущего определения элемента управления <Label> на вкладке Data Binding, чтобы больше не использовалось расширение разметки {Binding}: <Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness=" Content = "/> В конструкторе окна вызовите новую приватную вспомогательную функцию по имени SetBindings(). Ниже показан код SetBindings(): private void SetBindings () ( // Создать объект Binding. Binding b = new Binding (); // Зарегистрировать преобразователь, источник и путь. b.Converter = new MyDoubleConverter (); b.Source = this.mySB; b.Path = new PropertyPath("Value"); // Вызвать метод SetBinding на Label. this.labelSBThumb.SetBinding (Label.ContentProperty, b) ; } Единственный фрагмент этой функции, который выглядит несколько необычно — это вызов SetBinding(). Обратите внимание, что первый параметр обращается к статическому, доступному только для чтения полю класса Label по имени ContentProperty. Как будет показано в главе 31, эта конструкция называется свойством зависимости. Пока просто знайте, что при установке привязки в коде в первом аргументе почти всегда требуется указывать имя класса, который нуждается в привязке (в данном случае Label), за которым следует имя свойства с суффиксом Property (см. главу 31). Запустив приложение, можно удостовериться, что элемент Label отображает только целые числа. Построение вкладки DataGrid К этому моменту построен достаточно интересный пример с использованием исключительно Expression Blend. Однако в оставшейся части проекта будет применяться Visual Studio 2010, что даст некоторое начальное представление относительно совместной работы Blend и Visual Studio. Сохраните проект и закройте Blend. Затем откройте этот же проект в Visual Studio 2010, выбрав пункт меню File^Open^Project/Solution (Файл "=> Открыть1^ Проект/Решение). В предыдущем примере привязки данных было показано конфигурирование двух (или более) элементов управления для участия в операции привязки данных. Допускается также привязывать данные из файлов XML, базы данных и объектов в памяти. Для завершения рассматриваемого примера спроектируем финальную вкладку DataGrid, которая будет отображать информацию, извлеченную из таблицы Inventory базы данных AutoLot. Как и с другими вкладками, начнем с замены текущего контейнера Grid на StackPanel. Сделайте это непосредственным обновлением XAML-разметки в Visual Studio. После этого в новом элементе StackPanel определите элемент управления DataGrid по имени gridlnventory: <TabItem x:Name="tabDataGrid" Header="DataGrid"> <StackPanel> <DataGrid x:Name="gridInventory" Height=88"/> </StackPanel> </TabItem>
Глава 28. Программирование с использованием элементов управления WPF 1101 Затем сошлитесь на сборку AutoLotDAL.dll, созданную в главе 23 (с применением Entity Framework), а также на System.Data.Entity.dll. Поскольку используется API- интерфейс Entity Framework, нужно обеспечить необходимые данные строки соединения в файле App.config. Начать можно с копирования файла App.conf ig из проекта AutoLotEDMGUI, рассматриваемого в главе 23, в текущий проект с помощью пункта меню Projects Add Existing Item (Проекте Добавить существующий элемент). Откройте файл кода окна и добавьте финальную вспомогательную функцию по имени ConfigureGridO; вызовите ее в конструкторе. Предполагая, что пространство имен AutoLotDAL импортировано, все, что остается сделать — это добавить несколько строк кода: private void ConfigureGridO { using (AutoLotEntitien context = new AutoLotEntities () ) { // Построить запрос LINQ, который извлечет некоторые данные из таблицы Inventory. var dataToShow = from c in context.Inventories select new { c.CarlD, c.Make, c.Color, c.PetName }; this.gridlnventory.ItemsSource = dataToShow; } } Обратите внимание, что привязывать напрямую context.Inventories к коллекции ItemsSource сетки не нужно; вместо этого строится запрос LINQ, который запрашивает те же данные в сущностях. Причина такого подхода в том, что объект Inventory также содержит дополнительные свойства EF, которые будут появляться в сетке, но которые не отображаются на физическую базу данных. Запустив этот проект в том виде, как он есть, можно увидеть исключительно простую плоскую сетку. Чтобы немного улучшить ее, воспользуйтесь окном Properties в Visual Studio 2010 для редактирования категории Rows элемента DataGrid. Как минимум, установите свойство AlternationCount равным 2 и задайте специальные цвета в свойствах AlternatingRowBackground и RowBackground, используя интегрированный редактор (возможные значения показаны на рис. 28.47). Properties DataGrid gndlnventory ^f Properties ф Events Rows AlternatingP,o*Background AlternationCcunt AreRowDetailsFrozen CanltserAddRows CanUserDeleteRows CanUserResizeRows MinRowHeight RowDetaiisTemplete RowDetailsvisibilityMode RowHeight RowStyle Ro wV alidationErro rT em pl«t«j RowValidationRules SelectionMode SelectionUnrt Layoot Background BordeiBrush V Рис. 28.47. Некоторые улучшения внешнего вида сетки
1102 Часть VI. Построение настольных пользовательских приложений с помощью WPF Внешний вид финальной вкладки для данного примера показан на рис. 28.48. | Ink API ] Documents [ CariD 83 107 555 678 \щ 1000 1001 l 1992 2222 1 Data Binding : DataGrid | Make Ford Ford Ford vugo BMW BMW Saab Vugo Color Rust Red Yellow Green Black Tar. Pink Blue PetName Rusty- Snake Buzz Ctunker Btmmer Daisy Pinkey Рис. 28.48. Финальная вкладка проекта Следует отметить, что конфигурировать WPF-элемент DataGrid можно огромным числом способов, так что ищите подробности в документации .NET Framework 4.0 SDK. Например, можно построить специальные шаблоны данных для DataGrid с применением графических типов WPF; в конечном счете, это позволит получить исключительно развитый потльзовательский интерфейс. На этом рассматриваемый в главе пример завершен. В последующих главах будут применяться и другие элементы управления, а к настоящему моменту вы должны почувствовать себя увереннее при построении пользовательских интерфейсов с помощью Expression Blend, Visual Studio 2010 и непосредственной работой с XAML-разметкой и кодом С#. Исходный код. Проект WpfControlsAndAPIs доступен в подкаталоге Chapter 28. Резюме В этой главе рассматривались некоторые аспекты, связанные с элементами управления WPF, начиная с обзора инструментария для элементов управления и роли диспетчеров компоновки (панелей). Первый пример был посвящен построению простого приложения текстового процессора. В нем демонстрировалось использование интегрированной в WPF функциональности проверки правописания, а также создание главного окна с системой меню, строкой состояния и панелью инструментов. Что более важно, вы узнали, как строить команды WPF Эти независимые от элемента управления события можно присоединять к элементу пользовательского интерфейса или жесту ввода (input gesture) для автоматического наследования готовой функциональности (например, операций с буфером обмена). Кроме того, вы узнали, как работать с инструментом Microsoft Expression Blend. В частности, с его помощью был построен сложный пользовательский интерфейс. Попутно вы узнали об интерфейсах Ink API и Document API, доступных в WPF. Вы также получили представление об операциях привязки данных WPF, включая использование класса DataGrid из WPF в .NET 4.0 для отображения информации из специальной базы данных AutoLot.
ГЛАВА 29 Службы визуализации графики WPF В этой главе мы рассмотрим средства визуализации графики Windows Presentation Foundation (WPF). Как вы увидите, WPF предлагает три отдельных пути визуализации графических данных — фигуры, рисунки и визуальные объекты. Разобравшись в преимуществах и недостатках каждого подхода, мы приступим к изучению мира интерактивной двухмерной графики с использованием классов из пространства имен System.Windows.Shapes. После этого будет показано, как с помощью рисунков и геометрии визуализировать двухмерные данные в облегченной манере. И, наконец, вы узнаете, обеспечить наивысший уровень мощи и производительности визуального уровня. Попутно рассматривается множество связанных тем, таких как создание специальных кистей и перьев, применение графических трансформаций к визуализациям и выполнение операций проверки попадания (hit-testing). Будет показано, как упростить решение задач кодирования графики с помощью интегрированных инструментов Visual Studio 2010, Expression Blend и Expression Design. На заметку! Графика является ключевым аспектом разработки WPF. Даже если задача не связана с построением приложения с интенсивной графикой (вроде видеоигры или мультимедийного приложения), темы, представленные в этой главе, критичны для работы с такими службами, как шаблоны элементов управления, анимация и настройка привязки данных. Службы графической визуализации WPF В WPF используется особая разновидность графической визуализации, которая называется графикой сохраненного режима (retained-mode). Это означает, что в случае использования XAML-разметки или процедурного кода для генерации графической визуализации, за сохранение этих визуальных элементов и обеспечение их корректной перерисовки и обновления в оптимальной манере отвечает WPF. Поэтому визуализируемые графические данные постоянно присутствуют, даже когда конечный пользователь скрывает изображение, перемещая или сворачивая окно, перекрывая одно окно другим и т.д. В отличие от этого, прежние версии API-интерфейсов визуализации графики (включая GDI+ в Windows Forms) были графическими системами режима интерпретации (immediate-mode). Согласно этой модели на программиста возлагается ответственность за то, чтобы визуализируемые элементы корректно запоминались и обновлялись на протяжении жизни приложения. Например, в приложении Windows Forms визуализа-
1104 Часть VI. Построение настольных пользовательских приложений с помощью WPF ция фигуры, наподобие прямоугольника, предполагала обработку события Paint (или переопределение виртуального метода OnPainO), получение объекта Graphics для рисования прямоугольника и, что наиболее важно, добавление инфраструктуры для обеспечения сохранения изображения, когда пользователь изменяет размеры окна (например, за счет создания переменных-членов для представления позиции прямоугольника и вызова Invalidate () в программе). Переход от графики режима интерпретации к графике сохраненному режиму — однозначно хорошее решение, поскольку позволяет программистам писать и сопровождать меньший объем кода. Однако нельзя утверждать, что графический API-интерфейс WPF полностью отличается от более ранних инструментариев визуализации. Например, подобно GDI+, в WPF поддерживаются разнообразные типы объектов кистей и перьев, техника проверки попадания, области отсечения, графические трансформации и т.д. Поэтому, если есть опыт работы в GDI+ (или GDI), это значит, что уже имеется хорошее представление о том, как выполнять базовую визуализацию под WPF. Опции графической визуализации WPF Как и с другими аспектами разработки WPF, существует выбор, каким образом выполнять графическую визуализацию, а также делать это в XAML-разметке или процедурном коде С# (либо с помощью их комбинации). В частности, в WPF предлагаются три разных подхода к визуализации графических данных. • Фигуры. В пространстве имен System.Windows .Shapes определено небольшое количество классов для визуализации двухмерных геометрических объектов (прямоугольников, эллипсов, многоугольников и т.п.). Хотя эти типы очень просты в применении и достаточно мощные, в случае непродуманного использования они могут привести к значительным накладным расходам памяти. • Рисунки и геометрии. Второй способ визуализации графических данных предусматривает работу с наследниками абстрактного класса System.Windows.Media. Drawing. Применяя такие классы, как GeometryDrawing или ImageDrawing (в дополнение к различным геометрическим объектам), можно осуществлять визуализацию графических данных в более легковесной (но с ограниченными возможностями) манере. • Визуальные объекты. Самый быстрый и наиболее легкий способ визуализации графических данных в WPF предполагает использование визуального уровня, который доступен только в кода С#. С помощью наследников класса System. Windows.Media.Visual можно взаимодействовать непосредственно с графической подсистемой WPF Причина существования разнообразных способов решения одной и той же задачи (т.е. визуализации графических данных) связана с необходимостью оптимального использования памяти и обеспечения приемлемой производительности приложения. Поскольку WPF — система, интенсивно использующая графику, нет ничего необычного в том, что приложению приходится визуализировать сотни или даже тысячи различных изображений на поверхности окна, и возможность выбора реализации (фигуры, рисунки или визуальные объекты) может иметь огромное значение. Следует понимать, что при построении приложения WPF велика вероятность, что придется использовать все три варианта выбора. В качестве эмпирического правила отметим, что если нужен небольшой объем интерактивных графических данных, которыми должен манипулировать пользователь (принимать ввод от мыши, отображать всплывающие подсказки и т.п.), то стоит отдать предпочтение классам из пространства имен System.Windows.Shapes.
Глава 29. Службы визуализации графики WPF 1105 В отличие от этого, рисунки и геометрии лучше подходят, когда необходимо моделировать сложные, в основном не интерактивные, основанные на векторах графические данные с использованием XAML или С#. Хотя рисунки и геометрии могут реагировать на события мыши, позволяют проверять попадание и выполнять операции перетаскивания, обычно для этого приходится писать больше кода. И последнее: если требуется самый быстрый способ визуализации значительных объемов графических данных, то для этого наиболее подходит визуальный уровень (visual layer). Например, предположим, что WPF используется для построения научного приложения, которое должно рисовать тысячи показателей данных. За счет применения визуального уровня эти показатели можно визуализировать самым оптимальным способом. Как будет показано далее в главе, визуальный уровень доступен только из кода С#, а не из XAML. Независимо от выбранного подхода (фигуры, рисунки и геометрии либо визуальные объекты), всегда будут использоваться общие графические примитивы, такие как кисти (для заполнения ограниченных областей), перья (для рисования контуров) и объекты трансформации (которые, разумеется, трансформируют данные). Начнем изучение с классов из пространства System.Windows.Shapes. На заметку! WPF поставляется также с полноценным API-интерфейсом для визуализации и манипулирования трехмерной графикой, которая в этой книге не рассматривается. Подробную информацию по этому поводу можно найти в документации .NET Framework 4.0 SDK. Визуализация графических данных с использованием фигур Члены пространства имен System.Windows.Shapes предлагают самый прямой и интерактивный, но и наиболее ресурсоемкий по занимаемой памяти способ визуализации двумерных изображений. Это пространство имен (определенное в сборке PresentationFramework.dll), достаточно мало, и состоит всего из шести запечатанных (sealed) классов, расширяющих абстрактный базовый класс Shape: Ellipse, Rectangle, Line, Polygon, Polyline и Path. Создайте новое приложение WPF под названием RenderingWithShapes. Найдите в браузере объектов Visual Studio 2010 абстрактный класс Shape (рис. 29.1) и раскройте все родительские узлы. Вы увидите, что каждый наследник Shape получает значительную часть функциональности по цепочке наследования. Некоторые из этих родительских классов должны быть знакомы из материала предыдущих двух глав. Вспомните, например, что в классе UIElement определены многочисленные методы для получения ввода от мыши и обработки событий перетаскивания, а в классе FrameworkElement доступны члены для работы с изменением размеров, всгшывающими подсказками и тому подобным. Имея эту цепочку наследования, имейте в виду, что при визуализации графических данных с использованием классов-наследников Shape, объекты получаются почти столь же функциональными, как элементы управления WPF! Например, определение факта щелчка на визуализированном изображении осуществляется не сложнее, чем обработка события MouseDown. Вот простой пример: написав следующий код XAML Объекта Rectangle: <Rectangle x:Name="myRect" Height=0" Width=0" Fill="Green" MouseDown="myRect_MouseDown"/>
1106 Часть VI. Построение настольных пользовательских приложений с помощью WPF | Object Browser X | Browse My Solution О Sys i> ъ - ч ■ -п * ч Elhpse Line Path Polygon Polyline Rectangle • r^j Base Types * fy FrameworlcElement (• 'O IFrameworklnputElement rJ° IlnputElement "° IQueryAmbient *"° ISupportJnrtialize s *\ UIEIement -° lAnimatable *° IlnputElement л -*$ Visual л tf$ DependencyObject л Ц| DispatcherObject ■Ц Object "* ArrangeOvernde(System.Windows.Size) ?* MeasureOverride(System.Windows.Size) ?% OnRender(System.Windows.Media.DrawingContext} ,♦ ShapeO "^ DefiningGeometry ЙГ« ; J3* Geometry! ransform ^? RenderedGeometry jf Stretch ^ Stroke ЙГ StrokeOashArray '*? StrokeOashCap public abstract ctass Shape : System. Windows. FrameworlcElement Member of S^eriL.WjLndjDwsJi Summary: Provides a base class for shape elements, such as System.Windows. Shapes. Ellipse, System.Wirtdows.Shapes.Potygon, and System.Windows.Shapes.Rectartgle. Рис. 29.1. Базовый класс Shape наследует значительную часть функциональности от своих родительских классов можно реализовать обработчик события MouseDown, который по щелчку на Rectangle изменяет его цвет фона: private void myRect_MouseDown(object sender, MouseButtonEventArgs e) { // Изменить цвет Rectangle при щелчке на нем. myRect.Fill = Brushes.Pink; В отличие от других графических инструментариев, здесь не придется писать громоздкий код инфраструктуры, которая вручную отобразит координаты мыши на геометрию, вручную вычислять попадание в границы, визуализировать в невидимый буфер и т.п. Члены System.Windows.Shapes просто реагируют на события, на которые вы зарегистрируетесь, подобно типичному элементу управления WPF (Button и т.д.). Отрицательная сторона такой готовой функциональности состоит в том, что фигуры потребляют довольно много памяти. При построении научного приложения, которое рисует тысячи точек на экране, применение фигур будет неподходящим выбором (по сути, это будет настолько же расточительным по памяти, как визуализация тысяч объектов Button). Однако когда нужно сгенерировать интерактивное двумерное векторное изображение, фигуры оказываются прекрасным выбором. Помимо функциональности, унаследованной от родительских классов UIEIement и FrameworkElement, в Shape определено множество собственных членов, наиболее полезные из которых перечислены в табл. 29.1. Таблица 29.1. Ключевые свойства базового класса Shape Свойства Назначение DefiningGeometry Fill Возвращает объект Geometry, представляющий общие размеры текущей фигуры. Этот объект содержит только точки, используемые для визуализации данных, не имея следов функциональности UIEIement или FrameworkElement Позволяет указать "объект кисти" для визуализации внутренней области фигуры
Глава 29. Службы визуализации графики WPF 1107 Окончание табл. 29.1 Свойства Назначение GeometryTransform Позволяет применять трансформацию к фигуре, прежде чем она будет визуализирована на экране. Унаследованное свойство Render-Transform (из UIElement) применяет трансформацию после ее визуализации на экране stretch Описывает, как фигура располагается внутри выделенного ей пространства, например, внутри диспетчера компоновки. Это управляется соответствующим перечислением System.Windows.Media.Stretch Stroke Определяет объект кисти или в некоторых случаях — объект пера (который на самом деле также является кистью), используемый для рисования границы фигуры StrokeDashArray, Эти (и прочие) свойства, связанные со штрихами (stroke), управляют StrokeEndLineCap, тем, как сконфигурированы линии при рисовании границ фигуры. StrokeStartLineCap, В большинстве случаев с помощью этих свойств будет настраиваться StrokeThickness кисть, применяемая для рисования границы или линии На заметку! Если вы забудете установить свойства Fill и stroke, WPF предоставит "невидимые" кисти, в результате чего фигура не будет видна на экране! Добавление прямоугольников, эллипсов и линий на поверхность Canvas Далее в этой главе будет показано, как использовать инструменты Expression Blend и Expression Design для генерации XAML-описаний графических данных. А пока давайте построим WPF-приложение, которое может визуализировать фигуры на основе XAML или С#, и параллельно посмотрим, что такое процессе проверки попадания. Прежде всего, добавьте в начальную XAML-разметку элемента <Window> определение контейнера <DockPanel>, содержащего (пока пустые) элементы <Тоо1Ваг> и <Canvas> (полотно). Обратите внимание, что каждому содержащемуся элементу назначается подходящее имя через свойство Name. <DockPanel LastChildFill = ,,True"> <ToolBar DockPanel.Dock="Top11 Name="mainToolBar11 Height=ll50"> </ToolBar> <Canvas Background="LightBlue11 Name=llcanvasDrawingArea"/> </DockPanel> Теперь наполним <ToolBar> набором объектов <RadioButton>, каждый из которых содержит специфический класс-наследник Shape. При этом каждому элементу <RadioButton> назначено одно и то же групповое имя GroupName (чтобы обеспечить взаимное исключение) и соответствующее имя: <ToolBar DockPanel.Dock="Top" Name=,,mainToolBar" Height=,,50"> <RadioButton Name="circleOption11 GrpupName=" shapeSeleetion"> <Ellipse Fill = ,,Green" Height=M35M Width=,,35" /> </RadioButton> <RadioButton Name="rectOption11 GroupName=llshapeSelection"> <Rectangle Fill=,,Red" Height=5" Width=5" Radiu5Y=,,10" RadiusX=,,10" /> </RadioButton>
1108 Часть VI. Построение настольных пользовательских приложений с помощью WPF <RadioButtan Name="lineOption11 GroupName=llshapeSelection"> <Line Height=M35M Width=,,35" StrokeThickness=0" Stroke="Blue11 Xl = 0" Yl = ,,10" Y2 = ,,25" X2 = ,,25" StrokeStartLineCap="Triangle11 StrokeEndLineCap="Round11 /> </RadioButton> </ToolBar> Как видите, объявления объектов Rectangle, Ellipse и Line в XAML довольно прямолинейны и требуют минимума комментариев. Вспомните, что свойство Fill позволяет указать кисть для рисования внутренностей фигуры. Когда нужна кисть сплошного цвета, можете просто задать жестко закодированную строку известных значений, а соответствующий преобразователь типов (см. главу 28) сгенерирует корректный объект. Одна интересная характеристика типа Rectangle связана с тем, что в нем определены свойства RadiusX и RadiusY, позволяющие при желании визуализировать скругленные углы. Линия Line представляется начальной и конечной точками с помощью свойств XI, Х2, Y1 и Y2 (учитывая, что высота и ширина применительно к линии не имеют смысла). В разметке устанавливаются несколько дополнительных свойств, управляющих визуализацией начальной и конечной точек Line, а также конфигурирующих настройки штриха. На рис. 29.2 показана визуализированная панель инструментов в визуальном конструкторе WPF в Visual Studio 2010. щ Е Fun with Shapes! •Iv . Рис. 29.2 Использование объектов Shape в качестве содержимого для набора элементов RadioButton Теперь с помощью окна Properties (Свойства) среды Visual Studio 2010 создайте обработчики события MouseLef tButtonDown для Canvas и события Click для каждой RadioButton. В файле С# цель заключается в том, чтобы визуализировать выбранную фигуру (круг, квадрат или линию), когда пользователь щелкнет на Canvas. Для начала определите следующее встроенное перечисление (и соответствующую переменную- член) внутри класса, унаследованного от Window: public partial class MainWindow : Window { private enum SelectedShape { Circle, Rectangle, Line } private SelectedShape currentShape; } Внутри каждого обработчика Click установите для переменной-члена currentShape корректное значение SelectedShape. Например, ниже показан код обработчика события Click объекта RadioButton по имени circleOption. Реализуйте остальные обработчики Click аналогичным образом:
Глава 29. Службы визуализации графики WPF 1109 private void circleOption_Click(object sender, RoutedEventArgs e) { currentShape = SelectedShape.Circle; } С помощью обработчика события MouseLef tButtonDown элемента Canvas будет визуализироваться корректная фигура (предопределенного размера) в начальной точке, соответствующей позиции X, Y курсора мыши. Ниже показана полная реализация: private void canvasDrawingArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Shape shapeToRender = null; // Сконфигурировать корректную фигуру для рисования. switch (currentShape) { case SelectedShape.Circle: shapeToRender = new Ellipse () { Fill = Brushes.Green, Height = 35, Width =35 }; break; case SelectedShape.Rectangle: shapeToRender = new Rectangle () { Fill = Brushes.Red, Height = 35, Width = 35, RadiusX = 10, RadiusY = 10 }; break; case SelectedShape.Line: shapeToRender = new Line() { Stroke = Brushes.Blue, StrokeThickness = 10, XI =0, X2 = 50, Yl = 0, Y2 = 50, StrokeStartLineCap= PenLineCap.Triangle, StrokeEndLineCap = PenLineCap.Round }; break; default: return; } // Установить верхний левый угол для рисования на полотне. Canvas.SetLeft(shapeToRender, e.GetPosition(canvasDrawingArea).X); Canvas.SetTop (shapeToRender, e.GetPosition(canvasDrawingArea) .Y); // Нарисовать фигуру. canvasDrawingArea.Children.Add(shapeToRender); } На заметку! Вы можете заметить, что объекты Ellipse, Rectangle и Line, создаваемые в методе, имеют те же настройки свойств, что и соответствующие определения XAML Как и ожидалось, этот код можно упростить, но это требует понимания объектных ресурсов WPF, которые рассматриваются в главе 30. В коде производится проверка переменной-члена currentShape для создания корректного объекта-наследника Shape. После этого устанавливаются значения координат левой верхней вершины внутри Canvas с использованием входного объекта MouseButtonEventArgs. И, наконец, в коллекцию объектов UIElement, поддерживаемых Canvas, добавляется новый объект-наследник Shape. Если теперь запустить программу, то можно щелкать левой кнопки мыши где угодно на Canvas и при этом на полотне будут появляться фигуры.
1110 Часть VI. Построение настольных пользовательских приложений с помощью WPF Удаление прямоугольников, эллипсов и линий с поверхности Canvas Имея Canvas с коллекцией объектов, может возникнуть вопрос: как теперь динамически удалить элемент, возможно, в ответ на щелчок пользователя правой кнопкой мыши на фигуре? Это делается с помощью класса VisualTreeHelper из пространства имен System.Windows.Media. В главе 31 вы узнаете о роли "визуальных деревьев" и "логических деревьев". А пока обработаем событие MouseRightButtonDown класса Canvas и реализуем обработчик, как показано ниже: private void canvasDrawingArea_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { // Сначала получить координаты X,Y щелчка пользователя. Point pt = e.GetPosition((Canvas)sender); // Воспользоваться методом HitTest() класса VisualTreeHelper, // чтобы проверить попадание на элемент в Canvas. HitTestResult result = VisualTreeHelper.HitTest (canvasDrawingArea, pt) ; // Если результат не равен null, щелчок произведен на фигуре. if (result l= null) { // Получить фигуру, на которой совершен щелчок, и удалить ее из Canvas. canvasDrawingArea.Children.Remove(result.VisualHit as Shape); } } Этот метод начинается с получения точного местоположения X, Y, где пользователь щелкнул в Canvas, и проверяет попадание через статический метод VisualTreeHelper. HitTest(). Возвращенное значение — объект HitTestResult — будет установлен в null, если пользователь не щелкнул на UIElement внутри Canvas. Если HitTestResult не равен null, с помощью свойства VisualHit можно получить элемент UIElement, на котором совершен щелчок, и привести его к объекту-наследнику Shape (вспомните, что Canvas может содержать любой UIElement, а не только фигуры). Подробности устройства "визуального дерева" будут изложены в следующей главе. На заметку! По умолчанию VisualTreeHelper.HitTest() возвращает UIElement верхнего уровня и не предоставляет информацию о других объектах, находящихся ниже (те перекрытых в Z-порядке). После этих модификаций появляется возможность добавлять фигуру на Canvas щелчком левой кнопки мыши и удалять ее щелчком правой кнопки мыши. На рис. 29.3 продемонстрирована функциональность текущего примера. Пока все хорошо. Мы использовали объекты-наследники Shape для визуализации содержимого элементов RadioButton, используя XAML-разметку и заполняя Canvas кодом С#. При рассмотрении роли кистей и графических трансформаций к этому примеру будет добавлена еще некоторая функциональность. Кстати говоря, в другом примере этой главы иллюстрируется реализация технологии перетаскивания на объектах UIElement. А пока уделим внимание членам пространства имен System.Windows.Shapes. Работа с элементами Polyline и Polygon В текущем примере используются только три класса-наследника Shape. Остальные дочерние классы (Polyline, Polygon и Path) очень утомительно корректно визуализировать, не используя инструмента вроде Expression Blend — просто потому, что они требуют рисования большого количества точек для своего представления.
Глава 29. Службы визуализации графики WPF 1111 [ l» Fun with Shapes! UrlIll-УИНИ Рис. 29.3. Работа с фигурами Инструмент Expression Blend будет применяться чуть позже, а сейчас представим краткий обзор остальных типов Shapes. Тип Polyline позволяет определить коллекцию координат [х, у) (через свойство Points) для рисования серий сегментов линий, не требующих замыкания. Тип Polygon похож, однако он запрограммирован так, что всегда замыкает контур, соединяя начальную точку с конечной, и заполняет внутреннюю область указанной кистью. Предположим, что показанный ниже элемент <StackPanel> описан в редакторе Kaxaml или в специальном редакторе XAML, который был создан в качестве примера в главе 27: <•-- Элемент Polyline автоматически не замыкает концы --> <Polyline Stroke ="Red11 StrokeThickness =0" StrokeLineJoin ="Round" Points =M10,10 40,40 10,90 300,50M/> <!-- Элемент Polygon всегда замыкает концы --> <Polygon Fill ="AliceBlue11 StrokeThickness =" Stroke ="Green" Points =0,10 70,80 10,50" /> На рис. 29.4 показывает визуализированный вывод. Рис. 29.4. Элементы Polyline и Polygon Работа с элементом Path Используя только типы Rectangle, Ellipse, Polygon, Polyline и Line, нарисовать детализированное двухмерное векторное изображение было бы чрезвычайно сложно, поскольку эти примитивы не позволяют легко фиксировать графические данные, подобные кривым, объединениям перекрывающихся данных и т.д. Последний унаследованный от Shape класс Path предоставляет возможность определять сложные двухмерные
1112 Часть VI. Построение настольных пользовательских приложений с помощью WPF графические данные как коллекцию независимых геометрий. После определения коллекцию таких геометрий можно присвоить свойству Data класса Path, где эта информация будет применяться для визуализации сложного двухмерного изображения. Свойство Data получает экземпляр класса, унаследованного от System.Windows. Media.Geometry, который содержит ключевые члены, перечисленные в табл. 29.2. Таблица 29.2. Избранные члены класса System.Windows.Media.Geometry Член Назначение Bounds FillContainsO GetAreaO GetRenderBounds () Transform Устанавливает текущий ограничивающий прямоугольник, содержащий в себе геометрию Определяет, находится ли данный Point (или другой объект Geometry) в границах определенного класса-наследника Geometry. Это полезно для вычислений попадания Возвращает общую область, Занятую объектом-наследником Geometry Возвращает Rect, содержащий минимальный возможный прямоугольник, который может быть использован для визуализации объекта класса- наследника Geometry Назначает геометрии объект Transform для изменения визуализации Классы, расширяющие Geometry (табл. 29.3), выглядят очень похоже на свои аналоги, унаследованные от Shape. Например, EllipseGeometry имеет члены, похожие на члены Ellipse. Значительное отличие состоит в том, что классы-наследники Geometry не знают, как визуализировать себя непосредственно, поскольку они не являются UIElement. Вместо этого классы-наследники Geometry представляют всего лишь коллекцию данных о точках, которая указывает Path, как визуализировать себя. На заметку! Path — не единственный класс в WPF, который может работать с коллекциями геометрий. Например, DoubleAnimationUsingPath, DrawingGroup, GeometryDrawing и даже UIElement могут использовать геометрии для визуализации, применяя свойства PathGeometry, ClipGeometry, Geometry и Clip соответственно. Таблица 29.3. Классы, унаследованные от Geometry Класс Назначение LineGeometry RectangleGeometry EllipseGeometry GeometryGroup CombinedGeometry PathGeometry Представляет прямую линию Представляет прямоугольник Представляет эллипс Позволяет сгруппировать вместе несколько объектов Geometry Позволяет объединить два разных объекта Geometry в единую фигуру Представляет фигуру, состоящую из линий и кривых Ниже приведена определенная в Kaxaml разметка для элемента Path, который использует несколько производных от Geometry типов. Обратите внимание, что свойство Data объекта Path устанавливается в объект GeometryGroup, содержащий другие объекты-наследники Geometry, такие как EllipseGeometry, RectangleGeometry и LineGeometry. Результат можно видеть на рис. 29.5.
Глава 29. Службы визуализации графики WPF 1113 <|-- Элемент Path содержит набор объектов Geometry, установленный в свойстве Data --> <Path Fill = "Orange" Stroke = "Blue" StrokeThickness = "> <Path.Data> <GeometryGroup> <EllipseGeometry Center = 5,70" RadiusX = 0" RadiusY = 0" /> <RectangleGeometry Rect = 5,55 100 30" /> <LineGeometry StartPoint=,0" EndPoint=0,30" /> <LineGeometry StartPoint=0,30" EndPoint=,30" /> </GeometryGroup> </Path.Data> </Path> ©3 Рис. 29.5. Элемент Path, содержащий различные объекты Geometry Изображение на рис. 29.5 может быть визуализировано с помощью показанных ранее классов Line, Ellipse и Rectangle. Однако для этого потребуется поместить в память несколько объектов UIElement. Когда для моделирования того, что нужно нарисовать, используются геометрии, а затем коллекция геометрий помещается в контейнер, который может визуализировать данные (в данном случае — Path), тем самым сокращается расход памяти. Теперь вспомните, что Path имеет ту же цепочку наследования, что и любой член System.Windows.Shapes, и потому в состоянии посылать те же уведомления о событиях, что и другие элементы UIElement. Поэтому, если определить тот же элемент <Path> в проекте Visual Studio 2010, то можно будет выяснить, когда пользователь щелкнет в любом месте линии, просто обрабатывая событие мыши (редактор Kaxaml не позволяет обрабатывать события в написанной разметке). Мини-язык моделирования путей Из всех классов, перечисленных в табл. 29.2Т PathGeometry является наиболее сложным для конфигурирования в терминах XAML и кода. Это связано с тем, что каждый сегмент PathGeometry состоит из объектов, содержащих различные сегменты и фигуры (например, ArcSegment, BezierSegment, LineSegment, PolyBezierSegment, PolyLineSegment, PolyQuadraticBezierSegment и т.д.). Ниже приведен пример объекта Path, свойство Data которого установлено в <PathGeometry>, состоящий из различных фигур и сегментов. •'Path Stroke="Black" StrokeThickness = "l"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint=0,50"> <PathFigure.Segments> <BezierSegment Pointl=00,0" Point2=00,200"
1114 Часть VI. Построение настольных пользовательских приложений с помощью WPF Point3=00,100"/> <LineSegment Point=00,100" /> <ArcSegment Size=0,50м RotationAngle=5M IsLargeArc="True11 SweepDi re с tion=" Clockwise" Point=00,100"/> </PathFigure.Segments> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> Следует отметить, что очень немногим программистам понадобится когда-либо вручную строить сложные двухмерные изображения, непосредственно описывая объекты классов-наследников Geometry или PathSegment. В действительности сложные пути будут формироваться графически с использованием инструмента Expression Blend или Expression Design. Но даже учитывая помощь указанных инструментов, объем XAML-разметки, требуемый для определения сложных объектов Path, может устрашать, поскольку такие данные должны включать полные описания разнообразных классов-наследников Geometry или PathSegment. Для того чтобы создавать более краткую и компактную разметку, в классе Path поддерживается специализированный "мини-язык". Например, вместо установки для свойства Data объекта Path коллекции объектов- наследников Geometry и PathSegment, его можно установить в единственный строковый литерал, содержащий набор известных символов и различных значений, которые определяют фигуру, подлежащую визуализации. Фактически, инструменты Expression Blend и Expression Design для построения объекта Path внутри автоматически используют мини-язык. Рассмотрим простой пример и его результирующий вывод (рис. 29.6): <Path Stroke="Black" StrokeThickness=" Data="M 10,75 С 70,15 250,270 300,175 H 240" /> Рис. 29.6. Мини-язык моделирования путей позволяет компактно описывать модель объекта Geometry/PathSegment Команда М (от move — двигать) принимает координаты X,Y позиции, представляющей начальную точку рисования. Команда С принимает серию точек для визуализации кривой (точнее — кубической кривой Безье), а команда Н рисует горизонтальную линию. И снова следует отметить, что вручную строить или разбирать строковый литерал, содержащий инструкции мини-языка, придется очень редко. Но, по крайней мере, XAML-разметка, сгенерированная Expression Blend, не будет казаться чем-то непонятным. Если интересуют детали этой конкретной грамматики, обращайтесь в раздел "Path Markup Syntax" ("Синтаксис разметки путей") документации .NET Framework 4.0 SDK.
Глава 29. Службы визуализации графики WPF 1115 Кисти и перья WPF Каждый из способов графической визуализации (фигура, рисование и геометрии, а также визуальные объекты) интенсивно использует кисти, что позволяет управлять заполнением внутренней области двухмерной фигуры. В WPF предлагаются шесть разных типов кистей, и все они расширяют класс System.Windows.Media.Brush. Хотя Brush — абстрактный класс, его наследники, перечисленные в табл. 29.4, могут применяться для заполнения области содержимым почти любого рода. Таблица 29.4. Классы, унаследованные от Brush Класс Назначение DrawingBrush Заполняет область объектом-наследником Drawing (GeometryDrawing, ImageDrawing или VideoDrawing) ImageBrush Заполняет область изображением (представленным объектом ImageSource) LinearGradientBrush Заполняет область линейным градиентом RadialGradientBrush Заполняет область радиальным градиентом SolidColorBrush Рисует сплошной цвет, установленный свойством Color VisualBrush Заполняет область объектом-наследником Visual (DrawingVisual, Viewport3DVisualи ContentVisual) Классы DrawingBrush и VisualBrush позволяют строить кисти на основе существующего класса, унаследованного от Drawing или Visual. Эти классы кистей используются во время работы с двумя другими опциями графики WPF (рисунками и визуальными объектами) и будут объясняться далее в этой главе. Класс ImageBrush позволяет строить кисть, отображающую графические данные йЬ внешнего файла или встроенного ресурса приложения, указанного в его свойстве ImageSource. Остальные типы кистей (LinearGradientBrush и RadialGradientBrush) использовать достаточно легко, хотя набор необходимого кода XAML может быть утомительным. К счастью, в среде Visual Studio 2010 поддерживаются интегрированные редакторы кистей, которые упрощают задачу генерации стилизованных кистей. Конфигурирование кистей с использованием Visual Studio 2010 Теперь давайте обновим WPF-приложение рисования RenderingShapes для использования некоторых интересных кистей. Три фигуры, с которыми мы имели дело до сих пор при визуализации данных в панели инструментов, используют простые сплошные цвета, так что их значения можно зафиксировать простыми строковыми литералами. Чтобы сделать задачу немного более интересной, применим теперь интегрированный редактор кистей. Убедитесь, что редактор XAML начального окна открыт в IDE-среде и выберите элемент Ellipse. Теперь найдите свойство Fill в окне Properties и щелкните на раскрывающемся списке. Откроется редактор кистей, показанный на рис. 29.7. Как видите, этот редактор содержит четыре ползунка, которые позволяют устанавливать значение ARGB (цветовые компоненты alpha, red, green и blue (прозрачность, красный, зеленый и синий)) для текущей кисти. Используя эти ползунки и связанную с ними область выбора цвета, можно создать любой сплошной цвет. С помощью этого инструмента измените цвет элемента Ellipse и просмотрите результирующую XAML-разметку. Вы заметите, что цвет сохраняется в виде шестнадцатеричного значения, например: <Ellipse Fill = "#FF47CE47" Height=,,35" Width=,,35" />
1116 Часть VI. Построение настольных пользовательских приложений с помощью WPF Что более интересно, тот же редактор позволяет конфигурировать и градиентные кисти, которые используются для определения серий цветов и точек перехода. В левом верхнем углу редактора кистей находятся четыре маленькие кнопки, первая из которых позволяет установить null-кистъ для визуализированного вывода. Остальные три предназначены для установки кисти сплошного цвета (это было только что сделано), градиентной кисти и кисти-изображения. Щелкните на кнопке градиентной кисти, и редактор отобразит несколько новых опций (рис. 29.8). Три кнопки в нижнем левом углу позволяют выбрать вертикальный, горизонтальный или радиальный градиент. Лента внизу покажет текущий цвет каждого градиентного перехода, и каждый из них будет помечен специальным ползунком. По мере перетаскивания ползунка по полосе градиента можно управлять смещением градиента. Более того, щелкнув на соответствующем ползунке, можно изменить цвет определенного градиентного перехода через селектор цвета. Наконец, щелчок на полосе градиента позволяет добавить дополнительные градиентные переходы. Потратьте некоторое время на освоение этого редактора, чтобы построить радиальную градиентную кисть, содержащую три градиентных перехода, и установите их цвета по своему выбору. На рис. 29.8 показа пример кисти, использующей три разных оттенка зеленого. В результате IDE-среда обновит XAML-разметку набором специальных кистей, установив их в соответствующих свойствах (в данном примере — свойство Fill элемента Ellipse) с применением синтаксиса "свойство-элемент". Например: <Ellipse Height = ,,35" Width=,,35"> <Ellipse.Fill> <RadialGradientBrush> <GradientStop Color=,,#FF87E71B" Of fset =  . 589" /> <GradientStop Color=,,#FF2BA92B" Of f set= . 013" /> <GradientStop Color="#FF34B71B" Offset="l" /> *" / Radi a 1 Gradient Br ush> </Ellipse.Fill> </Ellipse> ■ Properties * П X i I EBpsc <noname> ^^м j I ^"Properties -/ Events ^^r | ; i Р| [search Effect □ ♦ 1 Green jT] FlowDirectij 0)И;|] Q Focusable j _ fsl FocusVisua _ ° ,^ ForceCHJ iBB ШШШШшХшшШШ О Horizontal/ ■ ■ ° j IsHitTestViJ ^^^^™" ^Ъ ] Ш Green / IsManipulat LayoutTrarJ ^ Margin □ 0 MaxHeiqht □ Infinity - j Рис. 29.7. Любое свойство, требующее кисти, может быть сконфигурировано с помощью интегрированного редактора кистей Рис. 29.8. Редактор кистей Visual Studio позволяет строить базовые градиентные кисти
Глава 29. Службы визуализации графики WPF 1117 Конфигурирование кистей в коде Итак, мы построили специальную кисть для XAML-определения элемента Ellipse, но поскольку соответствующий код С# устарел, все равно получается круг со сплошным зеленым цветом. Чтобы синхронизировать все, обновим соответствующий оператор case для использования только что созданной кисти. Ниже приведено необходимое обновление, которое выглядит несколько сложнее, чем можно было ожидать, поскольку шестна- дцатеричное значение преобразуется в правильный объект Color через класс System. Windows.Media.ColorConverter (модифицированный вывод показан на рис. 29.9). case SelectedShape.Circle: shapeToRender = new Ellipse () { Height = 35, Width = 35 }; // Создать кисть RadialGradientBrush в коде. RadialGradientBrush brush = new RadialGradientBrush(); brush.Gradientstops.Add(new Gradientstop ( (Color)ColorConverter.ConvertFromString("#FF87E71B"), 0.589)); brush.GradientStops.Add(new GradientStop ( (Color)ColorConverter.ConvertFromString("#FF2BA92B"), 0.013)); brush.GradientStops.Add(new GradientStop ( (Color) ColorConverter .ConvertFromString (,,#FF34B71B") , 1) ) ; shapeToRender.Fill = brush; break; ■ Fun with Shapes! 1 *4 ' ■ * I \ ч l^| (HI [-Д-^ Рис. 29.9. Рисование более изящных кругов Кстати, объекты GradientStop можно строить, указывая простой цвет в качестве первого параметра конструктора с использованием перечисления Colors, возвращающего сконфигурированный объект Color: GradientStop g = new GradientStop(Colors.Aquamarine, 1) ; Если же нужен более тонкий контроль, можно сразу передать сконфигурированный объект Color, например: Color myColor = new Color () { R = 200, G = 100, В = 20, A = 40 }; GradientStop g = new GradientStop(myColor, 34); Разумеется, применение перечисления Colors и класса Color не ограничено градиентными кистями. Их можно использовать где угодно — там, где нужно представлять значение цвета в коде.
1118 Часть VI. Построение настольных пользовательских приложений с помощью WPF Конфигурирование перьев В отличие от кисти, перо (реп) — это объект для рисования границ геометрий, либо в случае класса Line или PolyLine — самих геометрий. В частности, класс Реп позволяет рисовать линию заданной толщины, представленную значением типа double. Вдобавок Реп может быть сконфигурирован с помощью тех же свойств, что были представлены в классе Shape, таких как начальный и конечный концы пера, шаблоны точек-тире и т.д. Например: <Pen Thickness=0" LineJoin="Round11 EndLineCap="Triangle11 StartLineCap="Round11 /> Во многих случаях непосредственно создавать объект Реп не придется, поскольку это делается неявно, когда устанавливаются значения для свойств, таких как StrokeThickness объект типа-наследника Shape (а также других типов UIElement). Однако построение специального объекта Реп может пригодиться при работе с типами- наследниками Drawing (описанными далее в этой главе). В Visual Studio 2010 редактор перьев, как таковой, отсутствует, но в окне Properties можно конфигурировать все свойства выбранного элемента, связанные со штрихами. Применение графических трансформаций Чтобы завершить обсуждение использования фигур, рассмотрим тему трансформаций. WPF поставляется с многочисленными классами, расширяющими абстрактный базовый класс System.Winodws. Media. Trans form. В табл. 29.5 перечислены основные классы, унаследованные от Transform. Таблица 29.5. Основные классы, унаследованные от System.Windows.Media.Transform Класс Назначение MatrixTransform Создает произвольную матричную трансформацию, используемую для манипулирования объектами или координатными системами на двухмерной поверхности RotateTransform Поворачивает объект по часовой стрелке вокруг указанной точки в двухмерной системе координат (х, у) ScaleTransform Масштабирует объект в двухмерной системе координат (х, у) SkewTransform Скашивает объект в двухмерной системе координат (х, у) TranslateTransform Транслирует (перемещает) объект в двухмерной системе координат (х, у) TransformGroup Представляет объект Transform, состоящий из других объектов Transform Трансформации могут применяться к любому классу UIElement (те. к наследникам Shape, а также к таким элементам управления, как Button, TextBox и им подобным). Используя эти классы трансформаций, можно визуализировать графические данные под заданным углом, смещать изображения по поверхности, растягивать, сжимать или поворачивать целевой элемент самыми разными способами. На заметку! Хотя объекты трансформаций могут применяться повсеместно, вы найдете их наиболее удобными при работе с анимацией WPF и специальными шаблонами элементов управления. Как будет показано далее, анимацию WPF можно использовать, чтобы включать для специального элемента управления визуальные знаки, предназначенные конечным пользователям.
Глава 29. Службы визуализации графики WPF 1119 \ Объекты трансформаций (или целые их множества) могут назначаться целевому объекту (Button, Path, и т.п.) с помощью двух общих свойств. Свойство LayoutTransform удобно тем, что трансформация происходит перед визуализацией элементов в диспетчере компоновки, и потому не влияют на z-упорядочивание (другими словами, трансформируемые данные изображений не перекрываются). Свойство RenderTransform, с другой стороны, работает после того, как элементы оказались в своих контейнерах, и потому вполне возможно, что элементы могут быть трансформированы таким образом, что могут перекрывать друг друга, в зависимости от того, как они организованы в контейнере. Первый взгляд на трансформации Ниже будет добавлена некоторая логика трансформаций к проекту Rendering WithShapes. Чтобы увидеть объект трансформации в действии, откройте редактор Kaxaml (или собственный специальный редактор XML) и определите простой элемент <StackPanel> в корне <Раде> или <Window>, установив для свойства Orientation значение Horizontal. Теперь добавьте следующий элемент <Rectangle>, который будет нарисован под углом в 45 градусов с использованием объекта RotateTransform. <•-- Элемент Rectangle с трансформацией поворота --> <Fectangle Height =00" Width =0" Fill ="Red"> <Rectangle.LayoutTransform> <RotateTransform Angle =5"/> ^-/Rectangle. LayoutTransf orm> </Rectangle> Далее элемент <Button> скашивается по поверхности на 20% с помощью трансформации <SkewTransform>: <!-- Элемент Button с трансформацией скоса --> <Button Content ="Click Me1" Width="95" Height=0"> <Button.LayoutTransform> <SkewTransform AngleX =0" AngleY =0"/> </Button.LayoutTransform> </Button> И для полноты картины ниже приведен элемент <Ellipse>, масштабированный на 20% посредством трансформации ScaleTransform (обратите внимание на значения, установленные в свойствах Height и Width), а также элемент <TextBox>, к которому применена групповая трансформация: <*-- Элемент Ellipse, масштабированный на 20% --> <Ellipse Fill ="Blue" Width=" Height="> <Ellipse.LayoutTransform> <ScaleTransform ScaleX =0" ScaleY =0"/> </Ellipse.LayoutTransform> </Ellipse> <•-- Элемент TextBox, повернутый и скошенный --> <TextBox Text ="Me Too!" Width=0"' Height=0"> <TextBox.LayoutTransform> <TransformGroup> <RotateTransform Angle =5"/> <SkewTransform AngleX =" AngleY =0"/> </TransformGroup> </TextBox.LayoutTransform> </TextBox> При применении трансформации не обязательно выполнять какие-либо ручные вычисления для корректного определения попадания, принятия фокуса ввода и т.п. Графический
1120 Часть VI. Построение настольных пользовательских приложений с помощью WPF механизм WPF самостоятельно решает такие задачи. Например, на рис. 29.10 можно видеть, что элемент TextBox по-прежнему реагирует на клавиатурный ввод. Рис. 29.10. Результат применения трансформаций к графическим элементам Трансформация данных Canvas Теперь давайте посмотрим, как включить некоторую логику трансформации в пример RencleringWithShapes. В дополнение к применению объекта трансформации к одиночному элементу (Rectangle, TextBox и т.д.), трансформации можно также применять на уровне диспетчера компоновки, чтобы трансформировать все его внутренние данные. Например, можно визуализировать всю панель <DockPanel> главного окна, повернув ее на заданный угол: <DockPanel LastChilflFill="True"> ^DockPanel.LayoutTransform> <PotateTransform Angle=5"/> « /DockPanel.LayoutTransform> </DockPanel> Это чересчур экстремально для рассматриваемого примера, поэтому давайте добавим последнее (менее агрессивное) средство, которое позволит пользователю повернуть весь контейнер Canvas с вложенной в него графикой. Начните с добавления финального элемента <ToggleButton> к <Тоо1Ваг>, определив его следующим образом: <ToggleButton Name="flipCanvas" Click="flipCanvas_Click" Content="Flip Canvas!"/> Внутри обработчика событий Click, который инициируется щелчком на новой кнопке переключения ToggleButton, создадим объект RotateTransform и подключим его к объекту Canvas через свойство LayoutTransform. Если ToggleButton не отмечена, трансформация удаляется путем установки этого же свойства в null. private void flipCanvas_Click(object sender, RoutedEventArgs e) { if (flipCanvas.IsChecked true) RotateTransform rotate = new RotateTransform(-180); canvasDrawingArea.LayoutTransform = rotate; } else { canvasDrawingArea.LayoutTransform = null; } Запустите приложение и добавьте какие-то графические фигуры в область Canvas. После щелчка на новой кнопке обнаруживается, что данные фигуры выходят за границы Canvas (рис. 29.11). Причина в том, что не был определен прямоугольник отсечения.
Глава 29. Службы визуализации графики WPF 1121 Рис. 29.11. Фигуры выходят за границы Canvas после трансформации Исправить это просто. Вместо того чтобы вручную писать сложную логику отсечения, просто установите свойство ClipToBounds элемента <Canvas> в true, что предотвратит визуализацию дочерних элементов вне границ родителя. Запустив программу вновь, вы обнаружите, что теперь графические данные не покидают границ отведенной области. <Canvas ClipToBounds = "True" . . . > Последняя небольшая модификация, которую понадобится провести, связана с тем фактом, что когда вы поворачиваете холст нажатием кнопки переключения, а затем щелкаете на нем для рисования новой фигуры, точка, где был произведен щелчок, не является той позицией, куда попадут графические данные. Вместо этого они появятся под курсором мыши. Чтобы исправить эту проблему, давайте еще раз взглянем на исходный код примера, к которому добавляется еще одна переменная-член типа Boolean (isFlipped). Она обеспечивает применение той же трансформации к ранее нарисованной фигуре перед выполнением визуализации (через RenderTransform). Ниже показан соответствующий фрагмент кода. private void canvasDrawmgArea_MouseLeftButtonDown (object sender, MouseButtonEventArgs e) { Shape shapeToRender = null; // isFlipped — приватное булевское поле. Его значение // изменяется при щелчке на кнопке переключения. if (isFlipped) { RotateTransform rotate = new RotateTransform(-180); shapeToRender.RenderTransform = rotate; } // Установить верхнюю левую точку для рисования на холсте. Canvas .Set Left (shapeToRender, e . Get Posit ion (canvas DrawmgArea) . X) ; Canvas . SetTop (shapeToRender, e . GetPosition (canvasDrawmgArea) . Y) ; // Нарисовать фигуру. сanvasDrawingArea.Children.Add(shapeToRender) ; На этом рассмотрение фигур (System.Windows.Shapes), кистей и трансформаций завершено. Прежде чем перейти к теме визуализации графики с помощью рисунков и геометрий, давайте посмотрим, как инструмент Expression Blend может упростить работу с примитивными графическими элементами.
1122 Часть VI. Построение настольных пользовательских приложений с помощью WPF Исходный код. Проект RenderingWithShapes доступен в подкаталоге Chapter 29. Работа с фигурами в Expression Blend Хотя в IDE-среде Visual Studio 2010 для работы с фигурами предусмотрено несколько инструментов, средство Expression Blend предлагает гораздо больше возможностей, включая редактор графических трансформаций. Чтобы попробовать их, запустите Expression Blend и создайте новое приложение WPF (назовите проект как вам угодно). Выбор фигуры для визуализации из палитры инструментов Палитра инструментов позволяет выбирать инструменты Ellipse, Rectangle и Line (щелчок и удержание кнопки с маленьким треугольником справа внизу позволяет открыть все доступные выборы, как показано на рис. 29.12). Нарисуйте несколько перекрывающихся фигур. Найдите область в палитре инструментов Blend, где расположены инструменты Реп и Pencil (рис. 29.13). Инструмент Pencil (карандаш) позволяет визуализировать произвольные многоугольники. Инструмент Реп (перо) больше подходит для визуализации дуг или прямых линий. Чтобы рисовать линию с помощью Реп, щелкните на желаемой начальной точке, а затем — на желаемой конечной точке в визуальном конструкторе. Имейте в виду, что оба эти средства генерируют определение Path, свойство Data которого устанавливается с применением мини-языка описания путей, упомянутого ранее в главе. Поработайте с этими инструментами и постройте какую-нибудь графику в визуальном конструкторе. Обратите внимание, что выбор инструмента Direct Selection на любом Path позволяет настраивать сегменты соединения (рис. 29.14). Рис. 29.12 Визуализация базовых фигур с помощью Blend Рис. 29.13. Работа с перьями и карандашами Рис. 29.14. Редактирование Path
Глава 29. Службы визуализации графики WPF 1123 Преобразование фигур в пути Инструменты Ellipse, Rectangle или Line соответствуют в XAML-раз метке элементам <Ellipse>, <Rectangle> и <Line>. Например: <Rectangle Stroke="Black" StrokeThickness="l" HorizontalAlignment="Left" Margin=4,42,0,0" VerticalAlignment="Top" Width=37" Height=5"/> Щелкните правой кнопкой мыши на фигуре в визуальном конструкторе выберите в контекстном меню пункт PatlT=>Convert to Path (Путь1^ Преобразовать в путь). В результате объявление исходной фигуры преобразуется в путь Path, содержащий набор геометрий, которые представлены с помощью мини-языка описания путей. Вот как выглядит предыдущий элемент <Rectangle> после преобразования: <Path Stretch="Fill" Stroke="Elack" StrokeThickness="l" HorizontalAlignment="Left" Margin=4/ 42, 0, 0" VerTncalAiigiiment=,,Top" Width=37" Height=5" Data="M0.5,0.5 L136.5,0.5 L136.5,34.5 LO.5,34.5 z"/> Преобразование одиночных фигур в путь, состоящий из геометрий, требуется редко, но гораздо чаще его приходится применять для комбинирования множества фигур в путь. Комбинирование фигур Выберите две или более фигур в визуальном конструкторе (с помощью операции щелчка и перетаскивания) и щелкните правой кнопкой мыши на наборе выбранных элементов. В меню Combine (Комбинировать) доступен набор пунктов, которые позволяют генерировать объекты Path на основе набора операций выбора и операций комбинирования (рис. 29.15). По сути, опции меню Combine представляют собой базовые операции диаграммы Венна. Опробуйте некоторые из них и затем просмотрите сгенерированную XAML-разметку (как и в Visual Studio 2010, нажатие <Ctrl+Z> отменяет предыдущую операцию). Рис. 29.15. Комбинирование фигур для генерации новых путей Редакторы кистей и трансформаций Подобно Visual Studio 2010, в Blend поддерживается редактор кистей. После выбора элемента в визуальном конструкторе (такого как одна из фигур) можно перейти в область
1124 Часть VI. Построение настольных пользовательских приложений с помощью WPF Brushes (Кисти) окна Properties. В самом верху редактора кистей находятся все свойства выбранного объекта, которому может быть назначен тип Brush. Ниже доступны те же самые базовые средства редактирования, которые демонстрировались ранее в этой главе. На рис. 29.16 показана одна из возможных конфигураций кисти. Единственное значительное отличие между редакторами кистей Blend и Visual Studio 2010 состоит в том, что в Blend можно удалить градиентный переход, щелкая на соответствующем ползунке и перетаскивая его за пределы ленты градиента (буквально в любое место за пределы ленты градиента; щелчок правой кнопкой здесь не поддерживается). Другое замечательное средство редактора кистей Blend состоит в том, что после выбора элемента, который использует градиентную кисть, можно нажать клавишу <G> или щелкнуть на значке Gradient Tool (Инструмент градиента) в панели инструментов и затем изменить начало градиента (рис. 29.17). Наконец, в Blend также поддерживается интегрированный редактор трансформаций. Выберите фигуру в визуальном конструкторе (разумеется, редактор трансформаций также работает с любым элементом, выбранным в окне Objects and Timeline (Объекты и шкала времени)). Теперь в окне Properties найдите панель Transform (Трансформация), как показано на рис. 29.18. Экспериментируя с редактором трансформаций, уделите некоторое время изучению результирующей XAML-разметки. Кроме того, обратите внимание, что многие трансформации могут быть применены непосредственно в визуальном конструкторе — наведением курсора мыши на узел изменения размеров выбранного объекта. Например, поместив курсор мыши рядом с узлом на боковой стороне выбранного элемента, можно применить скашивание. (Однако проще и предпочтительней для его использовать редактор трансформаций.) На заметку! Вспомните, что членами пространства имен System.Windows.Shapes являются UIElement и наследники, имеющие множество событий, на которые можно реагировать. Это означает возможность обработки событий в рисунках с использованием Blend, как это делается для типичного элемента управления (см. главу 29). Рис. 29.16. Редактор кистей Blend Рис. 29.17. Изменение начала градиента Рис. 29.18. Редактор трансформаций Blend
Глава 29. Службы визуализации графики WPF 1125 На этом завершается обзор служб графической визуализации WPF и ознакомительный экскурс в использование Blend для генерации простых графических данных. Теперь давайте рассмотрим второй способ визуализации графических данных — применение рисунков и геометрий. Визуализация графических данных с использованием рисунков и геометрий Хотя типы Shape позволяют генерировать любые интерактивные двумерные поверхности, из-за богатой цепочки наследования они потребляют довольно много памяти. И хотя класс Path может помочь снизить накладные расходы за счет использования включенной геометрии (вместо огромной коллекции других фигур), в WPF предлагается развитый API-интерфейс рисования и геометрии, который позволяет визуализировать даже более легковесные двухмерные векторные изображения. Входной точкой в этот API-интерфейс является абстрактный класс System.Windows. Media.Drawing (из сборки PresentationCore.dll), который сам по себе всего лишь определяет ограничивающий прямоугольник для хранения визуализаций. Обратите внимание, что на рис. 29.19 цепочка наследования класса Drawing существенно проще, чем у Shape, учитывая, что в ней отсутствуют как UIElement, так и FrameworkElement. л CJ BaseTyp« л -fy Animatable л -*f$ Freezeble л **4 DependencyObject л *\% DispatcherObject % Object ^ lAnimatable V CloneO ч» CloneCurrentValueO 23* Bounds public abstract class Drawing : System.Windows.Media.Animation,Animatable Member of Syytem.Windows.Media Summary: Abstract class that describes a 2-D drawing. This class cannot be inherited by your code. Рис. 29.19. Класс Drawing более легковесный, чем Shape WPF предоставляет различные классы, которые расширят Drawing; каждый из них предлагает определенный способ рисования содержимого, как описано в табл. 29.6. Таблица 29.6. Классы, унаследованные от Drawing Класс Назначение DrawingGrdoup GeometryDrawing GlyphRunDrawing ImageDrawing VideoDrawing Используется для комбинирования коллекции отдельных объектов-наследников Drawing в единую составную визуализацию Используется для визуализации двухмерных фигур в очень легковесной манере Используется для визуализации текстовых данных с применением служб графической визуализации WPF Используется для визуализации файла изображения, или набора геометрий, внутри ограничивающего прямоугольника Служит для воспроизведения аудио- или видео-файла. Этот тип может полностью использоваться только в процедурном коде. Для воспроизведения видео в XAML-разметке лучше подойдет класс MediaPlayer
1126 Часть VI. Построение настольных пользовательских приложений с помощью WPF Будучи более легковесными, типы-наследники Drawing не обладают встроенной возможностью обработки событий, поскольку они не являются UIElement или FrameworkElement (хотя допускают программную реализацию логики проверки попадания). Тем не менее, они поддаются анимации (средства анимации WPF рассматриваются в главе 30). Другое ключевое отличие между типами-наследниками Drawing и Shape состоит в том, что типы-наследники Drawing не умеют визуализировать себя, поскольку не наследуются от UIElement! Вместо этого производные типы должны помещаться в какой-то хост-объект (а именно — Drawinglmage, DrawingBrush или DrawingVisual) для отображения содержимого. Объект Drawinglmage позволяет помещать рисунки и геометрии внутрь элемента управления Image из WPF, который обычно используется для отображения данных из внешнего файла. DrawingBrush позволяет строить кисть на основе рисунков и их гео- . метрий, чтобы установить свойство, требующее кисти. Наконец, DrawingVisual применяется только на визуальном уровне графической визуализации, полностью управляемом из кода С#. Хотя использовать рисунки немного сложнее, чем простые фигуры, отделение графической композиции от графической визуализации делает типы-наследники Drawing намного легче типов-наследников Shape, сохраняя их ключевые службы. Построение кисти DrawingBrush с использованием объектов Geometry Ранее в этой главе элемент Path заполнялся группой геометрий следующим образом: <Path Fill = "Orange" Stroke = "Blue" StrokeThickness = "> <Path.Data> <GeometryGroup> <EllipseGeometry Center = 5,70" RadiusX = 0" RadiusY = 0" /> <RectangleGeometry Rect = 5,55 100 30" /> <LineGeometry StartPoint=,0" EndPoint=0,30" /> <LineGeometry StartPoint=0,30" EndPoint=,30" /> </GeometryGroup> </Path.Data> </Path> В этом случае получается интерактивность Path при чрезвычайной легковесности, присущей геометриям. Однако если необходимо визуализировать аналогичный вывод, но никакая (готовая) интерактивность не нужна, можете поместить тот же элемент <GeometryGroup> внутрь DrawingBrush, как показано ниже: <DrawingBrush> <DrawingBrush.Drawing> <GeometryDrawing> <GeometryDrawing.Geometry> <GeometryGroup> <EllipseGeometry Center = 5,70" RadiusX = 0" RadiusY = 0" /> <RectangleGeometry Rect = 5,55 100 30" /> <LineGeometry StartPoint=,0" EndPoint=0,30" /> <LineGeometry StartPoint=0,30" EndPoint=,30" /> </GeometryGroup> </GeometryDrawing.Geometry> <!-- Специальное перо для рисования границ --> <GeometryDrawing.Pen>
Глава 29. Службы визуализации графики WPF 1127 <Pen Brush="Blue" Thickness="/> </GeometryDrawing.Pen> <!-- Специальная кисть для заполнения внутренней области --> <GeometryDrawing.Brush> <SolidColorBrush Color="Orange"/> </GeometryDrawing.Brush> </GeometryDrawing> </DrawingBrush.Drauing> </DrawingBrush> При помещении группы геометрий в Draw in gB rush также нужно установить объект Реп, используемый для рисования границ, поскольку более не наследуется свойство Stroke от базового класса Shape. Здесь был создан элемент <Реп> с теми же настройками, которые использовались в значениях Stroke и StrokeThickness из предыдущего примера Path. Более того, поскольку свойство Fill класса Shape больше не наследуется, нужно также применять синтаксис "элемент-свойство" для определения объекта кисти, предназначенного элементу <T>rawingGeometry^>; в данном случае это кисть сплошного оранжевого цвета, как в предыдущих установках Path. Рисование с помощью DrawingBrush Теперь кисть DrawingBiush можно использовать для установки значения любого свойства, требующего объекта кисти. Например, подготовив следующую разметку в Kaxaml, с помощью синтаксиса "элемент-свойство" можно рисовать с применением изображения по всей поверхности Page: <Page xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Page.Background> <!-- Та же кисть DrawingBrush, что и раньше --> <DrawingBrush> </DrawingBrush> </Page.Background> </Page> Или же кисть <DrawingBrush> можно использовать для установки другого совместимого с кистью свойства, такого как свойство Background элемента Button: <Page xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Button Height=00" Width=00"> <Button.Background> <!-- Та же кисть DrawingBrush, что и раньше --> <DrawingBrush> </DrawingBrush> </Button.Background> </Button> </Page> Независимо от того, какое совместимое с кистью свойство будет установлено в специальный объект <DrawingBrush>, визуализировать двухмерное графическое изображение получится с намного меньшими накладными расходами, чем в случае фигур.
1128 Часть VI. Построение настольных пользовательских приложений с помощью WPF Включение типов Drawing в Drawinglmage Тип Drawinglmage позволяет включать рисованную геометрию в элемент управления <Image> из WPF. Рассмотрим следующую разметку: <Раде xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Image Height=00" Width=00"> <Image.Source> <DrawingImage> <DrawingImage. Drawing> <GeometryDrawing> <GeometryDrawing. Geometry> <GeometryGroup> <EllipseGeometry Center = 5,70" RadiusX = 0" RadiusY = 0" /> <RectangleGeometry Rect = 5,55 100 30" /> <LineGeometry StartPoint=,0" EndPoint=0,30" /> <LineGeometry StartPoint=0,30" EndPoint=,30" /> </GeometryGroup> </GeometryDrawing.Geometry> <!-- Специальное перо для рисования границ --> <GeometryDrawing.Pen> <Pen Brush="Blue" Thickness="/> </GeometryDrawing.Pen> <•-- Специальная кисть для заполнения внутренней области --> <GeometrуDrawing.Brushy <SolidColorBrush Color="Orange"/> </GeometryDrawing.Brush> </GeometryDrawing> </DrawingImage.Drawing> </DrawingImage> </Image.Source> </Image> <-/Page> В этом случае элемент <GeometryDrawing> помещен в <DrawingImage>, а не в <DrawingBrush>. Используя этот <DrawingImage>, можно установить свойство Source элемента управления Image. Генерация сложной векторной графики с использованием Expression Design Честно говоря, ручное написание кода XAML, представляющего сложный набор геометрий, сегментов, фигур и путей, может оказаться буквально кошмаром, и никто так не поступает. Как уже было показано, Expression Blend содержит несколько средств IDE- среды, которые позволяют работать с базовой графикой; однако, когда нужно генерировать профессиональную, полноценную векторную графику, то инструменту Expression Design нет равных. На заметку! Бесплатная пробная версия Expression Design доступна для загрузки'на официальном веб-сайте Microsoft по адресу http://www.microsoft.com/expression. Инструмент Expression Design обладает средствами, подобными тем, что можно найти в профессиональных инструментах для графического дизайна, таких как Adobe
Глава 29. Службы визуализации графики WPF 1129 Photoshop или Adobe Illustrator. В руках талантливого художника Expression Design позволит создавать очень сложные двухмерные и трехмерные изображения, которые могут быть экспортированы в самые разнообразные файловые форматы, включая XAML. Способность экспортировать графическое изображение в XAML очень полезна, поскольку это позволяет включать разметку в приложения, назначать объектам имена и манипулировать ими в коде. Например, художник может с помощью Expression Design нарисовать изображение трехмерного куба. После его экспорта в XAML программист может добавить атрибут x:Name к элементам, с которыми нужно взаимодействовать, и получить доступ к сгенерированному объекту из кода С#. Экспорт документа Expression Design в XAML Полностью все возможности инструмента Expression Design здесь описываться не будут. Вместо этого будут производиться манипуляции некоторыми стандартными примерами документов, поставляемыми с этим продуктом. Запустите Expression Design и выберите пункт меню Help1^Samples (Справкам Примеры). В открывшемся диалоговом окне дважды щелкните на одном из примеров рисунков. Для данного примера это будет файл bearpaper. design, входящий в состав Expression Design 3.0. Выберите пункт меню File^Export (Файл^Экспорт). Откроется диалоговое окно Export (Экспорт), которое позволяет сохранить данные выбранного изображения в виде одного из популярных графических форматов, включая .png, .jpeg, .gif, .tif, .bmp и .wdp (HD Photo). Если Expression Design используется просто для создания профессиональных статических изображений, эти форматы файлов вполне подойдут Однако если планируется добавить интерактивность к векторной графике, необходимо сохранить файл с описанием XAML. Выберите вариант XAML WPF Canvas (Холст XAML WPF) в раскрывающемся списке Format (Формат). На рис. 29.20 обратите внимание на другие опции, включающие возможность назначения каждому элементу имени по умолчанию (через атрибут x:Name) и сохранения текстовых данных в виде объекта Path. Оставьте установки по умолчанию и щелкните на кнопке Export All (Экспортировать все). Рис. 29.20. Файлы Expression Design можно экспортировать как XAML-раз метку
1130 Часть VI. Построение настольных пользовательских приложений с помощью WPF Сгенерированную XAML-разметку объекта Canvas теперь можно скопировать в Visual Studio 2010, Expression Blend либо в область <Page> или <Window> внутри редактора Kaxaml (или в специальном редакторе XAML, который был создан в главе 27). В сгенерированной XAML-разметке представлены объекты Path в формате по умолчанию. Здесь можно создать обработчики событий в частях изображения, которым необходимо манипулировать, написав соответствующий код С#. Например, можно импортировать данные картинки с изображением плюшевого мишки в новое WPF-приложение и добавить обработчик события MouseDown для одного из объектов-глаз. В обработчике события попробуйте применить объект трансформации или измените цвет. На заметку! Инструмент Expression Design больше в этой книге применяться не будет. Дополнительную информацию по работе в нем можно получить из его встроенной справочной системы, которая вызывается нажатием <F1>. Визуализация графических данных с использованием визуального уровня Последний вариант визуализации графических данных с помощью WPF называется визуальным уровнем (visual layer). Как уже упоминалось, доступ к этому уровню возможен только из кода (он не поддерживает XAML). Хотя для подавляющего большинства WPF-приложений вполне достаточно фигур, рисунков и геометрий, визуальный уровень обеспечивает самый быстрый способ визуализации крупного объема графических данных. Как ни странно, он также может быть полезен, когда необходимо визуализировать единственное изображение на очень большой площади. Например, если задача состоит в заполнении фона окна простым статическим изображением, то визуальный уровень оказывается самым быстрым способом сделать это. Кроме того, он удобен, когда требуется очень быстро менять фон окна в зависимости от ввода пользователя или еще чего-нибудь. Давайте построим небольшой пример программы, иллюстрирующей основы использования визуального уровня. Базовый класс Visual и производные дочерние классы Абстрактный класс System.Windows.Media.Visual предлагает минимальный набор служб (визуализацию, проверку попадания, трансформации) для визуализации графики, но не предусматривает поддержки дополнительных невизуальных служб, что может привести к разбуханию кода (события ввода, службы компоновки, стили и привязка данных). Обратите внимание на простую цепочку наследования для типа Visual, показанную на рис. 29.21. I Object Browser Browse- My Solution I <Search> "if VideoDrawing d :jj| Base Types d -t$ DependencyObject d 1$ DispatcherObject ^Object 1> 'Щ VtsualBrush r> % VisualCollection V AddVisualChild(Sy5tem.Windows.Media.Visua!) ♦ FindCommonVisualAncfcstcr^Syitem.V.'ii-.-iows.DeperdencyObject) V GetyisualChitd(int) public abstract class Visual: System. Winduws.D<>pandcrKyObj»Ct Member of Syrt»m,Wir>drws.M«dia I Summary: Provides rendering support in WPF, which includes hrt testing, coordinate transformation and bounding box calculations. Рис. 29.21. Класс Visual предоставляет базовую проверку попадания, трансформации координат и вычисления ограничивающих прямоугольников
Глава 29. Службы визуализации графики WPF 1131 Учитывая, что Visual — это абстрактный базовый класс, для выполнения действительных операций визуализации должен использоваться один из его производных классов. В WPF определено несколько подклассов Visual, включая DrawingVisual, Viewport3DVisual и ContainerVisual. В рассматриваемом ниже примере внимание сосредоточено на DrawingVisual — легковесном классе рисования, который используется для визуализации фигур, изображений или текста. Первый взгляд на класс DrawingVisual Чтобы визуализировать данные на поверхности с помощью класса DrawingVisual, потребуется выполнить следующие базовые шаги: 1. Получить объект DrawingContext от DrawingVisual. 2. Использовать DrawingContext для визуализации графических данных. Эти два шага представляют абсолютный минимум того, что нужно предпринять для визуализации некоторых данных на поверхности. Однако если необходимо, чтобы визуализируемые графические данные сами отвечали за определение проверки попадания (что может быть важно для взаимодействия с пользователем), тогда понадобится выполнить следующие дополнительные шаги: 1. Обновить логическое и визуальное деревья, поддерживаемые контейнером, на поверхности которого производится рисувание. 2. Переопределить два виртуальных метода из класса FrameworkElement, позволив контейнеру получать созданные визуальные данные. Давайте немного задержимся на последних двух шагах. Чтобы посмотреть, как использовать класс DrawingVisual для визуализации двухмерных данных, создайте в Visual Studio новое приложение WPF по имени RenderingWithVisuals. Наша первая цель — применение DrawingVisual для динамического присваивания данных элементу управления Image из WPF. Начните со следующего обновления XAML-разметки окна: <Window x:Class="RenderingWithVisuals.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/present at ion" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title=" Fun with the Visual Layer" Height=50" Width=25" Loaded="Window _ Loaded" WindowStartupLocation="CenterScreen"> <StackPanel Background="AliceBlue" Name="myStackPanel"> <Image Name="mylmage" Height="80"/> </StackPanel> </Window> Обратите внимание, что элемент управления <Image> пока не имеет значения для Source, поскольку оно будет присвоено во время выполнения. Кроме того, предусмотрен обработчик события Loaded окна, который выполняет работу по построению графических данных в памяти, используя объект Draw in gB rush. Ниде показана реализация обработчика события Loaded: private void Window_Loaded(object sender, RoutedEventArgs e) { const int TextFontSize = 30; // Создать объект System.Windows.Media.FormattedText. FormattedText text = new FormattedText("Hello Visual Layer1", new System.Globalization.Culturelnfo("en-us"), FlowDirection.LeftToRight, new Typeface(this.FontFamily, FontStyles.Italic, FontWeights.DemiBold, FontStretches.UltraExpanded),
1132 Часть VI. Построение настольных пользовательских приложений с помощью WPF TextFontSize, Brushes.Green); // Создать DrawingVisual и получить DrawingContext. DrawingVisual drawingVisual = new DrawingVisual() ; using(DrawingContext drawingContext = drawingVisual.RenderOpen() ) { // Вызвать тобой из методов DrawingContext для визуализации данных. drawingContext.DrawRoundedRectangle(Brushes.Yellow, new Pen(Brushes.Black, 5), newRectE, 5, 450, 100), 20, 20); drawingContext.DrawText(text, new PointB0, 20)); } // Динамически создать битовую карту, используя данные из DrawingVisual. RenderTargetBitmap bmp = new RenderTargetBitmapE00, 100, 100, 90, PixelFormats.Pbgra32); bmp.Render(drawingVisual) ; // Установить источник для элемента управления Image. mylmage.Source = bmp; } В этом коде представлен ряд новых классов WPF, которые кратко описываются ниже (более подробные сведения по ним можно получить в документации .NET Framework 4.0 SDK). Метод начинается с создания нового объекта FormattedText, представляющего текстовую часть изображения в памяти, которое мы конструируем. Как видите, конструктор позволяет указывать многочисленные атрибуты, вроде размера шрифта, семейства шрифтов, цвета переднего плана и самого текста. Затем получается необходимый объект DrawingContext через вызов RenderOpen() на экземпляре DrawingVisual. Здесь в DrawingVisual визуализирован цветной, со скругленными углами прямоугольник, за которым следует форматированный текст. В обоих случаях графические данные помещаются в DrawingVisual с использованием жестко закодированных значений, что не слишком хорошая идея для реального приложения, но вполне сойдет для простого теста. На заметку! Ознакомьтесь с описанием класса DrawingContext в документации .NET Framework 4.0 SDK, чтобы просмотреть все члены, связанные с визуализацией. Если вы работали в прошлом с объектом Graphics из Windows Forms, то DrawingContext должен выглядеть очень похожим. Последние несколько операторов отображают DrawingVisual на объект RenderTargetBitmap, который является членом пространства имен System.Windows. Media.Imaging. Этот класс принимает визуальный объект и трансформирует его в находящееся в памяти битовое изображение. После этого устанавливается свойство Source элемента управления Image и получается вывод, показанный на рис. 29.22. f Fun with the Visual Layer ! r Hello Visual Layer! 1:«=нвЁиайГ Рис. 29.22. Использование визуального уровня для отображения находящейся в памяти битовой карты
Глава 29. Службы визуализации графики WPF 1133 На заметку! Пространство имен System.Windows.Media.Imaging содержит множество дополнительных классов кодирования, которые позволяют сохранять находящийся в памяти объект RenderTargetBitmap в физический файл в различных форматах Дополнительную информацию ищите в JpegBitmapEncoder и связанных с ним классах. Визуализация графических данных в специальном диспетчере компоновки Хотя использование DrawingVisual для рисования в фоне элемента управления WPF представляет интерес, наверное, чаще придется строить специальный диспетчер компоновки (Grid, StackPanel, Canvas и т.п.), внутри которого для визуализации содержимого применяется визуальный уровень. После создания такой специальный диспетчер компоновки можете включать в нормальный элемент Window (или Page, или UserControl). В таком случае часть пользовательского интерфейса будет обрабатываться высоко оптимизированным агентом визуализации, а для визуализации некритичных аспектов включающего элемента Window будут использоваться фигуры и рисунки. Если дополнительная функциональность, предлагаемая выделенным диспетчером компоновки, не требуется, можно просто расширить FrameworkElement, который уже имеет необходимую инфраструктуру, позволяющую хранить внутри также и визуальные элементы. Давайте рассмотрим пример. Вставьте в проект новый класс по имени CustomVisualFrameworkElement. Унаследуйте его от FrameworkElement и импортируйте пространства имен System.Windows, System.Windows.Input и System.Windows. Media. Этот класс будет поддерживать переменную-член типа VisualCollection, содержащую два фиксированных объекта DrawingVisual (конечно, можно было бы добавить члены в эту коллекцию, но простоты это не делается). Модифицируйте класс CustomVisualFrameworkElement следующим образом: class CustomVisualFrameworkElement : FrameworkElement { // Коллекция всех визуальных объектов. VisualCollection theVisuals; public CustomVisualFrameworkElement () { // Заполнить коллекцию VisualCollection несколькими объектами DrawingVisual. // Аргумент конструктора представляет владельца визуальных объектов. theVisuals = new VisualCollection (this); theVisuals.Add(AddRect()); theVisuals.Add(AddCircle ()); } private Visual AddCircle () { DrawingVisual drawingVisual = new DrawingVisual (); // Получить DrawingContext для создания нового содержимого. using (DrawingContext drawingContext = drawingVisual.RenderOpen ()) { // Создать круг и нарисовать его в DrawingContext. Rect rect = new Rect (new PointA60, 100), new SizeC20, 80)); drawingContext.DrawEllipse(Brushes.DarkBlue, null, newPointG0, 90), 40, 50); } return drawingVisual; } private Visual AddRect () { DrawingVisual drawingVisual = new DrawingVisual () ;
1134 Часть VI. Построение настольных пользовательских приложений с помощью WPF using (DrawingContext drawingContext = drawingVisual.RenderOpen()) { Rect rect = new Rect(new Point A60, 100), new Size C20, 80)); drawingContext.DrawRectangle(Brushes.Tomato, null, rect); } return drawingVisual; Перед тем как можно будет использовать специальный элемент FrameworkElement в Window, потребуется переопределить два виртуальных метода, упомянутых ранее, которые оба вызываются внутренне WPF во время процесса визуализации. Метод GetVisualChild() возвращает дочерний элемент по указанному индексу из коллекции дочерних элементов. Свойство VisualChildrenCount возвращает количество визуальных дочерних элементов внутри этой визуальной коллекции. Оба метода легко реализовать, поскольку реальную работу можно делегировать переменной-члену VisualCollection: protected override int VisualChildrenCount { get { return theVisuals.Count; } } protected override Visual GetVisualChild(int index) { // Значение должно быть больше нуля, поэтому на всякий случай проверить. if (index < 0 | | index >= theVisuals. Count) { throw new ArgumentOutOfRangeException(); } return theVisuals[index]; } Теперь есть достаточно функциональности, чтобы протестировать специальный класс. Обновите описание XAML элемента Window, добавив один объект CustomVisualFrameworkElement в существующий контейнер StackPanel. Это потребует создания специального пространства имен XML, которое отображается на пространство имен .NET (см. главу 28). <Window x:Class="RenderingWithVisuals.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:custom="clr-namespace:RenderingWithVisuals" Title="Fun with the Visual Layer" Height=50" Width=25" Loaded="Window_Loaded" WindowstartupLocation="CenterScreen"> <StackPanel Background="AliceBlue" Name="myStackPanel"> <Image Name="mylmage" Height="80"/> <custom:CustomVisualFrameworkElement/> </StackPanel> </Window> Результат выполнения программы показан на рис. 29.23. Реагирование на операции проверки попадания Поскольку DrawingVisual не поддерживает инфраструктуру UIElement или FrameworkElement, необходимо программно добавить возможность вычисления операций проверки попадания. К счастью, на визуальном уровне сделать это очень просто, благодаря концепции логического и визуального деревьев.
■ Fun with the Visual Layer Глава 29. Службы визуализации графики WPF 1135 Hello Visual Layer! • Рис. 29.23. Использование визуального уровня для визуализации данных в специальном элементе FrameworkElement Оказывается, что в результате написания блока XAML, по сути, строится логическое дерево элементов. Однако за каждым логическим деревом стоит намного более развитое описание, известное как визуальное дерево, которое содержит низкоуровневые инструкции визуализации. Упомянутые деревья подробно рассматриваются в главе 32, а сейчас достаточно знать, что пока специальные визуальные объекты не будут зарегистрированы в этих структурах данных, операции проверки попадания выполнять невозможно. К счастью, контейнер VisualCollection делает это автоматически (что объясняет, почему необходимо передавать ссылку на специальный элемент FrameworkElement в качестве аргумента конструктора). Обновите класс CustomVisualFrameworkElement для обработки события MouseDown в конструкторе класса, используя стандартный синтаксис С#: this.MouseDown += MyVisualHost_MouseDown; Реализация этого обработчика вызовет метод VisualTreeHelper.HitTest(), чтобы проверить нахождение курсора мыши в границах одного из отображенных визуальных объектов. Для этого в качестве параметра метода HitTestO указывается делегат HitTestResultCallback, который выполнит вычисления. Добавьте в класс CustomVisualFrameworkElement следующие методы: void MyVisualHost_MouseDown(object sender, MouseButtonEventArgs e) { // Найти место, где пользователь выполнил щелчок. Point pt = e.GetPosition((UIElement)sender); // Вызвать вспомогательную функцию через делегат. VisualTreeHelper.HitTest(this, null, new HitTestResultCallback(myCallback), new PointHitTestParameters (pt) ) ; } public HitTestResultBehavior myCallback(HitTestResult result) { // Если был совершен щелчок на визуальном объекте, // переключиться между скошенной и нормальной визуализацией. if (result.VisualHit.GetType () == typeof(DrawingVisual)) { if (((DrawingVisual)result.VisualHit).Transform == null) ((DrawingVisual)result.VisualHit).Transform = new SkewTransformG, 7),
1136 Часть VI. Построение настольных пользовательских приложений с помощью WPF else { ((DrawingVisual)result.VisualHit).Transform = null; } } // Остановить углубление в визуальное дерево методом HitTest(). return HitTestResultBehavior.Stop; } Запустите программу. Теперь должна появиться возможность выполнить щелчок на любом из отображенных визуальных объектов и увидеть трансформацию в действии. Хотя это очень простой пример работы с визуальным уровнем WPF, помните, что можно использовать те же самые кисти, трансформации, перья и диспетчеры компоновки, что и при работе с XAML. Это значит, что вы уже знаете достаточно много о работе классов- наследников Visual. Исходный код. Проект RenderingWithVisuals доступен в подкаталоге Chapter 29. На этом исследование служб графической визуализации Windows Presentation Foundation завершено. В действительности мы лишь слегка коснулись обширной области графических средств WPF. Дальнейшее изучение фигур, рисунков, кистей, трансформаций и визуальных объектов продолжайте самостоятельно (некоторые дополнительные детали на эту тему будут приводиться в оставшихся главах, посвященных WPF). Резюме Поскольку Windows Presentation Foundation является чрезвычайно насыщенным графикой API-интерфейсом для построения пользовательских интерфейсов, не удивительно, что существует несколько способов визуализации графического вывода. В начале главы были рассмотрены все три способа визуализации (фигуры, рисунки и визуальные объекты) вместе с разнообразными примитивами визуализации, такими как кисти, перья и трансформации. Вспомните, что когда нужно построить интерактивную двухмерную визуализацию, фигуры делают этот процесс очень простым. Статические, не интерактивные изображения могут визуализироваться более оптимально с использованием рисунков и геометрии. Визуальный уровень (доступный только из кода) обеспечивает максимальную степень контроля и высокую производительность.
ГЛАВА 30 Ресурсы, анимация и стили WPF В этой главе будут представлены три важных и взаимосвязанных темы, которые позволят углубить понимание API-интерфейса Windows Presentation Foundation (WPF). Первая тема — роль логических ресурсов. Как вы увидите, система логических ресурсов (также называемых объектными ресурсами) — это способ ссылаться на часто используемые объекты внутри WPF-приложения. Хотя логические ресурсы часто пишутся на XAML, они могут быть определены и в процедурном коде. Затем будет показано, как определять, выполнять и управлять последовательностью анимации. Вопреки тому, что можно было подумать, применение анимации WPF не ограничено видеоиграми или мультимедиа-приложениями. В API-интерфейсе WPF анимация может использоваться, например, для подсветки кнопки, когда она получает фокус, или увеличения размера выбранной строки в DataGrid. Освоение анимации — ключевой аспект построения специальных шаблонов элементов управления (о чем пойдет речь в главе 31). Завершается глава рассмотрением роли стилей WPF. Подобно веб-странице, которая использует стили CSS или механизм тем ASP.NET, приложение WPF может определять общий вид и поведение набора элементов управления. Эти стили определяются в разметке и сохраняются в качестве объектных ресурсов для последующего использования, а затем динамически применяются во время выполнения. Система ресурсов WPF Нашей первой целью является исследование способов встраивания и доступа к ресурсам приложения. В WPF поддерживаются два вида ресурсов. Первый — это двоичный ресурс, который обычно включает элементы, обычно рассматриваемые большинством программистов как ресурс в его традиционном смысле (встроенные файлы изображений или звуковых клипов, значки, используемые приложением, и т.д.). Второй вид, называемый объектным или логическим ресурсом, представляет именованный объект .NET, который может быть упакован и многократно использован по всему приложению. Хотя любой объект .NET может быть упакован в виде объектного ресурса, логические ресурсы особенно полезны при работе с графическими данными любого рода, учитывая, что можно определять часто используемые графические примитивы (кисти, перья, анимации и т.п.) и ссылаться на них по мере необходимости.
1138 Часть VI. Построение настольных пользовательских приложений с помощью WPF Работа с двоичными ресурсами Прежде чем приступить к теме объектных ресурсов, давайте быстро посмотрим, как упаковывать такие двоичные ресурсы, как значки и файлы изображений (например, логотипы компаний или изображения для анимации), в приложения. Создайте в Visual Studio 2010 новое WPF-приложение по имени BinaryResourcesApp. Обновите разметку начального окна для использования DockPanel в качестве корня компоновки: <Window х:Class="BinaryResourcesApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Fun with Binary Resources" Height=00" Width=,,649"> <DockPanel LastChildFill=,,True"> </DockPanel> </Window> Теперь предположим, что приложение должно отображать один из трех файлов изображений внутри части окна, в зависимости от пользовательского ввода. Элемент управления Image из WPF может применяться не только для отображения типичных файлов изображений (*.bmp, *.gif, *.ico, *.jpg, *.png, *.wdp и *.tiff), но также данных из Drawinglmage (как было показано в главе 29). Постройте пользовательский интерфейс окна на основе диспетчера компоновки DockPanel с простой панелью инструментов, включающей кнопки Next (Вперед) и Previous (Назад). Ниже панели инструментов можно расположить элемент управления Image, который пока не имеет значения, установленного в свойстве Source, так как это будет делаться в коде: <Window х:Class="BinaryResourcesApp.MainWindow" xmlns = "http : //schemas .microsoft. com/wmfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Fun with Binary Resources" Height=00" Width=49"> <DockPanel LastChildFill="True"> <ToolBar Height=0" Name="picturePickerToolbar11 DockPanel. Dock="Top"> <Button x:Name="btnPreviousImage11 Height=0" Width=0011 BorderBrush="Black11 Margin=11 Content="Previous" Click=llbtnPreviousImage_Click"/> <Button x:Name="btnNextImage" Height=M40M Width=00" BorderBrush=MBlackM Margin=M5M Content=MNextM Click="btnNextImage_Click"/> </ToolBar> <!-- Этот элемент Image будет заполняться в коде --> <Border BorderThickness=" BorderBrush="Green"> <Image x:Name="imageHolder" Stretch="Fill" /> </Border> </DockPanel> </Window> Обратите внимание, что событие Click обрабатывается для каждого объекта Button. Предполагая, что для обработки этих событий использовалась IDE-среда, в файле кода С# будут определены два пустых метода. Каким образом реализовать обработчики событий Click для выполнения цикла по графическим данным? И что более важно: графические данные будут храниться на пользовательском жестком диске или же будут встроены в скомпилированную сборку? Рассмотрим оба варианта. Включение в проект файлов свободных ресурсов Предположим, что изображения должны поставляться в виде набора файлов внутри подкаталога пути установки приложения. В окне Solution Explorer среды Visual Studio щелкните правой кнопкой мыши на узле проекта и выберите в контекстном меню пункт Add^New Folder (Добавить1^Новая папка) для создания подкаталога под названием Images.
Глава 30. Ресурсы, анимация и стили WPF 1139 Solution Explorer » □ X I ]J Solution 'BinaryResourcesApp A project) л »2Э BinaryResourcesApp j|| Properties ai References л *>\J} Images jy Deer.jpg ■i Dogs.jpg \-M Welcome.jpg i> gwj App.xaml *'; MarnWindowjcaml 1 Q?5 Solution Explorer К Properties :. )i л Build Action Copy to Output Directory Custom Tool Custom Tool Namespace Advanced Content Copy always Рис. 30.1. Новый подкаталог в проекте WPF содержит данные изображений Рис. 30.2. Конфигурирование данных изображений для копирования в выходной каталог Solution Explorer : Solution 'BinaryResourcesApp' A project) л .jl BinaryResourcesApp Ш1 Properties ''Л References s ; _;> bin л _. Debug л ., Images 3 Deer.jpg Теперь щелкните правой кнопкой мыши на папке Images и выберите в контекстном меню пункт Add1^Existing Item (Добавить1^Существующий элемент), чтобы скопировать в нее файлы изображений. В исходном коде для этого проекта доступны три файла изображений с именами Welcome.jpg, Dogs.jpg и Deep.jpg, которые можно включить в проект, или же можно просто добавить три файла изображений по своему выбору. На рис. 30.1 показан текущий вид окна Solution Explorer. Конфигурирование свободных ресурсов Если необходимо, чтобы IDE-среда Visual Studio 2010 копировала содержимое проекта в выходной каталог, потребуется скорректировать несколько настроек, используя окно Properties (Свойства). Чтобы обеспечить копирование содержимого папки Images в папку \bm\Debug, начните с выбора всех изображений в Solution Explorer. Когда все изображения выбраны, в окне Properties установите свойство Build Action (Действие сборки) в Content, а свойство Copy Output Directory (Копировать в выходной каталог) в Copy always (рис. 30.2). Перекомпилировав программу, щелкните на кнопке Show all Files (Показать все файлы) в Solution Explorer и просмотрите скопированную папку Images в каталоге \bin\Debug (может потребоваться также щелкнуть на кнопке обновления). Результат показан на рис. 30.3. Программная загрузка изображения В WPF доступен класс по имени Bitmaplmage, входящий в пространство имен System.Windows.Media.Imaging. Этот класс позволяет загружать данные из файла изображения, местоположение которого представлено объектом System.Uri. Обработав событие Loaded окна, можно заполнить список List<T> элементов Bitmaplmages следующим образом: public partial class MainWindou : Window ] Welcome.jpg j BinaryResourcesApp.exe ij BinaryResourcesApp.pdb J BinaryResourcesApp.vshost.exe f> LU Release bflr Images iJJ Deer.jpg £ Dogs.jpg pjpj Welcome.jpg Г~! obj »*j App.xaml jwj MainWindow.xaml -?p Solution Explorer I Рис. 30.3. Скопированные данные изображений // Список файлов Bitmaplmage.
1140 Часть VI. Построение настольных пользовательских приложений с помощью WPF List<BitmapImage> images = new List<BitmapImage>(); // Текущая позиция в списке. private int currImage = 0; private const int MAX_IMAGES = 2; private void Window_Loaded(object sender, RoutedEventArgs e) { try { string path = Environment.CurrentDirectory; // Загрузить эти изображения при загрузке окна. images.Add(new Bitmaplmage(new Uri(string.Format(@"{0}\Images\Deer.jpg11, path)))); images.Add (new Bitmaplmage(new Uri(string.Format(@M{0}\Images\Dogs.jpg", path)))); images. Add (new Bitmaplmage (new Uri (string. Format ((^"{OjMmagesNWelcome.jpg11, path)))); // Показать первое изображение в List<>. imageHolder.Source = images[currlmage]; } catch (Exception ex) { MessageBox.Show(ex.Message); } 1 } Обратите внимание, что в этом классе также определена переменная-член типа int (currlmage), которая позволяет обработчикам событий Click проходить циклом по каждому элементу в List<T> и отображать его в элементе Image, устанавливая свойство Source. (Здесь обработчик события Loaded устанавливает свойство Source в первое изображение из List<T>.) Вдобавок константа МАХ_IMAGES дает возможность проверить верхнюю и нижнюю границы по мере итерации по списку. Ниже показаны обработчики Click, которые делают это: private void btnPreviousImage_Click(object sender, RoutedEventArgs e) { if (--currlmage < 0) currlmage = MAX_IMAGES; imageHolder.Source = images[currlmage]; } private void btnNextImage_Click(object sender, RoutedEventArgs e) { if (++currlmage > MAX_IMAGES) currlmage = 0; imageHolder.Source = images[currlmage]; } Теперь можно запустить программу и переключаться между всеми изображениями. Встраивание ресурсов приложения Если вы предпочитаете встроить файлы изображений непосредственно в сборку .NET в виде двоичных ресурсов, выберите файлы изображений в Solution Explorer (в папке \Images, а не \bin\Debug\Images). Затем установите свойство Build Action в Resource, а свойство Copy to Output Directory — в Do not copy (рис. 30.4). В меню Build (Сборка) Visual Studio 2010 выберите пункт Clean Solution (Очистить решение) для очистки текущего содержимого \bin\Debug\Images и перестройте проект. Обновите окно Solution Explorer и обратите внимание, что папки \bin\Debug\Images в каталоге \bin\Debug больше нет.
Глава 30. Ресурсы, анимация и стили WPF 1141 Properties Welcome.jpg File Properties :К: 41 ! Copy to Output Directory Custom Tool Custom Tool Namespace I Resource Do not copy a Buid Action How the file relates to the build and deployment processes. Рис. 30.4. Конфигурирование изображений как встроенных ресурсов При текущих параметрах сборки графические данные больше не копируются в выходную папку, а встраиваются в саму сборку. Предполагая, что проект перекомпилирован, откройте сборку в утилите reflector.exe и удостоверьтесь в наличии встроенных данные изображений (рис. 30.5). ■jf Red Gate; NETR QOI Qg чЗ WindowsBase i+l -uJ Calc [+1 -CJ PresentationFramework 3 -CJ BinaryResourcesApp \*> l\ BinaryResourcesApp.exe Q ,_j Resources j4 BinaryResourcesApp.g.resources ,^3 BinajyResourcesApp.Properties.Resources.resources !. Name _J mainwmdow.baml Value 0c 00 00 00 4d 00 53 00 ... A324 bytes) // public resource BinaryResourcesApp.g.resources Size 3532687 bytes Рис. 30.5. Встроенные данные изображений Теперь необходимо модифицировать код для загрузки изображений, чтобы извлекать их уже из скомпилированной сборки: private void Window_Loaded(object sender, RoutedEventArgs e) { try { images.Add(new Bitmaplmage(new Uri(@"/Images/Deer.jpg", UriKind.Relative))); images.Add(new Bitmaplmage(new Uri(@"/Images/Dogs.jpg", UriKind.Relative))); images .Add (new Bitmaplmage (new Uri @11/ Images /Welcome . jpg", UriKind. Relative) ) ) ; imageHolder.Source = images[currlmage]; } catch (Exception ex) { MessageBox.Show(ex.Message); } Как видите, в этом случае больше не нужно определять путь установки, а можно просто просматривать ресурсы по имени, принимающем во внимание имя исходного подкаталога. Также обратите внимание, что при создании объектов Uri указывается
1142 Часть VI. Построение настольных пользовательских приложений с помощью WPF значение UriKind.Relative. В любом случае в нынешнем виде исполняемая программа представляет собой единую сущность, которая может быть запущена из любого местоположения на машине, поскольку все скомпилированные данные находятся внутри сборки. На рис. 30.6 показано готовое приложение. Исходный код. Проект BinaryResourcesApp доступен в подкаталоге Chapter 30. Работа с объектными (логическими) ресурсами При построении WPF-приложения очень часто приходится определять большой объем XAML-разметки для использования во множестве мест окна или, возможно, в нескольких окнах или проектах. Например, предположим, что с помощью Expression Blend построена идеальная кисть линейного градиента, описываемая 10 строками кода разметки. Теперь эту кисть необходимо использовать в качестве фонового цвета для каждого элемента Button в проекте, состоящем из 8 окон, т.е. всего получается 16 элементов Button. Худшее, что можно сделать в такой ситуации — это копировать и вставлять одну и ту же XAML-разметку для каждого элемента управления Button. Ясно, что это может стать кошмаром при сопровождении, поскольку придется вносить изменения во множестве мест всякий раз, когда нужно скорректировать внешний вид и поведение кисти. К счастью, объектные ресурсы позволяют определить фрагмент XAML-разметки, назначить ему имя и сохранить в подходящем словаре для последующего использования. Подобно бинарному ресурсу, объектные ресурсы часто компилируются в сборку, которая нуждается в них. Однако при этом не требуется оперировать свойством Build Action. Достаточно поместить XAML-разметку в правильное место, а компилятор позаботится об остальном. Манипуляции объектными ресурсами составляют значительную часть разработки WPF. Как будет показано, объектные ресурсы могут быть намного сложнее, чем специальная кисть. Можно определять анимацию на основе XAML, трехмерную визуализацию, специальный стиль элемента управления, шаблон данных и многое другое — и все это в одном многократно используемом ресурсе.
Глава 30. Ресурсы, анимация и стили WPF 1143 Роль свойства Resources Как упоминалось ранее, объектные ресурсы должны помещаться в подходящий объект словаря, чтобы их можно было использовать во всем приложении. Как известно, каждый наследник FrameworkElement поддерживает свойство Resources. Это свойство инкапсулирует объект ResourceDictionary, содержащий определенные объектные ресурсы. ResourceDictionary может хранить элементы любого типа, поскольку оперирует экземплярами System.Object, и допускает маштуляции из XAML или процедурного кода. В WPF все элементы управления, окна (Window), страницы (Page) (используемые при построении навигационных приложений или программ ХВАР) и UserControl расширяют FrameworkElement, поэтому почти все виджеты предоставляют доступ к ResourceDictionary. Более того, класс Application, хотя и не расширяет FrameworkElement, поддерживает свойство с идентичным именем Resources для тех же целей. Определение ресурсов уровня окна Чтобы приступить к исследованию роли объектных ресурсов, создайте приложение WPF по имени ObjectResourcesApp, используя для этого Visual Studio 2010, и замените начальный контейнер Grid горизонтально выровненным диспетчером компоновки StackPanel. В этом StackPanel определите два элемента управления Button: <Window x:Class="ObjectResourcesApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml" Title="Fun with Object Resources" Height=50" Width=25"> <StackPanel Orlentation="Horizontal"^ <Euttcn Ilargin=5" Height=00" Width=00" Content="OK" FontSize=0"/ > <Button Margin=5" Height=00" Width=00" Content="Cancel" FontHize=0"/> </StackPanel^ </Windou^ Теперь выберите кнопку OK и установите свойство цвета Background в специальный тип кисти, используя интегрированный редактор кистей (см. главу 29). После этого обратите внимание, что кисть встраивается в контекст дескрипторов ^Button"* и </Button>: <Eutton Margin=5" Height=00" Width=00" Content="OK" FontSize=0"- <Button.BacLground> <PadialGradientBrush> <GradientStop Color="#FFC44EC4" Offset=" /> ^Graiient£tzp Color="#FF829CEB" Offset="l" /> <GradientStop Color="#FF793879" Offset=.669" /> </RadialGradientBrush> ^/Button.Вас kground> ^/Button^ Чтобы позволить кнопке Cancel также использовать эту кисть, необходимо распространить область <RadialGradientBrush> на ресурсный словарь родительского иле- мента. Например, если переместить его в <StackPanel>, обе кнопки смогут использовать одну и ту же кисть, поскольку они являются дочерними элементами диспетчера компоновки. Более того, можно было бы поместить кисть в ресурсный словарь самого окна, так что все аспекты содержимого окна (вложенные панели, и т.п.) могли бы свободно пользоваться ею. Когда требуется определить ресурс, для установки свойства Resources владельца применяется синтаксис "свойство-элемент". Кроме того, элементу ресурса задается значение х:Кеу, которое будет использовано другими частями окна, когда им нужно бу-
1144 Часть VI. Построение настольных пользовательских приложений с помощью WPF дет обратиться к объектному ресурсу. Имейте в виду, что х:Кеу и x:Name — не одно и то же! Атрибут x:Name позволяет получить доступ к объекту как к переменной-члену в файле кода, в то время как атрибут х:Кеу позволяет сослаться на элемент в словаре ресурсов. Среды Visual Studio 2010 и Expression Blend позволяют продвинуть ресурс на более высокий уровень, используя соответствующие окна Properties. В Visual Studio 2010 сначала идентифицируется свойство, имеющее сложный объект, который необходимо упаковать в виде ресурса (в рассматриваемом примере это свойство Background). Рядом со свойством находится небольшая ромбовидная кнопка, щелчок на которой приводит к открытию всплывающего меню. Выберите в этом меню пункт Extract value to Resource... (Извлечь значение в ресурс), как показано на рис. 30.7. [properties Button 1 J3* Properties •* Events Ё! it 4 1 Padding MinWidth 1 MinHeight 1 MaxWidth 1 MaxHeight 1 HorizontalContentAlignment I VerticalCcntentAlignment 1 FlowDirectron I ZIndex \л Brashes E57K9HHHHHH I BorderBrush 1 Foreground 1 OpacrtyMask □ ■ e a т= □ □ □ □ ~ r= □ □ 1 V - п x] 1 ' 1 0 0 Infinity Infinity Center Center LeftToRjght 0 ■ System .Windows. Media. RadialGr Reset Value Apply Data Binding... Apply Resource... Extract Value to Resource... к ———fH^—■■ ju'« —-»| Рис. 30.7. Перемещение сложного объекта в контейнер ресурсов Теперь будет задан вопрос об имени вашего ресурса (myBrush) и предложено указать, куда он должен быть помещен. В данном примере оставьте выбор по умолчанию — MainWindow.xaml (рис. 30.8). Рис. 30.8. Назначение имени объектному ресурсу Покончив с этим, вы увидите, что разметка будет реструктурирована следующим образом: <Window х:Class="ObjectResourcesApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml" Title="Fun with Object Resources" Height=50" Width=25"> <Window.Resources>
Глава 30. Ресурсы, анимация и стили WPF 1145 <RadialGradientBrush x:Key="myBrush"> <GradientStop Color="#FFC44EC4" Offset=" /> <GradientStop Color="#FF829CEB" Offset="l" /> <GradientStop Color="#FF793879" Offset=.669" /> </RadialGradientBrush> </Window.Resources> <StackPanel Orientation="Horizontal"> <Button Margin=5" Height=00" Width=M200" Content="OK" FontSize=0" Background="{StatlcResource myBrush}"></Button> <Button Margin=5" Height=00" Width=00" Content="Cancel" FontSize=0"/> </StackPanel> </Window> Обратите внимание на новый контекст <Window.Resources>, который теперь содержит объект RadialGradientBrush, имеющий ключевое значение myBrush. Расширение разметки {StaticResource} Другое изменение, которое имеет место при извлечении объектного ресурса: свойство, которое было целью извлечения (опять-таки — Background) теперь использует расширение разметки {StaticResource}. Как видите, имя ключа указано в виде аргумента. Если теперь кнопке Cancel нужно будет использовать ту же кисть для рисования фона, она сможет это сделать. Или же, если кнопка Cancel имеет некоторое сложное содержимое, любой подэлемент этой кнопки может также использовать ресурс уровня окна, например, свойство Fill элемента Ellipse. <StackPanel Orientation="Horizontal'^ <Button Margin=5" Height=00" Width=00" Content="OK" FontSize=0" Background="{StaticResource myBrush}"> </Button> <Button Margin=5" Height=00" Width=00" FontSize=0"> <StackPanel> <Label HorizontalAlignment="Center" Content= "No Way!"/> <Ellipse Height=00" Width=00" Fill="{StaticResource myBrush}"/> </StackPanel> </Button> </StackPanel> Изменение ресурса после извлечения После того, как ресурс извлечен в контекст окна, его можно найти его в окне Document Outline (Схема документа), открываемое через пункт меню View1^Other Windows^ Document Outline (ВидоДругие окна^Схема документа) и показанное на рис. 30.9. [Document Outline 1 .j-Window 1 л [ л I л -Resources L RadialGradientBrush (myBrush) -GradientStop -GradientStop -GradientStop StaclcPanel j-Button 1 л L Button К л LStackPanel 1- Label L Ellipse - nxj и Рис. 30.9. Просмотр ресурса в окне Document Outline
1146 Часть VI. Построение настольных пользовательских приложений с помощью WPF Выбрав любой аспект объектного ресурса в окне Document Outline, можно изменять значения, используя окно Properties. Таким образом, можно изменить индивидуальные переходы градиентов, их смещения и прочие аспекты кисти. Расширение разметки {DynamicResource} При подключении к ключевому ресурсу свойство также может использовать расширение разметки {DynamicResource}. Чтобы понять разницу, назначьте кнопке О К имя btnOK и обработайте ее событие Click. В этом обработчике события воспользуйтесь свойством Resources для получения специальной кисти, и затем изменения некоторых ее аспектов: private void btnOK_Click(object sender, RoutedEventArgs e) { // Получить кисть и внести изменение. RadialGradientBrush b = (RadialGradientBrush)Resources["myBrush"]; b.GrartientStops[1] = new GradientStop(Colors.Black, 0.0); } На заметку! Здесь индексатор Resources используется для поиска ресурса по имени. Однако имейте в виду, что если ресурс не обнаруживается, это приводит к генерации исключения времени выполнения. Можно также применять метод TryFindResource (), который не генерирует исключение времени выполнения, а просто возвращает null, если указанный ресурс не найден. Запустив это приложение и щелкнув на кнопке ОК, вы увидите, что изменение кисти учтено, и каждая кнопка обновляется для визуализации модифицированной кисти. А что если вы полностью измените тип кисти, указанной ключом myBrush? Например: private void btnOK_Click(object sender, RoutedEventArgs e) { // Поместить совершенно новую кисть в ячейку myBrush. Resources["myBrush"] = new SolidColorBrush(Colors.Red); } На этот раз после щелчка на кнопке никаких обновлений не ожидается. Причина в том, что расширение разметки {Static Re source} применяет ресурс только однажды и остается "подключенным" к исходному объекту на протяжении времени жизни приложения. Однако если изменить в разметке все вхождения {Static Re source} на {DynamicResource}, обнаружится, что специальная кисть будет заменена кистью со сплошным красным цветом. По существу расширение разметки {DynamicResource} может обнаруживать замену лежащего в основе ключевого объекта новым. Как и следовало ожидать, это требует некоторой дополнительной инфраструктуры времени выполнения, так что обычно стоит придерживаться использования {Static Re source}, если только не планируется заменять объектный ресурс другим объектом во время выполнения, уведомляя все элементы, использующие этот ресурс. Ресурсы уровня-приложения Когда в словаре ресурсов окна имеются объектные ресурсы, то все элементы этого окна могут пользоваться ими, но другие окна приложения — нет. Назначьте кнопке Cancel имя btnCancel и обработайте событие Click. Добавьте в текущий проект новое окно (по имени TestWindow.xaml), содержащее единственную кнопку Button, по щелчку на которой окно закрывается:
Глава 30. Ресурсы, анимация и стили WPF 1147 public partial class TestWindow : Window { public TestWindow() { InitializeComponent (); } private void btnClose_Click(object sender, RoutedEventArgs e) { this.Close (); } } В обработчике Click кнопки Cancel первого окна загрузите и отобразите это новое окно, как показано ниже: private void btnCancel_Click(object sender, RoutedEventArgs e) { TestWindow w = new TestWindow(); w.Owner = this; w.WindowStartupLocation = WindowStartupLocation.CenterOwner; w. ShowDialog() ; } Если новое окно желает использовать myBrush, в данный момент оно не может этого сделать, поскольку myBrush не находится внутри корректного "контекста". Решение состоит в определении объектного ресурса на уровне приложения, а не на уровне конкретного окна. В Visual Studio 2010 не предусмотрено какого-либо способа автоматизировать это, потому просто вырежьте текущий объект кисти из области <Windows.Resource> и поместите его в область <Application.Resources> файла App.xaml: <Application x : Class="Ob;jectResourcesApp . Арр" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> <RadialGradientBrush x:Key="myBrush"> <GradientStop Color="#FFC44EC4" Offset=" /> <GradientStop Color="#FF829CEB" Offset="l" /> <GradientStop Color="#FF793879" Offset=.669" /> </RadialGradientBrush> </Application.Resources> </Application> Теперь объект TestWindow может использовать эту же кисть для рисования своего фона. Найдите свойство Background для этого нового Window, щелкните на маленьком белом квадрате Advanced properties (Расширенные свойства) и выберите пункт меню Apply Resource... (Применить ресурс). Затем можно найти кисть уровня приложения, набирая ее имя в поле поиска (рис. 30.10). Определение объединенных словарей ресурсов Ресурсы уровня приложения — хорошая отправная точка, но что, если необходимо определить набор сложных (или не слишком сложных) ресурсов, которые должны многократно использоваться во множестве проектов WPF? В данном случае требуется определить то, что известно как "объединенный словарь ресурсов". Это — всего лишь файл .xaml, который не содержит ничего помимо коллекции объектных ресурсов. Один проект может иметь любое необходимое количество таких файлов (один для кистей, один для анимации, и т.д.), каждый из которых может добавляться в диалоговом окне Add New Item (Добавить новый элемент), доступном через меню Project (рис. 30.11).
1148 Часть VI. Построение настольных пользовательских приложений с помощью WPF Properties Window <n&mnm> I J5* Properties / Events : lid I VerticalContentAlignment FlowDirecticn > Brushes I BorderBrush I Foreground I Opac'ityMask □ ■ □ □ ЖШН^г R ШПШ / Text I УМЙТУ my ф myBrush Key □ Top Q LeftToRight Resouice... U 1 • П X| « X 1 ' 1 ,, ~^* а§ Static » - Рис. 30.10. Применение ресурсов уровня приложения Installed Templates л Visual С* Cede Data General Web Windows Forms WPF Reporting Workflow Sort by. j Default Ш j ^J Page (WPF) ;«М] User Control (WPF) Type: Visual О XAML resource dictionary Resource Dictionary (WPF) Visual C* ^ Custom Control (WPF) . Jj Flow Document (WPF) OO Pe9e Function (WPF) |j£j Splash Screen (WPF) MyBrushes.xaml Рис. 30.11. Вставка в проект нового объединенного словаря ресурсов В новом файле MyBrushes.xaml понадобится вырезать текущие ресурсы из контекста Application.Resources и перенести их в словарь, как показано ниже: <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <PadialGradientBrush x:Key="myBrush"> <GradientStop Color="#FFC44EC4" Offset=" /> <GradientStop Color="#FF829CEB" Offset="l" /> <GradientStop Color="#FF793879" Offset=.669" /> <7RadialGradientBrush> </ResourceDictionary> Теперь, несмотря на то, что этот словарь ресурсов является частью проекта, возникают ошибки времени выполнения. Причина в том, что все словари ресурсов
Глава 30. Ресурсы, анимация и стили WPF 1149 должны быть объединены (обычно на уровне приложения) в существующий словарь ресурсов. Для этого воспользуйтесь следующим форматом (обратите внимание, что множество словарей ресурсов могут быть объединены добавлением нескольких элементов <ResourceDictionary> в область <ResourceDictionary.MergedDictionaries>). <Application x:Class="Ob3ectResourcesApp.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <!— Взять логические ресурсы из файла MyBrushes . xaml —> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source = "MyBrushes.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application> Определение сборки из одних ресурсов И последнее (по порядку, но не по важности): есть возможность создавать библиотеки классов, которые не содержат ничего, кроме словарей объектных ресурсов. Это может быть полезно, если определен набор тем, которые должны применяться на уровне машины. Объектные ресурсы можно упаковать в выделенную сборку, и тогда приложения, которые хотят использовать их, смогут загружать их в память. Самый легкий способ построения сборки из одних ресурсов состоит в том, чтобы начать с проекта WPF User Control Library (Библиотека пользовательских элементов управления WPF). Добавьте такой проект (под названием MyBrushesLibrary) к текущему решению, используя пункт меню Add^New Project (Добавить1^Новый проект) среды Visual Studio 2010 (рис. 30.12). Теперь полностью удалите файл UserControll.xaml из проекта (единственное, что понадобится, это ссылаемые сборки WPF). Перетащите файл MyBrushes.xaml в проект MyBrushesLibrary и удалите его из проекта ObjectResourcesApp. Окно Solution Explorer должно теперь выглядеть, как показано на рис. 30.13. Installed Templates л Visual C# Windows Web Office Cloud Reporting SharePoint Sifverlight Test '.VCF Workflow Other Languages Name Location: Щ WPF Browser Application Visual C* П Empty Project Visual C# J] Windows Service Visual C* I *C0 WPF Custom Control Library Visual C# WPF User Control Library N Visual C# la Type Visual C# Windows Presentation Foundation user control library WPF Windows Forms Control Libr.T^fWWr User Control Library i 7 MyBrushesLibrary C:\MyCode Рис. 30.12. Добавление проекта WPF User Control Library в качестве начальной точки для построения библиотеки из одних только ресурсов
1150 Часть VI. Построение настольных пользовательских приложений с помощью WPF ■ Solution Explorer ;^3 Solution 'ObjectResourcesApp' B projects) л i^3 MyBrushesLibrary !> Ш Properties b ~ai References ;£*[_ Myfirushesjcaml л _JH ObjectResourcesApp 3i Properties > ^ References t> j«* Appjcaml j> fag MBinWindowj(aml i> jai^ TestWindowjcaml -^ Solution Explorer I Рис. 30.13. Перемещение файла MyBrushes.xaml в новый проект библиотеки Скомпилируемте проект библиотеки кода. Затем установите ссылку на эту библиотеку из проектj ObjectPesourcesApp, используя диалоговое окно Add Reference (Добавить ссылку). Теперь необходимо объединить эти двоичные ресурсы со словарем ресурсов уровня приложения из проекта ObjectResourcesApp. Это потребует некоторого довольно забавного синтаксиса, как показано ниже: <Арр1к. ntion.Pesources> <ResonrceDictionary> <.Р< ^"iu г се Dictionary .MergedDictionaries ~- <! - - Синтаксис: /ИмяСборки; Сощропег^/ИмяФайлаХат1вСборке. xaml - -> rcsourceDictionary Source = "/MyBrusheoLibrary/Component/MyBrushes.xaml"/> -/RLSourceDictionary.MergedDictionaries> </Re ",uurctDictionary> </Application.Resources> Имейте в виду, что эта срока чувствительна к пробелам. Если возле двоеточия или косой черты будут излишние пробелы, возникнут ошибки времени выполнения. Первая часть строки — это дружественное имя внешней библиотеки (без расширения файла). После двоеточия идет слово Component, за которым — имя скомпилированного двоичного ресурса, совпадающее с именем исходного словаря ресурсов XAML. Извлечение ресурсов в Expression Blend Как уже упоминалось, в инструменте Expression Blend предусмотрены подобные способы продвижения локальных ресурсов на уровень окна, уровень приложения и даже уровень словаря ресурсов. Предположим, что определена специальная кисть для свойства Background главного окна, требуется упаковать его как новый ресурс. Используя редактор кистей, щелкните на (маленьком белом) прямоугольнике Advanced Properties возле свойства Background для доступа к опции Convert to New Resource (Преобразовать в новый ресурс), как показано на рис. 30.14. Н&аипеч Data Properties ■ . Name Window ^jj Ff Type Window • bushes —— шшшяяш | Convert tc New ftwo>ate.., ,^И /m ъ m [«■» #FF5A24S9 j 0 0 Щ «□► 57% » Аррешлнсе Opacity 100K Viubrirty Visible Alkn»Transp<trency Рис. 30.14. Извлечение нового ресурса в Expression Blend
Глава 30. Ресурсы, анимация и стили WPF 1151 В открывшемся диалоговом окне объектному ресурсу необходимо назначить имя и указать, где Blend должен сохранить его (текущее окно, приложение или новый объединенный словарь ресурсов). В данном случае myNewBrush помещается на уровень приложения (рис. 30.15). После этого файл App.xaml будет обновлен, как и следовало ожидать. С использованием вкладки Resources (Ресурсы), которая открывается через меню Window, можно модифицировать существующие ресурсы в соответствующем редакторе (рис. 30.16). Рис. 30.15. Определение нового ресурса уровня приложения с помощью Expression Blend Рис. 30.16. Модификация существующих ресурсов в Expression Blend На этом знакомство с системой управления ресурсами WPF завершено. В большинстве приложений нередко придется использовать описанные приемы, и мы еще не раз обратимся к ним в оставшихся главах, посвященных WPF. Теперь давайте приступим к исследованию API-интерфейса встроенной анимации Windows Presentation Foundation. Исходный код. Проект Ob^ectResourceApp доступен в подкаталоге Chapter 30.
1152 Часть VI. Построение настольных пользовательских приложений с помощью WPF Службы анимации WPF В дополнение к службам графической визуализации, которые рассматривались в главе 29, в WPF предлагается API-интерфейс для поддержки служб анимации. Услышав термин анимация, сразу на ум приходят вращающийся логотип компании, последовательность сменяемых друг друга изображений (создающих иллюзию движения), бегущая по экрану строка текста либо программа специфического типа, подобная видеоигре или мультимедиа-приложению. Хотя API-интерфейсы анимации WPF определенно могут использоваться для упомянутых выше целей, анимации могут применяться в любой момент, когда требуется добавить к приложению дополнительный лоск. Например, можно построить анимацию для кнопки на экране, которая слегка увеличивается, когда курсор мыши попадает в ее границы (и уменьшается обратно, когда курсор покидает ее поверхность). Или же можно анимировать окно, чтобы оно закрывалось, используя определенное визуальное представление, например, постепенно исчезая до полной прозрачности. Фактически поддержка анимации WPF может применяться в приложениях любого рода (бизнес- приложениях, мультимедиа-программах, видеоиграх и т.д.), когда нужно создать более привлекательное впечатление у пользователя. Как и многие другие аспекты WPF, понятие анимации не представляет собой здесь ничего нового. Новостью является то, что в отличие от других API-интерфейсов, которые вы могли использовать в прошлом (включая Windows Forms), разработчики не обязаны создавать необходимую инфраструктуру вручную. В среде WPF нет необходимости создавать заранее фоновые потоки или таймеры, чтобы выполнять анимированные последовательности, определять специальные типы для представления анимации, очищать и перерисовывать изображения, либо заниматься утомительными математическими вычислениями. Как и другие аспекты WPF, анимации могут быть построены целиком в XAML- разметке, целиком в коде С#, либо за счет комбинации того и другого. Более того, с помощью Microsoft Expression Blend можно проектировать анимацию с применением интегрированных инструментов и мастеров, даже не видя ни единого фрагмента кода С# или XAML. В следующей главе, когда пойдет речь о шаблонах элементов управления, будет продемонстрировано использование Expression Blend для создания анимации. А пока давайте рассмотрим общую роль API-интерфейса анимации. На заметку! В среде Visual Studio 2010 отсутствует поддержка написания анимации с помощью графических инструментов. В Visual Studio для создания анимации придется набирать код XAML вручную. Роль классов анимации Чтобы разобраться в поддержке анимации в WPF, необходимо начать с рассмотрения классов анимации из пространства имен System.Windows.Media.Animation сборки PresentationCore.dll. Здесь можно найти свыше 100 разных типов классов, которые содержат в своем имени слово Animation. Все эти классы могут быть отнесены к одной из трех обширных категорий. Во-первых, любой класс, который следует соглашению об именовании вида ТилДаннь/xAnimation (ByteAnimation, ColorAnimation, DoubleAnimation, Int32Animation и т.п.) позволяет работать с анимацией с линейной интерполяцией. Она позволяет изменять значение во времени гладко, от начального к конечному.
Глава 30. Ресурсы, анимация и стили WPF 1153 Во-вторых, классы, которые следуют соглашению об именовании вида Тип Да иных AnimationUsmgKeyFrames (StringAnimationUsingKeyFrames, DoubleAnimation UsingKeyFrames, PointAnimationUsingKeyFrames и т.п.) представляют анимацию ключевыми кадрами, которая позволяет проходить циклом по набору определенных значений за указанный период времени. Например, ключевые кадры можно использовать для замены надписи на кнопке, проходя циклом по сериям индивидуальных символов. В-третьих, классы, которые следуют соглашению об именовании вида ТипДанных AnimationUsingPath (DoubleAnimationUsingPath, PointAnimationUsingPath и т.п.) представляют анимацию на основе пути, которая позволяет анимировать объекты, перемещая их по определенному пути. Например, при построении приложения глобального позиционирования (GPS) можно использовать анимацию на основе пути, чтобы перемещать элемент по кратчайшему пути к указанному пользователем месту. Теперь очевидно, что эти классы не позволяют каким-то образом предоставить последовательность анимации непосредственно переменной определенного типа (в конце концов, анимировать значение п с помощью Int32Animation невозможно). Например, рассмотрим свойства Height и Width типа Label. Оба они являются свойствами зависимости, упаковывающими значение double. Чтобы определить анимацию, которая будет увеличивать ширину метки с течением времени, можно подключить объект DoubleAnimation к свойству Height и позволить WPF позаботиться о деталях выполнения анимации. Другой пример: если требуется изменить цвет кисти с зеленого на желть»! в течение 5 секунд, это можно сделать с использованием анимации ColorAnimation. Следует прояснить, что классы Animation могут подключаться к любому свойству зависимости определенного объекта, который соответствует лежащему в основе типу. Как будет показано в главе 31, свойства зависимости — это специальная разновидность свойств, используемых многими службами WPF, включая анимацию, привязку данных и стили. По соглашению свойство зависимости определено как статическое, доступное только на чтение поле класса, чье имя формируется добавлением слова Property к нормальному имени свойства. Например, свойство зависимости для свойства Height класса Button будет доступно в коде как Button.HeightProperty. Свойства То, From и By Во всех классах Animation определено несколько ключевых свойств, которые управляют начальным и конечным значениями, используемыми для выполнения анимации: • То — представляет конечное значение анимации; • From — представляет начальное значение анимации; • By — представляет общую величину, на которую анимация изменяет начальное значение. Несмотря на тот факт, что все классы поддерживают свойства То, From и By, они не получают их через виртуальные члены базового класса. Причина в том, что лежащие в основе типы, упакованные в эти свойства, варьируются в широких пределах (целые, цвета, объекты Thickness и т.п.), и представление всех возможностей через единственный базовый гсласс привело бы к очень сложным конструкциям при кодировании. В связи с этим может также возникнуть вопрос: почему бы не воспользоваться обобщениями .NET для определения единственного обобщенного класса анимации с единственным параметром типа (например, Animate<T>)? Опять-таки, учитывая, что существует огромное число типов данных (цвета, векторы, целые, строки т т.д.), используемых для анимации свойств зависимости, это решение не будет столь ясным, как можно было
1154 Часть VI. Построение настольных пользовательских приложений с помощью WPF бы ожидать (не говоря уже о том, что XAML предлагает лишь ограниченную поддержку обобщенных типов). Роль базового класса Timeline Хотя для определения виртуальных свойств То, From и By не использовался единственный базовый класс, классы Animation все же разделяют общий базовый класс: System.Windows .Media. Animation.Timeline. Этот тип предоставляет множество дополнительных свойств, которые управляют темпом анимации (табл. 30.1). Таблица 30.1. Основные свойства базового класса Timeline Свойство Назначение AccelerationRatio, Эти свойства могут использоваться для управления общим темпом DecelerationRatio, последовательности анимации SpeedRatio AutoReverse Это свойство получает и устанавливает значение, которое указывает, должна ли временная шкала воспроизводиться в обратном направлении после завершения прямой итерации (значение по умолчанию — false) BeginTime Это свойство получает и устанавливает время запуска временной шкалы. По умолчанию принято значение 0, что запускает анимацию немедленно Duration Это свойство позволяет установить продолжительность времени воспроизведения временной шкалы FileBehavior, Эти свойства используются для управления тем, что случится по за- RepeatBehavior вершении временной шкалы (повторение анимации, ничего, и т.д.) Написание анимации в коде С# Довольно высоки шансы, что большинство проектов анимации WPF будут созданы с использованием редактора анимации Expression Blend, и потому представлены в виде разметки. Однако при этом первом знакомстве со службами анимации WPF мы не будем использовать ничего, кроме кода С#, поскольку это более прямолинейный подход. В частности, мы построим окно, которое содержит элемент Button, обладающий довольно странным поведением: он вращается вокруг своего левого верхнего угла, когда на него наводится курсор мыши. Начните с создания нового WPF-приложения по имени SpinningButtonAnimationApp в Visual Studio 2010. Обновите начальную разметку, как показано ниже (обратите внимание на обработку события МоиseEvent кнопки): <Window x:Class="SpinningButtomAnimationApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2 006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2 00 6/xaml" Title="Animations in C# code" Height=50" Width=2 5" WindowstartupLocation="CenterScreen"> <Grid> <Button x:Name="btnSpinner" Height=0" Width=00" Content="I Spin1" MouseEnter="btnSpinner_MouseEnter"/> </Grid> </Window> Теперь импортируйте пространство имен System.Windows.Animation и добавьте следующий код в файл С#:
Глава 30. Ресурсы, анимация и стили WPF 1155 public partial class MainWindow : Window { private bool isSpinning = false; private void btnSpinner_MouseEnter(object sender, MouseEventArgs e) { if (!isSpinning) { isSpinning = true; // Создать объект анимации double и зарегистрировать //с событием Completed. DoubleAnimation dblAnim = new DoubleAnimation(); dblAnim. Completed += (o, s) => { isSpinning = false; }; // Установить начальное и конечное значения. dblAnim.From = 0; dblAnim. To = 3 60; // Создать объект RotateTransform и присвоить // его свойству RenderTransform кнопки. RotateTransform rt = new RotateTransform(); btnSpinner.RenderTransform = rt; // Выполнить анимацию объекта RotateTransform. rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim); } } } Первая главная задача этого метода — сконфигурировать объект DoubleAnimation, который начнется со значения 0 и закончится значением 360. Обратите внимание, что на этом объекте также обрабатывается событие Completed за счет переключения булевской переменной уровня класса, которая используется для того, чтобы выполняющаяся анимация не была сброшена в начало. Затем создается объект RotateTransform, который подключается к свойству RenderTransform элемента управления Button (btnSpinner). И последнее: объект RenderTransform информируется о начале анимации его свойства Angle с использованием для этого объект£ DoubleAnimation. Описание анимации в коде обычно осуществляется вызовом метода BeginAnimation () и передачей ему лежащего в основе свойства зависимости, которое требуется анимировать (вспомните, что по существующему соглашению это — поле класса), и связанного объекта анимации. Добавим в программу еще одну анимацию, которая заставит кнопку плавно становиться невидимой при щелчке. Для начала создадим обработчик события Click объекта btnSpinner и поместим в него следующий код: private void btnSpinner_Click(object sender, RoutedEventArgs e) { DoubleAnimation dblAnim = new DoubleAnimation(); dblAnim.From = 1.0; dblAnim.To = 0.0; btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim); } Здесь изменяется свойство Opacity, чтобы постепенно скрыть кнопку из виду. В настоящее время, однако, это сделать трудно, поскольку кнопка вращается слишком быстро. Каким образом управлять ходом анимации? Ответ на этот вопрос ищите ниже. Управление темпом анимации По умолчанию анимация занимает примерно одну секунду на переход между значениями, присвоенными свойствам From и То. Поэтому кнопке требуется одна секунда,
1156 Часть VI. Построение настольных пользовательских приложений с помощью WPF чтобы повернуться на 360 градусов, и так же за секунду она постепенно скрывается из виду (после щелчка на ней). Определить другой период времени на выполнение анимации можно сделать с помощью свойства Duration объекта анимации, которому присваивается экземпляр объекта Duration. Обычно период времени устанавливается передачей объекта TimeSpan конструктору Duration. Рассмотрим следующее изменение, при котором кнопке выделяется на вращение 4 секунды: private void btnSpinner_MouseEnter(object sender, MouseEventArgs e) { if (!isSpinning) { isSpinning = true; // Создать объект анимации double и зарегистрировать // событие Completed. DoubleAmmation dblAnim = new DoubleAnimation (); dblAnim. Completed += (o, s) => { isSpinning = false; }; //На завершение поворота кнопке отводится 4 секунды. dblAnim.Duration = new Duration(TimeSpan.FromSecondsD)); } } Благодаря этой модификации, появляется возможность щелкнуть на кнопке во время ее вращения, после чего она плавно исчезает. На заметку! Свойство BeginTime класса Animation также принимает объект TimeSpan. Вспомните, что это свойство устанавливается для указания времени до начала процесса анимации. Запуск в обратном порядке и циклическое выполнение анимации Объекты Animation можно также заставить запускать анимацию в обратном порядке по ее завершении, устанавливая свойство AutoReverse в true. Например, если необходимо, чтобы кнопка снова стала видимой после исчезновения, для этого можно написать следующий код: private void btnSpinner_Click(object sender, RoutedEventArgs e) { DoubleAnimation dblAnim = new DoubleAnimation() ; dblAnim.From = 1.0; dblAnim.To = 0.0; //По завершении - запуск в обратном порядке. dblAnim.AutoReverse = true; btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim); } Если хотите, чтобы анимация повторялась некоторое количество раз (или никогда не прекращалась), то для этого можно использовать свойство RepeatBehavior, общее для всех классов Animation. Передача конструктору простого числового значения позволяет задать жестко закодированное количество повторений. С другой стороны, если передать конструктору объект TimeSpan, то можно указать длительность времени повторения анимации. Наконец, чтобы выполнять анимацию бесконечно, можно просто указать RepeatBehavior.Forever. Рассмотрим следующие способы изменения поведения повтора любого из двух объектов DoubleAnimation, использованных в этом примере:
Глава 30. Ресурсы, анимация и стили WPF 1157 // Бесконечный цикл. dblAmm.RepeatBehavior = RepeatBehavior . Forever ; // Повторить три раза. dblAmm.RepeatBehavior = new RepeatBehavior C) ; // Повторять в течение 30 секунд. dblAnim.FepeatBphavior = new RepeatBehavior(TimeSpan.FromSeconds C0) ) ; На этом исследования анимационных аспектов объекта в коде С# и API-интерфейса анимации WPF завершены. Теперь посмотрим, как сделать то же самое в XAML- разметке. Исходный код. Проект SpinningButtonAnimationApp доступен в подкаталоге Chapter 30. Описание анимации в XAML Описание анимации в разметке подобно описанию ее в коде, по крайней мере, если речь идет о простых анимированных последовательностях. Когда нужно создавать более сложные анимации, которые могут включать одновременные изменения значений сразу множества свойств, объем разметки может значительно увеличиться. К счастью, позаботиться обо всех деталях могут редакторы анимации Expression Blend. Но даже несмотря на это, важно знать основы представления анимации в XAML, поскольку это облегчит решение задачи модификации и подгонки сгенерированного инструментом содержимого. На заметку! В подкаталоге XamlAnimations примеров кода для данной главы имеется множество файлов XAML. Скопируйте эти файлы разметки в специальный редактор XAML или в редактор Kaxaml, чтобы просмотреть результаты. В основном создание анимации подобно всему тому, что вы уже видели: сначала конфигурируется объект Animation, который затем ассоциируется со свойством объекта. Однако одно большое отличие состоит в том, что API-интерфейс WPF не слишком дружественен к вызову функций. В связи с этим вместо вызова BeginAnimation () применяется раскадровка (storyboard) в качестве непрямого уровня. Давайте рассмотрим полный пример анимации, определенный в терминах XAML, с последующим подробным разбором. Следующее определение XAML отобразит окно, содержащее единственную метку. Как только объект Label загружается в память, он начинает последовательность анимации, при которой размер шрифта увеличивается от 12 до 100 за период в четыре секунды. Анимация будет повторяться столько времени, сколько объект остается загруженным в память. Эта разметка находится в файле GrowLabelFont.xaml, поэтому скопируйте его в приложение MyXamlPad.exe и понаблюдайте за поведением. Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns: .' = "http://schemes.microsoft.com/winfx/2006/xaml" Hpight= 0 0" Width="h00" WindouStartupLocation=" Center/Screen" Title="r,ro\Jinq Label Font1""-» <StackPanel ^Labrl Content = "InteLesting. . . "> «■ La Ьр 1. Tr l qq'r r s - - EventTriqqer PnutedEvent = "Label.Loaded"> E^ entTilqger.Action: Begj riL4or' t._ ird> -M-i'i 'bo'-trd TargetPtoperty = "FontSize">
1158 Часть VI. Построение настольных пользовательских приложений с помощью WPF <DoubleAnimation From = 2" То RepeatBehavior </Storyboard> </Beginstoryboard> </EventTrigger.Actions> </EventTrigger> </Label.Triggers> </Label> </StackPanel> </Window> А теперь подробно разберем этот пример. Роль раскадровки Двигаясь от самого вложенного элемента к внешнему, сначала мы встречаем элемент <DoubleAnimation>, который использует те же свойства, что были установлены в процедурном коде (То, From, Duration и RepeatBehavior): <DoubleAnimation From = 2" To = 00" Duration = :0:4" RepeatBehavior = "Forever"/> Как упоминалось ранее, элементы Animation помещаются внутрь элемента <Storyboard>, который используется для отображения объекта анимации на родительский тип через свойство TargetProperty — которым в данном случае является Font Size. Элемент <Storyboard> всегда помещен в родительский элемент по имени <BeginStoryboard>, который представляет собой всего лишь способ обозначения местоположения раскадровки: <BeginStoryboard> <Storyboard TargetProperty = "FontSize"> <DoubleAnimation From = 2" To = 00" Duration = :0:4" RepeatBehavior = "Forever"/> </Storyboard> </Beginstoryboard> Роль триггеров событий Как только элемент <BeginStoryboard> определен, должно быть указано какое-то действие, которое вызовет запуск анимации. В WPF предусмотрены разные способы реагирования на условия времени выполнения в разметке, один из которых называется триггер. В самом общем виде триггер можно рассматривать как способ реагирования на условия событий в XAML-разметке, минуя процедурный код. Обычно, когда определяется реакция на событие в С#, пишется специальный код, который выполняется по наступлению события. Триггер — это всего лишь способ получить уведомление о том, что некоторое событие произошло (загрузка элемента в память, наведение курсора мыши на элемент, получение элементом фокуса и т.п.). Получив уведомление о наступлении события, можно запускать раскадровку. В рассматриваемом примере мы реагируем на факт загрузки элемента Label в память. Поскольку нас интересует событие Loaded элемента Label, элемент <EventTrigger> помещается в коллекцию триггеров элемента Label: <Label Content = "Interesting. .. "> <Label.Triggers> <EventTrigger RoutedEvent = "Label.Loaded"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard TargetProperty = "FontSize"> 00" Duration = :0:4" "Forever"/>
Глава 30. Ресурсы, анимация и стили WPF 1159 <DoubleAnimation From = 2" То = 00" Duration = :0:4" RepeatBehavior = "Forever"/> </Storyboard> </Beginstoryboard> </EventTrigger.Actions> </EventTrigger> </Label.Triggers> </Label> Теперь рассмотрим другой пример определения анимации в XAML — с использованием анимации ключевыми кадрами. Анимация с использованием дискретных ключевых кадров В отличие от объектов анимации с линейной интерполяцией, которая обеспечивает продвижение только между начальной и конечной точками, ключевые кадры позволяют создавать коллекции определенных значений анимации, которые должны иметь место в определенные моменты времени. Чтобы проиллюстрировать использование типа дискретного ключевого кадра, предположим, что необходимо построить элемент управления Button, который анимирует свое содержимое таким образом, чтобы на протяжении трех секунд появлялось значение ОК!, по одному символу за раз. Показанная ниже разметка находится в файле StringAnimation.xaml. Скопируйте ее в программу MyXamlPad.exe и просмотрите результат. <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml" Height=00" Width=00" WindowStartupLocation="CenterScreen" Title="Animate String Data!"> <StackPanel> <Button Name="myButton" Height=0" FontSize=6pt" FontFamily="Verdana" Width = 00"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <StringAnimationUsingKeyFrames RepeatBehavior = "Forever" Storyboard.TargetName="myButton" Storyboard.TargetProperty="Content" Duration=:0:3"> <DiscreteStringKeyFrame Value="" KeyTime=:0:0" /> <DiscreteStringKeyFrame Value=" KeyTime=:0:1" /> <DiscreteStringKeyFrame Value="OK" KeyTime=:0:1.5" /> <DiscreteStringKeyFrame Value="OK!" KeyTime=:0 : 2" /> </StringAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> </StackPanel> </Window> Прежде всего, обратите внимание, что для кнопки определен триггер события, который гарантирует запуск раскадровки при загрузке кнопки в память. Класс StringAnimationUsingKeyFrames отвечает за изменение содержимого кнопки, через значения Storyboard.TargetName и Storyboard.TargetProperty.
1160 Часть VI. Построение настольных пользовательских приложений с помощью WPF Внутри контекста элемента <StringAnimationUsingKeyFrames> определены четыре элемента DiscreteStringKeyFrame, которые изменяют свойство Content на протяжении двух секунд (длительность, установленная StringAnimationUsingKeyFrames, составляет в сумме три секунды, поэтому между финальным ! и следующим появлением О заметна небольшая пауза). Теперь, когда вы получили некоторое представление о том, как строятся анимации в коде С# и XAML, давайте уделим внимание стилям WPF, которые интенсивно используют графику, объектные ресурсы и анимацию. Исходный код. Свободные файлы XAML доступны в подкаталоге XamlAnimations каталога Chapter 30. Роль стилей WPF При построении пользовательского интерфейса WPF-приложения нередко требуется обеспечить общий вид и поведение для множества элементов управления. Например, необходимо, чтобы все типы кнопок имели общую высоту, ширину, цвет и размер шрифта своего строчного содержимого. Хотя это можно обеспечить за счет установки значений индивидуальных свойств, такой подход затрудняет внесение изменений, поскольку при каждом изменении придется переустанавливать один и тот же набор свойств на множестве объектов. К счастью, в WPF предлагается простой способ ограничения внешнего вида и поведения взаимосвязанных элементов управления с использованием стилей. Просто говоря, стиль WPF — это объект, который поддерживает коллекцию пар "свойство/значение". С точки зрения программирования индивидуальный стиль представлен классом System.Windows.Style. Этот класс имеет свойство по имени Setters, которое является строго типизированной коллекцией объектов Setter. Именно объект Setter позволяет определять пары "свойство/значение". Вдобавок к коллекции Setters класс Style также определяет ряд других важных членов, которые позволяют включать триггеры, ограничивать место применения стиля и даже создавать новый стиль на основе существующего (воспринимайте это как "наследование стиля"). Ниже перечислены важные члены класса Style: • Triggers — представляет коллекцию объектов триггеров, которая позволяет фиксировать различные условия событий в стиле; • BasedOn — позволяет строить новый стиль на основе существующего; • TargetType — позволяет ограничивать места применения стиля. Определение и применение стиля Почти в любом случае объект Style упаковывается в виде объектного ресурса. Подобно любому объектному ресурсу, его можно упаковывать на уровне окна или уровне приложения, а также внутри выделенного словаря ресурсов (это замечательно, потому что делает объект Style легко доступным по всему приложению). Теперь вспомните, что целью является определение объекта Style, который наполняет (как минимум) коллекцию Setters набором пар "свойство/значение". Создайте новое приложение WPF по имени Wpf Styles, используя Visual Studio 2010. Давайте построим стиль, фиксирующий базовые характеристики элемента управления в нашем приложении. Откройте файл App.xaml и определите следующий именованный стиль:
Глава 30. Ресурсы, анимация и стили WPF 1161 <Application х:Class="WpfStyles.Арр" xmlns="http://schemas.microsoft.com/winfx/20 06/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> <Style x:Key ="BasicControlStyleII> <Setter Property = "Control.FontSize" Value =4"/> <Setter Property = "Control.Height" Value = 0"/> <Setter Property = "Control.Cursor" Value = "Hand"/> </Style> </Application.Resources> </Application> Обратите внимание, что BasicControlStyle добавляет три объекта Setter к внутренней коллекции. Теперь применим этот стиль к нескольким элементам управления в главном окне. Поскольку этот стиль является объектным ресурсом, элементы управления, которые хотят использовать его, нуждаются в расширении разметки {StackResource} или {DynamicResource} для нахождения стиля. Когда они находят стиль, то устанавливают элемента ресурса в идентично именованное свойство Style. Рассмотрим следующее определение <Window>: <Window x:Class="WpfStyles.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="A Window with Style'" Height=29" Width=25" WindowStartupLocation="CenterScreen"> <StackPanel> <Label x:Name="lblInfo" Content="This style is boring..." Style="{StaticResource BasicControlStyle}" Width=50"/> <Button x:Name="btnTestButton" Content="Yes, but we are reusing settings'" Style="{StaticResource BasicControlStyle}" Width=50"/> </StackPanel> </Window> Запустив это приложение, вы обнаружите, что оба элемента управления поддерживают один и тот же курсор, высоту и размер шрифта. Переопределение настроек стиля В примере определены элементы Button и Label, которые подчиняются ограничениям, накладываемым нашим стилем. Разумеется, если элемент управления желает применить стиль и затем изменить некоторые из определенных установок, это нормально. Например, Button теперь использует курсор Help (вместо курсора Hand, определенного в стиле): <Button x:Name="btnTestButton" Content="Yes, but we are reusing settings'" Cursor="Help" Style="{StaticResource BasicControlStyle}" Width=50" /> Стили обрабатываются перед индивидуальными установками свойств элемента управления, использующего стиль, поэтому элементы управления могут "переопределять" настройки от случая к случаю. Автоматическое применение стиля с помощью TargetType ^ Сейчас стиль определен таким образом, что его может принять любой элемент управления (и должен делать это явно, устанавливая свое свойство Style), исходя из того, что каждое свойство квалифицировано классом Control. Для программы, определяющей десятки настроек это означало бы значительный объем повторяющегося кода. Один из способов в некоторой степени улучшить ситуацию состоит в применении ат-
1162 Часть VI. Построение настольных пользовательских приложений с помощью WPF рибута TargetType. Добавление этого атрибута к открывающемуся элементу Style позволяет точно указать, когда он может быть применен: <Style x:Key ="BasicControlStyle11 TargetType="ControlII> <Setter Property = "FontSize" Value =4,,/> <Setter Property = "Height" Value = 0,,/> <Setter Property = "Cursor" Value = IIHand"/> </Style> На заметку! При построении стиля, использующего тип базового класса, не нужно беспокоиться о том, что присваиваемое значение свойству зависимости не поддерживается производными типами. Если производный тип не поддерживает определенное свойство зависимости, оно игнорируется. Это до некоторой степени помогает, но все равно мы имеем стиль, который может применяться к любому элементу управления. Атрибут TargetType более удобен, когда необходимо определить стиль, который может быть применен только к определенному типу элементов управления. Добавьте следующий стиль в словарь ресурсов приложения: <Style x:Key ="BigGreenButton" TargetType=,,Button"> <Setter Property = "FontSize" Value =0,,/> <Setter Property = "Height" Value = 00,,/> <Setter Property = "Width" Value = ,,100"/> <Setter Property = "Background" Value = "DarkGreen"/> <Setter Property = "Foreground" Value = "Yellow"/> </Style> Этот стиль будет работать только с элементами управления Button (или его подклассами), и если применить его к несовместимому элементу, то возникнут ошибки разметки и времени компиляции. Если Button использует новый стиль так, как продемонстрировано ниже, то вывод будет выглядеть подобно показанному на рис. 30.17: <Button x:Name="btnTestButton" Content="This Style ROCKS!" Cursor=,,Help" Style=" { StaticResource BigGreenButton} " Width=,,250" /> Рис. 30.17. Элементы управления с разными стилями Создание подклассов существующих стилей Новые стили можно также строить на основе какого-то существующего стиля, с помощью свойства BasedOn. Расширяемый стиль должен иметь соответствующий атрибут х:Кеу в словаре, поскольку производный стиль будет ссылаться на него по имени, используя расширение разметки {StaticResource}. Ниже показан новый стиль, основанный на стиле BigGreenButton, который поворачивает элемент-кнопку на 20 градусов:
Глава 30. Ресурсы, анимация и стили WPF 1163 <!-- Стиль основан на BigGreenButton --> <Style x:Key =,,TiltButton" TargetType=,,Button" BasedOn = "{StaticResource BigGreenButton} "> <Setter Property = "Foreground" Value = "White"/> <Setter Property = "RenderTransform"> <Setter.Value> <RotateTransform Angle = 0"/> </Setter.Value> </Setter> </Style> На этот раз вывод будет выглядеть, как на рис. 30.18. Рис. 30.18. Использование производного стиля Роль безымянных стилей Предположим, что нужно гарантировать, чтобы все элементы управления Text В ох имели одинаковый внешний вид и поведение. Пусть определен стиль в форме ресурса уровня приложения, доступ которому имеют все окна программы. Хотя это шаг в правильном направлении, но если есть множество окон с множеством элементов управления TextBox, то свойство Style придется устанавливать много раз! Стили WPF могут быть неявно применены ко всем элементам управления внутри заданного контекста XAML. Чтобы создать такой стиль, необходимо использовать свойство TargetType, но не присваивать ресурсу Style значение х:Кеу. Такой "безымянный стиль" теперь применяется ко всем элементам управления корректного типа. Ниже показан другой стиль уровня приложения, который будет применен автоматически ко всем элементам управления TextBox текущего приложения: <!— Стиль по умолчанию для всех текстовых полей --> <Style TargetType="TextBox"> <Setter Property = "FontSize" Value =4"/> <Setter Property = "Width" Value = 00'7> <Setter Property = "Height" Value = 0"/> <Setter Property = "BorderThickness" Value = "/> <Setter Property = "BorderBrush" Value = "Red"/> <Setter Property = "FontStyle" Value = "Italic"/> </Style> Можно определить любое количество элементов управления TextBox, и все они автоматически получат определенный внешний вид. Если конкретному элементу TextBox не нужен внешний вид и поведение по умолчанию, он может отказаться от него, установив свойство Style в {x:Null}. Например, элемент txtTest будет иметь безымянный стиль по умолчанию, а элемент txtTest2 сделает все по-своему:
1164 Часть VI. Построение настольных пользовательских приложений с помощью WPF «'TextBox x:Name = IItxtTest"/> ^TextBox x:Name = ,,txtTest2" Style=" { x :Null} " BorderBrush="Black" BorderThickness = ,,5" Height = ,,60" Width=00" Text="Ha'"/ > Определение стилей с триггерами Стили WPF могут также содержать триггеры, упаковывая объекты Trigger в коллекцию Triggers объекта Style. Использование триггеров в стиле позволяет определить некоторые элементы <Setter> таким образом, что они будут применены только в случае истинности определенного условия триггера. Например, возможно, требуется увеличить размер шрифта, когда курсор мыши наведен на кнопку. Или, может быть, требуется обеспечить, чтобы текстовое поле с текущим фокусом было подсвечено желтым фоном. Ниже показана соответствующая разметка: <!-- Стиль по умолчанию для всех текстовых полей --> Style TargetType=,,TextBox"> • Setter Property = "FontSize" Value = 4"/:- 'Setter Property = "Width" Value = 00"/> ^Setter Property = "Height" Value = 0"/^ Setter Property = "BorderThickness" Value = "/> <Setter Property = "BorderBrush" Value = "Ped" /> •"Setter Property = "FontStyle" Value = "Italic"/^ <!-- Следующая установка будет применена, только когда текстовое поле находится в фокусе --> <Style.Triggers> <Trigger Property = "IsFocused" Value = "True" • <Setter Property = "Background" Value = "Yellow"/> </Trigger> <!Style.Triggers> </Style> Опробовав этот стиль, вы обнаружите, что по мере перехода с помощью клавиши <ТаЬ> между объектами TextBox текущий выбранный TextBox получает желтый фон (если только это не отключено присваиванием {x:Null} свойству Style). Триггеры свойств также весьма интеллектуальны, в том смысле, что когда условие триггера не истинно, свойство автоматически получает значение, присвоенное по умолчанию. Поэтому, как только TextBox теряет фокус, он также автоматически принимает цвет по умолчанию, без какого-либо вашего участия. В отличие от этого, триггеры событий (которые рассматривались при описании анимации WPF) не возвращаются автоматически к предыдущему состоянию. Определение стилей с множеством триггеров Триггеры также могут быть спроектированы так, что определенные элементы <Setter> будут применены, когда истинно множество условий (это похоже на построение оператора if для множества условий). Предположим, что необходимо установить фон Yellow для элемента TextBox только в том случае, если он имеет активный фокус и курсор мыши наведен на него. В этом случае можно воспользоваться элементом <MultiTrigger> для определения каждого условия: <!— Стиль по умолчанию для всех текстовых полей --> --Style TargetType = "TextBox"> <Setter Property = "FontSize" Value =4"/> <Setter Property = "Width" Value = 00"/> ^Setter Property = "Height" Value = 0"/> <Setter Property = "BorderThickness" Value = "/> <Setter Property = "BorderBrush" Value = "Red"/> <Setter Property = "FontStyle" Value = "Italic"/"-
Глава 30. Ресурсы, анимация и стили WPF 1165 <!-- Следующая установка будет применена, только когда текстовое поле в фокусе И на него наведен курсор мыши —> <Style.Triggers> <MultiTrigger> <MultiTrigger . Conditioris> <Condition Property = "IsFocused" Value = "True"/-> <Condition Property = "IsMouseOver" Value = "True"/> </MultiTrigger.Conditions> <"Setter Property = "Background" Value = "Yellow"/> </MultiTrigger> </Style.Triggers> </Style> Анимированные стили Стили также могут включать триггеры, которые запускают последовательность анимации. Ниже приведен последний стиль, который, будучи примененным к элементам управления Button, заставит элемент расти и сжиматься в размере, когда курсор мыши находится внутри области поверхности кнопки: <!— Стиль растущей кнопки —> <Style x:Key = "GrowingButtonStyle" TargetType="Button"> <Setter Property = "Height" Value = 0'7> <Setter Property = "Width" Value = 00"/> <Style.Triggers> <Tngger Property = "IsMouseOver" Value = "True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard TargetProperty = "Height"> <DoubleAnimation From = 0" To = 00" Duration = :0:2" AutoReverse="True"/> </Storyboard> </Beginstoryboard> </Trigger.EnterActions> </Trigger> </Style.Triggers> </Style> Здесь коллекция триггеров проверяет истинность свойства IsMouseOver. Если оно истинно, определяется элемент <Trigger.EnterActions> для запуска простой раскадровки, которая заставляет кнопку за две секунды увеличиться до значения Height, равного 200 (и затем вернуться к значению Height, равному 40). Чтобы добавить другие изменения свойств, можно также определить область <Trigger.ExitActions> для определения любых специальных действий, которые должны быть выполнены, когда IsMouseOver равно false. Программное применение стилей Вспомните, что стиль может быть применен также во время выполнения. Это полезно, когда нужно позволить конечным пользователем выбирать, как должен выглядеть их пользовательский интерфейс, либо если требуется принудительно применить внешний вид и поведение на основе настроек безопасности (например, стиль DisableAllButton) или каких-то других условий. В этом проекте уже определено множество стилей, и многие из них могут быть применены к элементам управления Button. Теперь давайте переделаем пользовательский интерфейс главного окна, чтобы позволить пользователю выбирать из некоторых этих стилей, указывая имя в списке ListBox. На основе такого выбора будет применяться соответствующий стиль. Ниже показана финальная разметка элемента <Window>:
1166 Часть VI. Построение настольных пользовательских приложений с помощью WPF <Window x:Class="WpfStyles.MainWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height=,,350" Title="A Window with Style1" Width=2511 Windows tar tupLocation=" Center Screen "> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top"> <Label Content="Please Pick a Style for this Button" Height=0"/> <ListBox x:Name ="lstStyles" Height ="80" Width =50" Background="LightBlue" SelectionChanged ="comboStyles_Changed" /> </StackPanel> <Button x:Name="btnStyle" Height=0" Width=00" Content="OK!"/> </DockPanel> </Window> Элемент управления ListBox (по имени lststyles) будет определен динамически, когда это потребуется конструктору окна: public MainWindow() { InitializeComponent (); // Наполнить это окно списка всеми стилями Button. IstStyles.Items.Add("GrowingButtonStyle"); lststyles.Items.Add("TiltButton"); IstStyles.Items.Add("BigGreenButton") ; lststyles.Items.Add("BasicControlStyle"); } Последняя задача связана с обработкой события SelectionChanged в соответствующем файле кода. Обратите внимание, что в следующем коде можно извлекать текущий ресурс по имени, используя унаследованный метод TryFindResouce(): private void comboStyles_Changed (object sender, SelectionChangedEventArgs e) { // Получить стиль, выбранный из списка. Style currStyle = (Style) TryFindResource(IstStyles.SelectedValue); if (currStyle != null) { // Установить стиль типа кнопки. this.btnStyle.Style = currStyle; Запустив это приложение, можно производить выбор одного из четырех стилей кнопок на лету. На рис. 30.19 показано готовое приложение. Исходный код. Проект Wpf Styles доступен в подкаталоге Chapter 30. Генерация стилей с помощью Expression Blend В завершение исследования стилей WPF давайте посмотрим, как Expression Blend может автоматизировать процесс конструирования стиля. Обсуждение редактора анимации (и связанные с ним темы, такие как триггеры и шаблоны элементов управления) откладывается до следующей главы. Здесь будут рассматриваться стили из группы Simple Styles (Простые стили), доступные в окне Assets Library (Библиотека активов).
Глава 30. Ресурсы, анимация и стили WPF 1167 A Window with Si Please Pick a Styte for this Button TiltButton BigGreenButton BasicControlStyte Рис. 30.19. Элементы управления с разными стилями Работа с визуальными стилями по умолчанию Инструмент Expression Blend поставляется с множеством стилей по умолчанию для распространенных элементов управления, которые вместе образуют группу под названием Simple Styles. Открыв окно Assets Library, в древовидном представлении слева легко обнаружить группу Styles (Стили), как показано на рис. 30.20. Рис. 30.20. Стили по умолчанию в Expression Blend При выборе одного из этих простых стилей происходит несколько вещей. Первым делом, элемент управления объявляется с расширением разметки {DynamicResource}. Например, со стилем Simple Button Style связана примерно следующая XAML-разметка: <Button Margin=16,49,220,0" Style="{DynamicResource SimpleButton}" VerticalAlignment="Top11 Height=311 Content=IIButton"/> Как ни странно, элемент управления будет выглядеть как нормальный Button. Однако, заглянув в окно Projects (Проекты), вы увидите, что Expression Blend добавил новый файл по имени Simple Styles.xaml (рис. 30.21). Дважды щелкнув на этом элементе и открыв редактор XAML, вы увидите очень большой раздел <ResourceDictionary> стилей элементов управления по умолчанию. Более того, открыв окно Resources (Ресурсы), можно обнаружить, что каждый перечисленный элемент может редактироваться щелчком на заданной позиции (рис. 30.22). Если щелкнуть на значке SimpleButton в представлении Resources, откроется новый визуальный конструктор, с помощью которого можно соответствующим образом изменить стиль, используя стандартные инструменты редактирования Blend (окно Properties, редактор анимации и т.п.). Все это показано на рис. 30.23.
1168 Часть VI. Построение настольных пользовательских приложений с помощью WPF Projects ■ Assets Triggers States Search Щ Solution "WpfApplkationo' A project^)) * |S Wpf Appbcation* > Щ Re* ~ Рис. 30.21. Это словарь ресурсов Properties Resources * Data в* DisabledForegroundBrush Disabled BaclcgroundBrush Щ Disabled BorderBrush ii WindowBackgroundBrush Щ Defaulted BorderBrush SolidBorderBrush Light BorderBrush LightColorBfush | GlyphBrush Sim pieBiittonFocusVisual SimpleButton rh^_ _. in_„. SimpteChedcBox Simp*eRad»oButton SimpteRepeatButton Sim pleThumbStyle Sim pteScrol IRepeatButton Style SimpleScrollBar SlmpteSorollViewer Sim pietist Box SimpleListBoxItem ъ ШШШШШШ* H ' |- ™™:i шшшшш- Щ . JEdrl resource 1 О «r • « ■ ■ • Рис. 30.22. Выбор стиля по умолчанию для редактирования jjTj WpfApplication>6.sln - Microsoft Expression Blend 3 Object Project Tools Window Help III* Solution ~' . Ц0 WptAppfcoihor* m GO Appjcaml* MainWindow-x '■ с < 1 Properties ' Resources Type Button Search * Brushes Background | BorderBrush ^m Foreground r , 1 Data * ■ ЕИ 1° r resources R 231 |Ш --1 fsr~l ESSBfl Рис. 30.23. Редактирование стиля по умолчанию
Глава 30. Ресурсы, анимация и стили WPF 1169 Изящество этого подхода в том, что файл Simple Styles.xaml объединяется с ресурсами приложения, и потому все локальные изменения будут автоматически компилироваться в окончательное приложение. Использование простых стилей по умолчанию позволяет получить начальную разметку для нормального внешнего вида и поведения элемента управления и его настройки его на уровне проекта. Ниже показана разметка, которая должна находиться в файле App.xaml: <Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml" x:Class="WpfApplication6.App" StartupUri="MainWindow.xaml"> <Application.Resources> <!-- Здесь определяются ресурсы, доступные на уровне приложения —> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Simple Styles.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application> На этом глава завершена. В следующей главе ваши знания о WPF будут дополнены рассмотрением шаблонов элементов управления и специальных классов UserControl. Попутно также будет проиллюстрировано несколько новых аспектов работы с Expression Blend. Резюме В первой части этой главы рассматривалась система управления ресурсами WPF. Мы начали с изучения работы с двоичными ресурсами, а затем ознакомились с ролью объектных ресурсов. Вы узнали, что объектные ресурсы — это именованные фрагменты XAML-разметки, которые могут быть сохранены в различных местах, позволяя многократно использовать это содержимое. После этого был описан API-интерфейс анимации WPF. Рассматривались некоторые примеры создания анимации в коде С#, а также в XAML-разметке. Для управления выполнением анимации, определенной в разметке, служат элементы и триггеры Storyboard. В завершение главы был рассмотрен механизм стилей WPF, который интенсивно использует графику, объектные ресурсы и анимации. Попутно вы узнали о том, как использовать Expression Blend для упрощения создания стилей с применением простых стилей по умолчанию в качестве отправной точки.
ГЛАВА 31 Шаблоны элементов управления WPF и пользовательские элементы управления В этой главе исследование модели программирования Windows Presentation Foundation (WPF) завершается рассмотрением вопросов построения специальных элементов управления. Хотя модель содержимого и механизм стилей позволяют добавлять уникальные части к стандартным элементам управления WPF, процесс построения специальных шаблонов и элементов UserControl дают возможность полностью определить то, как элемент управления должен визуализировать свой вывод, отвечать на переходы между состояниями и интегрироваться в API-интерфейсы WPF Глава начинается с рассмотрения двух тем, которые важны для создания специального элемента управления, а именно — свойств зависимости (dependency properties) и маршрутизируемых событий (routed events). После освоения этих тем мы перейдем к изучению роли шаблона по умолчанию и программному взаимодействию с ним во время выполнения. Оставшаяся часть этой главы посвящена построению классов UserControl с помощью Visual Studio 2010 и Expression Blend. Попутно вы узнаете больше о триггерах WPF (впервые представленных в главе 30), а также о новом механизме Visual State Manager (VSM), появившемся в .NET 4.0. Наконец, будет показано, как использовать редактор анимации Expression Blend для определения переходов между состояниями, и увидите, как включать специальные элементы управления в более крупные WPF-приложения. Роль свойств зависимости Подобно любому API-интерфейсу .NET, внутри реализации WPF используются все члены системы типов .NET (классы, структуры, интерфейсы, делегаты, перечисления) и каждый член типа (свойства, методы, события, константные данные, поля только для чтения и т.п.). Однако WPF также поддерживает уникальную программную концепцию под названием свойства зависимости (dependency property).
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1171 На заметку! Свойства зависимости — это также часть API-интерфейса Window Workflow Foundation (WF). Хотя свойство зависимости WF конструируется способом, аналогичным свойству зависимости WPF, при реализации в WF используется другой стек технологий. Подобно "нормальному" свойству .NET (которое в литературе, посвященной WPF, часто называют свойством CLR (CLR property)), свойства зависимости могут устанавливаться декларативно, с использованием XAML или программно в файле кода. Более того, свойства зависимости (подобно свойствам CLR) в конечном итоге предназначены для инкапсуляции полей данных класса и могут быть сконфигурированы как доступные только для чтения, только для записи или для чтения и записи. Что более интересно: почти в любом случае вы будете пребывать в счастливом неведении относительно того, что на самом деле устанавливаете (или читаете) свойство зависимости, а не свойство CLR! Например, свойства Height и Width, которые элементы управления WPF наследуют от FrameworkElement, а также член Content, унаследованный от ControlContent — все это фактически свойства зависимости: <!— Установить три свойства зависимости --> <Button x:Name = "btnMyButton" Height = 0" Width = 00" Content = "OK"/> С учетом всех этих сходств возникает вопрос: зачем нужно было определять в WPF новый термин для такой знакомой концепции? Причина кроется в способе реализации свойства зависимости внутри класса. Ниже будет показан пример кодирования; однако на высшем уровне все свойства зависимости создаются следующим образом. • Прежде всего, класс, определяющий свойство зависимости, должен иметь в своей цепочке наследования DependencyObject. • Единственное свойство зависимости представляется, как общедоступное, статическое, предназначенное только для чтения поле в классе типа DependencyProperty. По существующему соглашению имя этого поля формируется добавлением слова Property к имени оболочки CLR (см. последний пункт этого списка). • Переменная DependencyProperty зарегистрирована с помощью статического вызова DependencyProperty.Register (), что обычно происходит в статическом конструкторе или встроенным образом, при объявлении переменной. • Наконец, класс определит дружественное к XAML свойство CLR, которое осуществляет вызовы методов, предоставленных DependencyObject, для получения и установки значения. Будучи реализованными, свойства зависимости представляют множество мощных средств, используемых различными технологиями WPF, включая привязку данных, службы анимации, стили и т.д. В основе своей мотивация свойств зависимости заключается в предоставлении способа вычисления значения свойства на основе значений других источников. Ниже приведен список некоторых основных преимуществ, которые выходят за рамки простой инкапсуляции данных, имеющейся в свойстве CLR. • Свойства зависимости могут наследовать свои значения от XAML-определения родительского элемента. Например, если вы определите значение атрибута FontSize в открывающем дескрипторе <Window>, то все элементы управления в этом Window будут по умолчанию иметь этот размер шрифта. • Свойства зависимости поддерживают возможность иметь значения, установленные элементами, находящимися в области видимости XAML, такие как установка Button свойства Dock родительской DockPanel. (Вспомните из главы 28, что присоединяемые свойства делают именно это, поскольку присоединяемые свойства — это разновидность свойств зависимости.)
1172 Часть VI. Построение настольных пользовательских приложений с помощью WPF • Свойства зависимости позволяют WPF вычислять значение на основе нескольких внешних значений, что может быть очень важно для анимации и служб привязки данных. • Свойства зависимости предоставляют инфраструктуру поддержки для триггеров WPF (также довольно часто используемых при работе с анимацией и привязкой данных). Запомните, что во многих случаях вы будете взаимодействовать с существующим свойством зависимости в манере, идентичной нормальному свойству CLR (благодаря оболочке XAML). Однако когда в главе 28 шла речь о привязке данных, вы узнали, что если нужно установить привязку данных в коде, то следует вызвать метод SetBindingO на целевом объекте операции и указать свойство зависимости, которым он будет оперировать: private void SetBindings () { Binding b = new Binding(); b.Converter = new MyDoubleConverter(); b.Source = this.mySB; b.Path = new PropertyPath("Value"); // Указать свойство зависимости. this.labelSBThumb.SetBinding(Label.ContentProperty, b) ; } Подобный код также применялся, когда в предыдущей главе шла речь о запуске анимации в коде: // Указать свойство зависимости. rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim); Единственный случай, когда может понадобиться строить собственное специальное свойство зависимости — во время разработки элемента управления WPF. Например, при построении класса UserControl с четырьмя специальными свойствами, которые должны тесно интегрироваться в API-интерфейсом WPF, они должны быть написаны с использованием логики свойств зависимости. В частности, если свойства должны быть целью привязки данных или операции анимации, если свойство должно уведомлять о своем изменении, если оно должно уметь работать, как Setter в стиле WPF, или если оно должно уметь получать свои значения от родительского элемента, то обычного свойства CLR для этого недостаточно. В случае применения обычного свойства другие программисты смогут получать и устанавливать значение, однако если они попытаются использовать такие свойства в контексте службы WPF, все будет работать не так, как ожидалось. Поскольку никогда заранее не известно, как другие пожелают взаимодействовать со свойствами специального класса UserControl, следует выработать привычку всегда определять свойства зависимости при построении специальных элементов управления. Проверка существующего свойства зависимости Прежде чем вы узнаете, как строить специальное свойство зависимости, давайте взглянем на внутреннюю реализацию свойства Height класса FrameworkElement. Соответствующий код показан ниже (с комментариями); тот же код можно просмотреть самостоятельно с помощью утилиты reflector.exe. // FrameworkElement "является" DependencyObject. public class FrameworkElement : UIElement, IFrameworklnputElement, IlnputElement, ISupportlnitialize, IHaveResources, IQueryAmbient {
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1173 // Статическое свойство только для чтения Dependency-Property. public static readonly DependencyProperty HeightProperty; // Поле DependencyProperty часто регистрируется //в статическом конструкторе класса. static FrameworkElement() HeightProperty = DependencyProperty.Register ( "Height", typeof(double), typeof(FrameworkElement) , new FrameworkPropertyMetadata((double) 1.0 / (double) 0.0, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(FrameworkElement.OnTransformDirty)), new ValidateValueCallback(FrameworkElement.IsWidthHeightValid)); } // Оболочка CLR, реализованная унаследованными // методами GetValue ()/SetValue(). public double Height { get { return (double) base.GetValue(HeightProperty); } set { base.SetValue(HeightProperty, value); } } } Как видите, свойства зависимости требуют порядочного дополнительного кода по сравнению с нормальным свойством CLR. На самом деле зависимость может оказаться еще более сложной, чем здесь показано. Прежде всего, помните, что если класс желает определить свойство зависимости, он должен иметь DependencyObject в своей цепочке наследования, поскольку в этом классе определены методы GetValue() и SetValue(), используемые оболочкой CLR. Поскольку FrameworkElement "является" ("is-a") DependencyObject, это требование удовлетворено. Далее вспомните, что сущность, которая будет хранить действительное значение свойства (в случае Height это double), представлено как общедоступное, статическое; предназначенное только для чтения поле типа DependencyProperty. В соответствии с существующим соглашением имя этого свойства всегда должно быть снабжено суффиксом Property, добавленным к имени связанной оболочки CLR, как показано ниже: public static readonly DependencyProperty HeightProperty; Учитывая, что свойства зависимости объявлены как статические поля, они обычно создаются (и регистрируются) внутри статического конструктора класса. Объект DependencyProperty создается вызовом статического метода DependencyProperty. Register(). Этот метод многократно переопределен; однако в случае Height метод DependencyProperty.Register () вызывается следующим образом: HeightProperty = DependencyProperty.Register( "Height", typeof(double), typeof(FrameworkElement), new FrameworkPropertyMetadata((doubleH.0, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(FrameworkElement.OnTransformDirty)), new ValidateValueCallback(FrameworkElement.IsWidthHeightValid));
1174 Часть VI. Построение настольных пользовательских приложений с помощью WPF Первый аргумент DependencyProperty.Register () — это имя нормального свойства CLR класса (в данном случае Height), в то время как второй аргумент несет информацию о лежащем в основе типе инкапсулированных данных (double). Третий аргумент указывает информацию о типе класса, к которому относится это свойство (в данном случае — FrameworkElement). Хотя это может показаться избыточным (в конце концов, поле HeightProperty утке определено внутри класса FrameworkElement), это очень разумный аспект WPF, который позволяет одному классу регистрировать свойства на другом (даже если определение класса запечатано (sealed)). Четвертый аргумент, переданный DependencyProperty.RegisterO в данном примере — это то, что на самом деле обеспечивает свойствам зависимости их уникальные характеристики. Здесь передается объект FrameworkPropertyMetadata, описывающий различные детали о том, как среда WPF должна обрабатывать это свойство в отношении уведомлений обратного вызова (если свойство должно извещать других об изменениях своего значения) и различные опции (представленные перечислением FrameworkPropertyMetadataOptions). Значения FrameworkPropertyMetadataOptions управляют тем, что именно затрагивается данным свойством (работает ли оно с привязкой данных, может ли наследоваться, и т.п.). В данном случае аргументы конструктора FrameworkPropertyMetadata описываются следующим образом: new FrameworkPropertyMetadata( // Значение свойства по умолчанию. (doubleH.0, // Опции метаданных. FrameworkPropertyMetadataOptions.AffectsMeasure, // Делегат, указывающий на свойство, вызываемое при изменении свойства. new PropertyChangedCallback(FrameworkElement.OnTransformDirty) ) Поскольку финальный аргумент конструктора FrameworkPropertyMetadata является делегатом, обратите внимание, что этот параметр конструктора указывает На статический метод класса FrameworkElement по имени OnTransformDirty(). Хотя код этого метода подробно рассматриваться не будет, имейте в виду, что всякий раз, когда строится специальное свойство зависимости, можно специфицировать делегат PropertyChangeCallback для указания на метод, который будет вызван, когда изменяется значение свойства. Это приводит к финальному параметру, переданному в метод DependencyProperty. Register () — второму делегату типа ValidateVakueCallback, который указывает На метод класса FrameworkElement, вызываемый для проверки достоверности значения, присвоенного свойству: new ValidateValueCallback(FrameworkElement.IsWidthHeightValid) Этот метод содержит логику, которую можно ожидать найти в блоке set свойства (подробнее об этом рассказывается в следующем разделе): private static bool IsWidthHeightValid(object value) { double num = (double) value; return ((!DoubleUtil.IsNaN(num) && (num >= 0.0)) && !double.IsPositivelnfinity(num)); } Как только объект DependencyProperty зарегистрирован, остается решить последнюю задачу — поместить поле в оболочку обычного свойства CLR (в данном случае — Height). Однако обратите внимание, что блоки get и set не просто возвращают или устанавливают значение double переменной-члена класса, но делают это непря-
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1175 мо, используя методы GetValueO и SetValueO из базового класса System.Windows. DependencyObject: public double Height { get { return (double) base.GetValue(HeightProperty); } set { base.SetValue(HeightProperty, value); } } Важные замечания относительно оболочек свойств CLR Резюмируя сказанное выше, отметим, что свойства зависимости выглядят как обычные нормальные свойства, когда вы получаете или устанавливаете их значения в XAML- разметке или в коде, но "за кулисами" они реализованы с применением намного более изощренной техники кодирования. Помните, что основная причина для прохождения этого процесса состоит в построении специального элемента управления, имеющего специальные свойства, которые должны быть интегрированы со службами WPF, требующими взаимодействия со свойством зависимости (например, анимацией, привязкой данных и стилями). Несмотря на то что часть реализации свойства зависимости включает определение оболочки CLR, вы никогда не должны помещать логику проверки достоверности в блок set. И потому оболочка CLR свойства зависимости никогда не должна делать ничего помимо вызовов GetValueO или SetValueO. Причина в том, что исполняющая среда WPF сконструирована таким образом, что в случае написания XAML-разметки, устанавливающей свойство, как показано ниже: <Button x:Name="myButton" Height=00" .../> исполняющая среда полностью минует блок set свойства Height и непосредственно вызывает SetValueO! Причина столь странного поведения кроется в оптимизации. Если бы исполняющей среде WPF пришлось непосредственно вызывать блок set свойства Height, то ей нужно было бы выполнить рефлексию времени выполнения для нахождения поля DependencyProperty (указанного в первом аргументе SetValueO), обращаться к нему в памяти, и т.д. Но раз так, то зачем вообще строить оболочку CLR? Дело в том, что XAML в WPF не позволяет вызывать функции в разметке, так что следующий фрагмент был бы ошибочным: <!-- Ошибка1 Вызывать методы в XAML-разметке для WPF нельзя! --> <Button x:Name="myButton" this.SetValue(00" ) .../> Установка или получение значения в разметке с использованием оболочки CLR должна восприниматься как способ указать исполняющей среде WPF о необходимости вызвать GetValue ()/SetValue (), поскольку напрямую это делать в разметке не разрешается. Что произойдет, если вызвать оболочку CLR в коде следующим образом? Button b = new Button () ; b.Height =10; В этом случае, если бы блок set свойства Height содержал какой-то код помимо вызова SetValueO, он должен был бы выполняться, так как оптимизация анализатора WPF XAML не вызывается. Краткий ответ может быть сформулирован так: когда вы регистрируете свойство зависимости, применяйте делегат ValidateValueCallback для указания на метод, выполняющий проверку достоверности данных. Это гарантирует правильное поведение, независимо от того, используется XAML или код для получения/ установки свойства зависимости.
1176 Часть VI. Построение настольных пользовательских приложений с помощью WPF Построение специального свойства зависимости Если к настоящему возникла небольшая путаница, то это совершенно нормально. Построение свойств зависимости может требовать некоторого времени на привыкание. Однако, как бы то ни было, это часть процесса построения многих специальных элементов управления WPF, так что давайте посмотрим, как строится свойство зависимости. Начните с создания приложения WPF по имени CustomDepPropApp. Выберите пункт меню Projects Add New Item (Проекте Добавить новый элемент), укажите в качестве шаблона User Control (WPF) (Пользовательский элемент управления (WPF))), и назначьте ему имя ShowNumberControl.xaml (рис. 31.1). Т | , Resource Dictionary (WPF) Visual C# jT About Box Visual C# . A, ADO.NET Entity Data Model Visual C# Рис. 31.1. Вставка нового специального элемента UserControl На заметку! Дополнительные сведения о классе UserControl из WPF представлены далее в главе. Как и окно, типы WPF UserControl имеют ассоциированный с ними файл XAML и связанный файл кода. Обновим XAML-разметку пользовательского элемента управления, добавив элемент управления Label в Grid: <UserControl x:Class="CustomDepPropApp.ShowNumberControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight=,,300" d:DesignWidth=00"> <Grid> <Label x:Name=,,numberDisplay" Height=0" Width=00" Васkground="LightBlue"/> </Grid> </UserControl> В файле кода этого специального элемента управления создайте обычное свойство .NET, которое упаковывает int и устанавливает новое значение для свойства Content элемента Label:
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1177 public partial class ShowNumberControl : UserControl { public ShowNumberControl () { InitializeComponent(); } // Нормальное свойство .NET private int currNumber = 0; public int CurrentNumber { get { return currNumber; } set { currNumber = value; numberDisplay .Content = CurrentNumber .ToStnng () ; } } Теперь обновите определение XAML окна, чтобы объявить экземпляр специального элемента управления внутри диспетчера компоновки StackPanel. Поскольку специальный элемент управления — не часть основного стека сборок WPF, потребуется определить специальное пространство имен XML, которое отобразится на этот элемент (см. главу 27). Ниже показана необходимая разметка: <Window х:Class="CustomDepPropApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns : myCtrls="clr-namespace: CustomDepPropApp" Title="Simple Dependency Property App" Height=50" Width=50" WindowstartupLocation="CenterScreen"> <StackPanel> <myCtrls:ShowNumberControl x:Name="myShowNumberCtrl" CurrentNumber=00"/> </StackPanel> </Window> Как видите, визуальный конструктор Visual Studio 2010 вроде бы корректно отображает значение, установленное в свойстве CurrentNumber (рис. 31.2). «ПК Рис. 31.2. Свойство выглядит вполне работоспособным А что если необходимо применить объект анимации к свойству CurrentNumber, чтобы значение изменялось от 100 до 200 за период в 10 секунд? Желая сделать это в разметке, можно было бы обновить раздел <myCtrls:ShowNumberControl> следующим образом: <myCtrls : ShowNumberControl х : Name=llmyShowNumberCtrl" CurrentNumber=" 100"> <myCtrls : ShowNumberControl. Tnggers>
1178 Часть VI. Построение настольных пользовательских приложений с помощью WPF <EventTrigger RoutedEvent = "myCtrls:ShowNumberControl.Loaded"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard TargetProperty = "CurrentNumber"> <Int32Animation From = 00" To = 00" Duration = :0:10"/> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </myCtrls:ShowNumberControl.Triggers> </myCtrls:ShowNumberControl> Если попробовать запустить это приложение, то объект анимации не сможет найти правильную цель, и потому будет проигнорирован. Причина в том, что свойство CurrentNumber не зарегистрировано как свойство зависимости! Чтобы исправить это, вернемся к файлу кода специального элемента управления и полностью закомментируем текущую логику свойства (включая приватное поле заднего плана). Теперь поместите курсор внутрь области класса и введите фрагмент кода propdp (рис. 31.3). ShowNumberControUamkcs* X I Э ^CustomDepPropApp.ShowNurnberControl ; InitializeComponent(); ; •% DependencyPropertyHelper ; *t$ DependencyPropertyKey \ И$ FrameworkPropertyMetadata :-jrf" FrameworkPropertyMetadataOptions ^3 prop JS| propa ..] propdp ; { ) Properties :J| PropertyChangedCaltback m _ IStringOj Рис. 31.3. Фрагмент кода progdp предоставляет начальную точку для построения свойства зависимости После ввода propdp нажмите два раза клавишу <ТаЬ>. Фрагмент кода развернется, предоставив базовый скелет свойства зависимости (рис. 31.4). ShowNurnberControljcamt.cs* X |g| ш шт *t% CustomDepPropApp.ShowNu mberControf ['JfMyProperty public int MyPropertyj { get { return (int)GetValue(HyPrcpertyProperty)j } set { SetValue(rtyPropertyProp€rty, value); } } // Using a DependencyProperty as the backing store for HyProperty. This enables animation, styling, binding, public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerxlpss)., new UIPropertyMetadata@))j Рис. 31.4. Развернутый фрагмент Простейшая версия свойства CurrentNumber будет выглядеть так: public partial class ShowNumberControl : UserControl
Глава 31, Шаблоны элементов управления WPF и пользовательские элементы управления 1179 public int CurrentNumber { get { return (int)GetValue(CurrentNumberProperty); } set { SetValue(CurrentNumberProperty, value); } } public static readonly DependencyProperty CurrentNumberProperty = DependencyProperty.Register("CurrentNumber", typeof(int) , typeof(ShowNumberControl) , new UIPropertyMetadata@)); } Это подобно тому, что вы видели в реализации свойства Height; однако фрагмент кода регистрирует свойство встроенным способом, а не в статическом конструкторе (что хорошо). Также обратите внимание, что объект UIPropertyMetadata используется для определения целого значения по умолчанию @) вместо более сложного объекта FrameworkPropertyMetadata. Добавление процедуры проверки достоверности данных Хотя теперь есть свойство зависимости по имени CurrentNumber, все же анимация в действии не видна. Следующая необходимая поправка, которая может понадобиться, состоит в указании функции для выполнения некоторой логики проверки достоверности. Для целей данного примера предположим, что нужно обеспечить, чтобы значение свойства CurrentNumber находилось в пределах от 0 до 500. Для этого добавьте финальный аргумент к методу DependencyProperty. Register () типа ValidateValueCallback, указывающий на метод по имени ValidateCurrentNumber. ValidateValueCallback — это делегат, который может только указывать на методы, возвращающие bool и принимающие object в качестве единственного аргумента. Этот object представляет новое значение, которое присваивается. Реализуйте ValidateCurrentNumber для возврата true, если входящее значение находится в заданном диапазоне, и false — в противном случае: public static readonly DependencyProperty CurrentNumberProperty = DependencyProperty.Register("CurrentNumber", typeof(int), typeof(ShowNumberControl) , new UIPropertyMetadataA00), new ValidateValueCallback(ValidateCurrentNumber)); public static bool ValidateCurrentNumber(object value) { // Простое бизнес-правило: значение должно лежать между 0 и 500. if (Convert.ToInt32(value) >= 0 && Convert.ToInt32(value) <= 500) return true; else return false; } Реакция на изменение свойства Итак, допустимое число уже имеется, но все еще нет анимации. Последнее изменение, которое потребуется внести — это задать второй аргумент конструктора UIPropertyMrtadata, которым является PropertyChangedCallback. Этот делегат может указывать на любой метод, принимающий DependencyObject в качестве первого параметра и DependencyPropertyChangeEventArgs — в качестве второго. Сначала обновите код следующим образом:
1180 Часть VI. Построение настольных пользовательских приложений с помощью WPF // Обратите внимание на второй параметр конструктора UIPropertyMetadata. public static readonly DependencyProperty CurrentNumberProperty = DependencyProperty.Register("CurrentNumber", typeof(int), typeof(ShowNumberControl), new UIPropertyMetadataA00, new PropertyChangedCallback(CurrentNumberChanged)), new ValidateValueCallback(ValidateCurrentNumber)); Конечной целью внутри метода CurrentNumberChamgedO является изменение свойства Content объекта Label на новое значение, присвоенное свойством CurrentNumber. Однако здесь присутствует серьезная проблема: метод CurrentNumberChanged() является статическим, поскольку он должен работать со статическим объектом DependencyProperty. Тогда как же получить доступ к Label для текущего экземпляра ShowNumberControl? Эта ссылка находится в первом параметре DependencyObject. Новое значение можно найти, используя входные аргументы события. Ниже показан необходимый код, который изменит свойство Content объекта Label: private static void CurrentNumberChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs args) { // Привести DependencyObject к ShowNumberControl. ShowNumberControl с = (ShowNumberControl)depObj; // Получить элемент Label в ShowNumberControl. Label theLabel = с.numberDisplay; // Установить в Label новое значение. theLabel.Content = args.NewValue.ToString(); } Какой длинный путь пришлось пройти, чтобы всего лишь изменить содержимое метки! Однако преимущество в том, что свойство зависимости CurrentNumber теперь может быть целью для стиля WPF, объекта анимации, операций привязки данных и т.д. На рис. 31.5 показано завершенное приложение (теперь оно действительно меняет значение во время выполнения). Рис. 31.5. Наконец-то анимация работает! На этом обзор свойств зависимости WPF завершен. Имейте в виду, что хотя вы получили определенное представление о назначении этих конструкций, многие детали не были раскрыты. Если вы окажетесь в ситуации, когда нужно строить множество специальных элементов управления, поддерживающих специальные свойства, загляните в подраздел "Properties" ("Свойства") узла "WPF Fundamentals" ("Основы WPF") документации .NET Framework 4.0 SDK. Там вы найдете намного больше примеров построения свойств зависимости, присоединяемых свойств, различные способы конфигурирования метаданных и массу других деталей. Исходный код. Проект CustomDepPropApp доступен в подкаталоге Chapter 31.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1181 Маршрутизируемые события Свойства — это не единственная программная конструкция .NET, которая требует настройки, чтобы хорошо работать с API-интерфейсом WPF. Стандартная модель событий CLR также претерпела некоторые усовершенствования, чтобы обеспечить обработку событий в такой манере, которая подходит к описанию XAML дерева объектов. Предположим, что имеется новый проект приложения WPF по имени WPFRoutedEvents. Модифицируйте описание XAML начального окна, добавив следующий элемент управления <Button>, в котором определено некоторое сложное содержимое: <Button Name="btnClickMe" Height=5" Width = 50" Click ="btnClickMe_Clicked"> <StackPanel Orientation ="Horizontal"> <Label Height=0" FontSize =0">Fancy Button!</Label> <Canvas Height =0" Width =00" > <Ellipse Name = "outerEllipse" Fill ="Green" Height =5" Width =0" Cursor="Handn Canvas.Left=5" Canvas.Top=2"/> <Ellipse Name = "innerEllipse" Fill ="Yellow" Height = 5" Width =6" Canvas.Top=7" Canvas.Left=2"/> </Canvas> </StackPanel> </Button> Обратите внимание, что в открывающем определении <Button> задается обработка события Click за счет указания имени метода, который должен быть вызван по наступлению события. Событие Click работает с делегатом RoutedEventHandler, который ожидает обработчика события, принимающего object в первом параметре и System.Winodws. RoutedEventArgs — во втором. Реализуем этот обработчик следующим образом: public void btnClickMe_Clicked(object sender, RoutedEventArgs e) { // Действия по щелчку на кнопке. MessageBox.Show("Clicked the button"); } Если вы запустите приложение, то увидите это окно сообщения, независимо от того, на какой части содержимого кнопки был совершен щелчок (зеленый Ellipse, желтый Ellipse, Label или поверхность Button). Это хорошо. Представьте, насколько громоздким была бы обработка событий WPF, если бы пришлось обрабатывать событие Click для каждого подэлемента. И дело не только в том, что создание отдельных обработчиков событий для каждого аспекта Button было бы трудоемкой задачей, но был бы получен сложный и громоздкий код, который требовал бы трудоемкого сопровождения. К счастью, маршрутизируемые события WPF позволяют устроить все так, чтобы единый обработчик события Click вызывался автоматически, независимо от того, на какую часть кнопки пришелся щелчок. Проще говоря, модель маршрутизируемых событий автоматически распространяет события верх (или вниз) по дереву объектов в поисках подходящего обработчика. Точнее говоря, маршрутизируемое событие может использовать три стратегии маршрутизации. Если событие распространяется от исходной точки во внешние контексты внутри дерева объектов, говорят, что это пузырьковое событие (bubbling event). В противоположность этому, если событие распространяется от внешнего элемента (например, Window) вниз к исходной точке, такое событие называется туннелируемым (tunneling event). Наконец, если событие инициируется и обрабатывается только исходным элементом (что может быть описано как нормальное событие CLR), говорят, что это прямое событие (direct event).
1182 Часть VI. Построение настольных пользовательских приложений с помощью WPF Роль маршрутизируемых пузырьковых событий В текущем примере если пользователь щелкает на внутреннем желтом элементе Ellipse, событие Click распространяется на следующий уровень (Canvas), затем на StackPanel и, наконец, на Button, где событие Click и обрабатывается. Аналогичным образом, если пользователь щелкает на Label, событие распространяется в StackPanel и, наконец, попадает в элемент Button. Благодаря этому шаблону "пузырькового" распространения маршрутизируемого события, не нужно беспокоиться о регистрации определенных обработчиков событий Click для всех членов составного элемента управления. Однако если требуется организовать специальную логику обработки щелчков для множества элементов внутри одного дерева объектов, то это можно сделать. Для целей иллюстрации предположим, что необходимо обработать щелчок на элементе управления outerEllipse в уникальной манере. Сначала обработайте событие MouseDown для этого подэлемента (графически визуализированные типы вроде Ellipse не поддерживают событие щелчка, но могут отслеживать активность кнопки мыши через MouseDown, MouseUp и т.п.): <Button Name="btnClickMe" Height=5" Width = 50" Click ="btnClickMe_Clicked"> <StackPanel Orientation ="Horizontal"> <Label Height=0" FontSize =0">Fancy Button!</Label> <Canvas Height =0" Width =00" > <Ellipse Name = "outerEllipse" Fill ="Green" Height =5" MouseDown ="outerEllipse_MouseDown" Width =0" Cursor="Hand" Canvas.Left=5" Canvas.Top=2"/> <Ellipse Name = "innerEllipse" Fill ="Yellow" Height = 5" Width =6" Canvas.Top=7" Canvas.Left=2"/> </Canvas> </StackPanel> </Button> Затем реализуйте соответствующий обработчик событий, который в целях демонстрации просто изменяет свойство Title главного окна: public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e) { // Изменить заголовок окна. this.Title = "You clicked the outer ellipse1"; } Теперь можно выполнять различные действия в зависимости от того, на чем конкретно щелкнул конечный пользователь (от внешнего эллипса и далее — в любом месте внутри области кнопки). На заметку! Маршрутизируемые пузырьковые события всегда движутся от исходной точки до следующего определяющего контекста. Поэтому в данном примере щелчок на объекте innerEllipse приводит к распространению события в Canvas, а не в outerEllipse, поскольку оба они относятся к типу Ellipese внутри области Canvas. Продолжение или прекращение пузырькового распространения В настоящее время, если пользователь щелкнет на объекте outerEllipse, инициируется зарегистрированный обработчик событий MouseDown для данного объекта Ellipse, и в этот момент событие распространится в событие Click кнопки. Чтобы прекратить пузырьковое распространение по дереву объектов, для этого нужно установить свойство Handled параметра RoutedEventArgs в true:
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1183 public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e) { // Изменение заголовка окна. this.Title = "You clicked the outer ellipse!"; // Прекратить пузырьковое распространение! e.Handled = true; } В этом случае обнаруживается, что заголовок окна изменился, но окно сообщения, отображаемое вызовом MessageBox в обработчике события Click элемента Button, не отобразится. По сути, маршрутизируемые пузырьковые события позволяют сложной группе содержимого действовать как единый логический элемент (например, Button) или как дискретные элементы (например, Ellipse внутри Button). Роль маршрутизируемых туннелируемых событий Строго говоря, маршрутизируемые события могут быть по природе своей пузырьковыми (как только что было описано) или туннелируемыми. Туннелируемые события (которые все начинаются с префикса Preview, например, PreviewMouseDown) спускаются вниз от элемента верхнего уровня во вложенные контексты дерева объектов. В общем случае, каждому пузырьковому событию в библиотеках базовых классов WPF соответствует связанное туннелируемое событие, которое инициируется перед его пузырьковым дополнением. Например, перед инициацией пузырькового события MouseDown сначала происходит туннелируемое событие PreviewMouseDown. Обработка туннелируемых событий выглядит как обработка любых других событий; вы просто назначаете имя обработчика события в XAML (или, если необходимо, используете соответствующий синтаксис обработки событий С# в файле кода) и реализуете этот обработчик в файле кода. Чтобы продемонстрировать взаимодействие туннелируемых и пузырьковых событий, начните с обработки события PreviewMouseDown для объекта outerEllipse: <Ellipse Name = "outerEllipse" Fill ="Green" Height =5" MouseDown ="outerEllipse_MouseDown" PreviewMouseDown ="outerEllipse_PreviewMouseDown" Width =0" Cursor="Hand" Canvas.Left=5" Canvas.Top=2"/> Затем пересмотрите текущее определение класса С#, обновляя каждый обработчик событий (для всех объектов) добавлением данных о текущем событии в переменную-член типа string по имени mouseActivity, используя входящий объект аргументов события. Это позволит наблюдать поток инициации событий в фоновом режиме. public partial class MainWindow : Window { string mouseActivity = string.Empty; public MainWindow() { InitializeComponent(); } public void btnClickMe_Clicked(object sender, RoutedEventArgs e) { AddEventlnfo(sender, e) ; MessageBox.Show(mouseActivity, "Your Event Info"); // Очистить строку для следующего использования. mouseActivity = ""; } private void AddEventlnfo(object sender, RoutedEventArgs e) { mouseActivity += string.Format(
1184 Часть VI. Построение настольных пользовательских приложений с помощью WPF "{0} sent a {1} event named {2}.\n", sender, e.RoutedEvent.RoutingStrategy, e.RoutedEvent.Name); } private void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e) { AddEventlnfo(sender, e) ; } private void outerEllipse_PreviewMouseDown( object sender, MouseButtonEventArgs e) { AddEventlnfo(sender, e); } } Обратите внимание, что ни в одном обработчике не прекращается пузырьковое распространение события. Запустив это приложение, вы увидите уникальное окно сообщения, зависящее от того, где был произведен щелчок на кнопке. На рис. 31.6 показан результат щелчка на внешнем объекте Ellipse. Рис. 31.6. Сначала туннелированное, а потом — пузырьковое распространение Итак, почему же события WPF обычно идут парами (одно туннелируемое и одно пузырьковое)? Ответ заключается в том, что за счет предварительного отслеживания событий вы получаете возможность выполнять любую специфическую логику (проверку достоверности данных, отключение пузырьковых действий и т.д.), прежде чем будет инициирован пузырьковый аналог события. Для примера предположим, что имеется элемент Text Box, который должен допускать ввод только числовых данных. В обработчике события PreviewKeyDown, если пользователь ввел какие-то нечисловые данные, можно отменить пузырьковое событие, установив свойство Handled в true. При построении специального элемента управления, который содержит специальные события, событие можно написать таким образом, чтобы оно могло распространяться, как пузырьковое (или туннелируемое) по дереву XAML. В настоящей главе создание специальных маршрутизируемых событий подробно не рассматриваются (правда, процесс не так уж сильно отличается от построения специального свойства зависимости). Если интересно, загляните в раздел "Routed Events Оггегаеш" ("Обзор маршрутизируемых событий") документации .NET Framework 4.0 SDK. Там вы найдете множество полезных подсказок. Исходный код. Проект WPFRoutedEvents доступен в подкаталоге Chapter 31.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1185 Логические деревья, визуальные деревья и шаблоны по умолчанию Есть еще несколько тем, которые следует рассмотреть, прежде чем приступать к изучению построения специальных элементов управления. В частности, необходимо понять разницу между логическим деревом, визуальным деревом и шаблоном по умолчанию. При вводе XAML-разметки в Visual Studio 2010, Expression Blend либо таком инструменте, как Kaxaml, разметка становится логическим представлением документа XAML. Точно также, при написании кода С#, добавляющего новые элементы к элементу управления StackPanel, новые элементы вставляются в логическое дерево. По сути, логическое представление показывает, как содержимое будет позиционировано внутри различных диспетчеров компоновки для главного окна (или другого корневого элемента, такого как Page или NavigationWindow). Тем не менее, за каждым логическим деревом стоит более сложное представление, которое называется визуальным деревом и внутренне использует WPF для корректной визуализации элементов на экране. Внутри любого визуального дерева находятся все детали шаблонов и стилей, используемых для визуализации каждого объекта, включая все необходимые рисунки, фигуры, визуальные элементы и анимации. Полезно знать разницу между логическим и визуальным деревьями, потому что когда строится специальный шаблон элемента управления, то, по сути, заменяется все или часть визуального дерева элемента управления и вставляете свое. Поэтому, если необходимо визуализировать элемент управления Button в виде звезды, можно определить новый звездообразный шаблон и включить его в визуальное дерево Button. Логически Button остается типом Button, и поддерживает все свойства, методы и события, как и ожидалось. Но визуально элемент получает совершенно новый внешний вид. Один этот факт делает WPF исключительно удобным API-интерфейсом, учитывая, что другие инструментарии потребовали бы в такой ситуации построения совершенно нового класса, представляющего звездообразную кнопку. В WPF достаточно определить новую разметку. На заметку! Элементы управления WPF часто описываются как лишенные внешнего вида (lookless). Это указывает на тот факт, что внешность элемента управления WPF полностью независима от его поведения и настраиваема. Программный просмотр логического дерева Хотя анализ логического дерева окна во время выполнения — не слишком часто применяемое программное действие в WPF, стоит упомянуть, что в пространстве имен System.Windows определен класс по имени LogicalTreeHelper, который позволяет просматривать структуру логического дерева во время выполнения. Для иллюстрации связи между логическими деревьями, визуальными деревьями и шаблонами элементов управления создайте новое приложение WPF по имени TreesAndTemplatesApp. Модифицируйте разметку окна, чтобы она содержала два элемента управления Button и большой доступный только для чтения элемент Text Box с включенными линейками прокрутки. Воспользуйтесь IDE-средой для обработки события Click каждой кнопки. Ниже показана необходимая XAML-разметка. <Window х:Class="TreesAndTemplatesApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://cchemas.microsoft.com/winfx/2006/xaml" Title="Fun with Trees and Templates" Height=18" Width="83 6" WindowstartupLocation="CenterScreen">
1186 Часть VI. Построение настольных пользовательских приложений с помощью WPF <DockPanel LastChildFill="TrueII> <Border Height=0" DockPanel. Dock=MTopM BorderBrush=,,Blue"> <StackPanel Qrlentation="Horizontal1^ <Button x:Name="btnShowLogicalTree11 Content="Logical Tree of Window" Margin=" BorderBrush="Blue" Height=0" Click="btnShowLogicalTree_Click,,/> <Button x :Name="btnShowVisualTree11 Content="Visual Tree of Window" BorderBrush="Blue" Height= 0" Click="btnShowVisualTree_Click"/> </StackPanel> </Border> <TextBox x:Name="txtDisplayArea" Margin=0" Background="AliceBlue" IsReadOnly="True" BorderBrush="Red" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" /> </DockPanel> </Window> Внутри файла кода С# определите переменную-член типа string по имени dataToShow. Теперь в обработчике Click объекта btnShowLogicalTree вызовите вспомогательную функцию, которая будет вызывать себя рекурсивно для наполнения строковой переменной логическим деревом Window. Для этого применяется статический метод GetChildrenO объекта LogicalTreeHelper. Ниже показан код. private void btnShowLogicalTree_Click (object sender, RoutedEventArgs e) { dataToShow = ""; BuildLogicalTree@, this); this.txtDisplayArea.Text = dataToShow; } void BuildLogicalTree(int depth, object obj ) { // Добавить имя типа к переменной-члену dataToShow. dataToShow += new string (' ', depth) + obj.GetType () .Name + "\n"; // Если элемент - не DependencyObject, пропустить его. if ( ' (obj is DependencyObject)) return; // Выполнить рекурсивный вызов для каждого дочернего элемента логического дерева foreach (object child in LogicalTreeHelper.GetChildren( obj as DependencyObject)) BuildLogicalTree(depth + 5, child); } Запустив приложение и щелкнув на кнопке Logical Tree of Window (Логическое дерево окна), вы увидите вывод дерева в текстовой области — почти точную копию первоначальной XAML-разметки (рис. 31.7). MainWindow DockPanef Border StackPanel Button String Button TextBox String Рис. 31.7. Просмотр логического дерева во время выполнения
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1187 Программный просмотр визуального дерева Визуальное дерево Window также можно просмотреть во время выполнения с использованием класса VisualTreeHelper из пространства имен System.Windows.Media. Ниже приведена реализация обрабочика события Click для второго элемента управления Button (btnShowVisualTree), которая выполняет аналогичную рекурсивную логику для построения текстового представления визуального дерева: private void btnShowVisualTree_Click(object sender, RoutedEventArgs e) { dataToShow = ""; BuildVisualTree@, this); this.txtDisplayArea.Text = dataToShow; } void BuildVisualTree (int depth, DependencyObject obj) { // Добавить имя типа к переменной-члену dataToShow. dataToShow += new string(' ' , depth) + obj.GetType () .Name + "\n"; // Выполнить рекурсивный вызов для каждого дочернего элемента визуального дерева for (int i=0; i < VisualTreeHelper.GetChildrenCount(obj); i++) BuildVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i) ) ; } Как показано на рис. 31.8, в визуальном дереве присутствует множество низкоуровневых агентов визуализации, таких как ContentPresenter, AdornerDecorator, TextBoxLineDrawingVisual и т.д. Main Window ,• Border AdornerDecorator ContentPresenter DockPanel Border StackPanel Button ButtonChrome ContentPresenter TextBlock Button ButtonChrome ContentPresenter TextBlock TextBox UstBoxChrome ScrollVisewer Grid Rectangle ScrollContentPresenter TextBoxView TextBoxLineDrawingVisual AdornerLayer ScrollBar ScrollBar AdornerLayer Рис. 31.8. Просмотр визуального дерева во время выполнения
1188 Часть VI. Построение настольных пользовательских приложений с помощью WPF Программный просмотр шаблона по умолчанию для элемента управления Вспомните, что визуальное дерево используется WPF для определения, каким образом визуализировать Window и все содержащиеся элементы. Каждый элемент управления WPF сохраняет собственный набор команд внутри своего шаблона по умолчанию. С программной точки зрения любой шаблон может быть представлен как экземпляр класса ControlTemplate. Кроме того, элемент управления имеет шаблон по умолчанию, который можно получить с использованием свойства Template: // Получить шаблон по умолчанию элемента Button. Button myBtn = new Button (); ControlTemplate template = myBtn.Template; Аналогично, можно создать новый объект ControlTemplate в коде и подключить его к свойству Template элемента управления: // Подключить новый шаблон для кнопки. Button myBtn = new Button (); ControlTemplate customTemplate = new ControlTemplate(); // Предполагается, что этот метод добавит весь код для шаблона звезды. MakeStarTemplate(customTemplate); myBtn.Template = customTemplate; Хотя можно было бы построить новый шаблон в коде, намного чаще это делается в XAML-разметке. Фактически, Expression Blend имеет огромное количество инструментов, которые можно использовать для определения шаблона с минимальными усилиями. Однако прежде чем приступить к построению собственных шаблонов, давайте завершим текущий пример и добавим возможность просмотра шаблона по умолчанию элемента управления WPF во время выполнения. Это может оказаться действительно удобным способом увидеть общую композицию шаблона. Для начала обновите разметку окна добавлением нового диспетчера компоновки StackPanel, стыкованного к левой стороне главной панели DockPanel, как показано ниже: <Border DockPanel. Dock="Lef t" Margin=011 BorderBrush="DarkGreen11 BorderThickness=11 Width=58"> <StackPanel> <Label Content="Enter Full Name of WPF Control" Width=40" FontWeight="DemiBold" /> <TextBox x:Name=,,txtFullName" Width=40" BorderBrush=,,Green" Background="BlanchedAlmond11 Height=ll22" Text="System.Windows.Controls.Button" /> <Button x:Name="btnTemplate" Content=ul5ee Template" BorderBrush="Green" Height=0" Width=00" Margin=" Click="btnTemplate_Click" HorizontalAlignment="Left" /> <Border BorderBrush="DarkGreen" BorderThickness=" Height=60" Width=01" Margin=0" Background="LightGreen" > <StackPanel x:Name="stackTemplatePanel" /> </Border> </StackPanel> </Border> Обратите внимание на пустой элемент StackPanel по имени stackTemplatePanel, поскольку на него будут осуществляться ссылки в коде. На рис. 31.9 показано, как примерно должно выглядеть окно.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1189 See Template Рис. 31.9. Обновленный пользовательский интерфейс окна Текстовая область вверху слева позволяет вводить полностью квалифицированное имя элемента управления WPF, находящегося в сборке PresentationFramework.dll. После загрузки библиотеки экземпляр объекта динамически загружается и отображается в большом квадрате внизу слева. И, наконец, шаблон по умолчанию элемента управления будет отображаться в текстовой области справа. Добавьте в свой класс С# новую переменную-член типа Control: private Control ctrlToExamme = null; Ниже приведен остальной код, который требует импорта пространств имен System. Reflection, System.Xmlи System.Windows.Markup: private void btnTemplate_Click(object sender, RoutedEventArgs e) dataToShow = ""; ShowTemplate (); this.txtDisplayArea.Text = } private void ShowTemplate() dataToShow; // Удалить элемент управления, находящийся в области предварительного просмотра. if (ctrlToExamme '= null) stackTemplatePanel .Children . Remove (ctrlToExamme) ; try { // Загрузить сборку PresentationFramework и создать экземпляр // указанного элемента управления. Задать его размер для отображения, // затем добавить пустую панель StackPanel. Assembly asm = Assembly.Load("PresentationFramework, Version=4.0.0.0," + "Culture=neutral, PublicKeyToken=31bf3856ad364e35"); ctrlToExamme = (Control)asm.Createlnstance(txtFullName.Text); ctrlToExamme. Height = 200; ctrlToExamme.Width = 200; ctrlToExamme .Margin = new Thickness E) ; stackTemplatePanel .Children .Add (ctrlToExamme) ;
1190 Часть VI. Построение настольных пользовательских приложений с помощью WPF // Определить некоторые настройки XML для сохранения отступов. XmlWriterSettings xmlSettmgs = new XmlWriterSettings(); xmlSettmgs . Indent = true; // Создать StringBuilder для хранения XAML-разметки. StringBuilder strBuilder = new StringBuilder(); // Создать XmlWriter на основе существующих настроек. XmlWriter xWriter = XmlWriter. Create (strBuilder, xmlSettmgs); // Сохранить XAML-разметку в объекте XmlWriter на основе ControlTemplate. XamlWriter. Save (ctrlToExamme. Template, xWriter) ; // Отобразить XAML-разметку в текстовом поле. dataToShow = strBuilder . ToStnng () ; } catch (Exception ex) { dataToShow = ex.Message; } } Большую часть работы занимает манипулирование скомпилированным ресурсом BAML для отображения его на строку XAML. На рис. 31.10 показано финальное приложение в действии на примере отображения шаблона по умолчанию элемента управления DatePicker. Logical Tree of Window Visual Tree of Window 1 Enter Full Name of WPF Control 1 j System,Windows-Controls. DatePicker 1 See Template Select л date 4 February, 2010 Su Mo Tu We Th Fr 31 1 2 3 4 S 7 S Э 10 11 12 14 15 16 17 16 19 21 22 23 |3 25 26 28 1 2 3 4 5 7 8 Э 10 11 12 D С S» fl 13 20 27 e % L > <?xml versions.0" encoding="utf-16"?> <ControlTemplate TargetType="DatePicker" xrnlns="httpy7schemas.micros; jj <Border BorderThicJcness="{TemplateBinding Border.BorderThickness)" Pelgj <VisualStateManager.VisualStateGroups> <VisuaiStateGroup Name="CommonSta,tes" /> </VisualStateManager.VtsualStateGroups> <Grid Name="PART_Root" HonzontalAlignment="(TemplateBinding Cor < Gnd.ColumnDef inttions > <ColumnDefinition Widths"** l> <ColumnDefinition Width="Auto* /> </Grid.ColumnDefinitions> <Grid.Resources> <ControlTemplate TargetType="Button" х:Кеу="ё"> <Grid> <VisualStateManager.VisualStateGroups> <VisualStateGroup Name="CommonStates" /> </V»ualStateManager.VisualStateGroups> <Grid &ackground="#llFFFFFF* Width=9* Height=8" FlowDire< <Grid.ColumnDefinitions > <ColumnDefinition Width=0*" /> <ColumnDefinition W»dth=0*" /> <ColumnOefinition Width=0*" /> <ColumnDefinition W»dth=0*" /> </Gnd.ColumnDefinitions > Рис. 31.10. Исследование ControlTemplate во время выполнения Теперь вы должны лучше представлять совместную работу логических деревьев, визуальных деревьев и шаблонов элементов управления. Оставшаяся часть главы будет посвящена построению специальных шаблонов и пользовательских элементов управления. Исходный код. Проект TreesAndTemplatesApp доступен в подкаталоге Chapter 31.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1191 Построение специального шаблона элемента управления в Visual Studio 2010 Построение специального шаблона для элемента управления может осуществляться исключительно в коде С#. При таком подходе можно было бы добавить данные к объекту ControlTemplate и присвоить его свойству Template элемента управления. Однако большую часть времени вы будете формировать внешний вид ControlTemplate, используя XAML, и для упрощения задачи обычно при этом применяется инструмент Expression Blend. Этот инструмент будет задействован в финальном примере настоящей главы, а пока построим простой шаблон в Visual Studio 2010. Хотя данная IDE- среда не имеет столько возможностей визуального конструирования, как Expression Blend, использование Visual Studio 2010 — хороший способ изучить детали конструкции шаблона. Создайте новое приложение WPF по имени ButtonTemplate. В этом проекте основной интерес представляют механизмы создания и использования шаблонов, так что разметка для главного окна очень проста: <Wmdow х: Class="ButtonTemplate . MamWindow" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Fun with Templates" Height=50" Width=25"> <StackPanel> <Button x:Name="myButton" Width=00" Height=00" Click="myButton_Click"/> </StackPanel> </Wmdow> В обработчике события Click просто отобразите окно сообщения (посредством MessageBox.ShowO), которое подтверждает факт щелчка на элементе управления. При построении шаблонов элементов управления помните, что поведение элемента управления неизменно, но его внешний вид может варьироваться. В настоящее время этот элемент Button визуализируется с использованием шаблона по умолчанию, который, как иллюстрирует последний пример, представляет собой ресурс BAML внутри данной сборки WPF. Когда вы хотите определить собственный шаблон, то по существу заменяете это визуальное дерево по умолчанию собственным деревом. Для начала обновите определение элемента <Button>, указав новый шаблон с помощью синтаксиса "свойство-элемент". Этот шаблон придаст элементу управления округлый вид: <Button x:Name="myButton" Width=00" Height=00" Click="myButton_Click"> <Button.Template> <ControlTemplate> <Grid x:Name="controlLayout"> <Ellipse x:Name="buttonSurface" Fill = "LightBlue"/> <Label x:Name="buttonCaption" VerticalAlignment = "Center" HorizontalAlignment = "Center" FontWeight = "Bold" FontSize = 0" Content = K!"/> </Grid> </ControlTemplate> </Button.Template> </Button> В приведенной выше разметке определен шаблон, состоящий из именованного элемента управления Grid, который содержит именованный элемент Ellipse и Label.
1192 Часть VI. Построение настольных пользовательских приложений с помощью WPF Поскольку в Grid не определены строки и столбцы, каждый дочерний элемент укладывается поверх предыдущего элемента управления, позволяя центрировать содержимое. Если теперь запустить приложение, можно заметить, что событие Click инициируется только в том случае, когда курсор мыши находится в границах Ellipse (и даже не в углах, окружающих эллипс)! Отсутствие необходимости вычислять попадание курсора мыши на поверхность элемента либо какие-то иные низкоуровневые детали — замечательная характеристика архитектуры шаблонов WPF. Поэтому, если шаблон использовал объект Polygon для визуализации некоторой необычной геометрии, можете быть уверены, что детали проверки попадания курсора мыши будут соответствовать форме элемента управления, а не охватывающему прямоугольнику. Шаблоны как ресурсы В настоящее время шаблон включен в специфический элемент управления Button, что ограничивает возможности повторного использования. В идеале шаблон круглой кнопки стоило бы поместить в словарь ресурсов для многократного использования в разных проектах или, как минимум, переместить его в контейнер ресурсов для повторного использования в текущем проекте. Давайте перенесем локальный ресурс Button на уровень приложения, используя Visual Studio 2010. Сначала найдите свойство Template элемента Button в окне Properties (Свойства). Теперь щелкните на значке с изображением маленького черного ромба и выберите Extract Value to Resource... (Извлечь значение в ресурс), как показано на рис. 31.11. [Properties 1 Button myButton 1 _5* Properties / Events 1 ; 1 OJ j Search Tablndex □ 2147483647 Tag □ ^^| Resource... ToolTip Reset Value Uid UseLayoutRounding & Apply Data Binding... VerticalAlignment j 9 . Apply Resource... VerticalContentAlign... Visibility Extract Value to Resource... Width + 1ЙГ"" » □ xj OKI A 1 I Л i Рис. 31.11. Извлечение локального ресурса В результирующем диалоговом окне определите новый шаблон по имени RoundButtonTemplate и сохраните его в App.xaml (рис. 31.12). Create Resource Key: RoundButtonTemplate Destination: appjtaml ;. ok l i Я) 1^Шв^ц^шмш1 i 3Lsm* 1 j Рис. 31.12. Размещение ресурса в файле App.xaml
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1193 После этого в разметке объекта Application появятся следующие данные: Application х:Class="ButtonTemplate.Арр" xmlns="http : //schema s .microsoft. com/wmfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWmdow.xaml"> <Application.Resources> <ControlTemplate x:Key="RoundButtonTemplate"> <Grid x:Name="controlLayout"> <Ellipse x: Name="buttonSurface" Fill = "LightBlue"/> <Label x:Name="buttonCaption" VerticalAlignment = "Center" HorizontalAlignment = "Center" FontWeight = "Bold" FontSize = 0" Content = "OK|M/> </Grid> </ControlTemplate> </Application.Resources> </Application> При желании применение этого шаблона можно ограничить. Подобно стилю WPF, к любому элементу <ControlTemplate> можно добавить атрибут TargetType. Обновите открывающий дескриптор <ControlTemplate>, указав, что только элементы управления Button могут использовать этот шаблон: <ControlTemplate x:Key="RoundButtonTemplate" TargetType ="Button"> Теперь, поскольку этот ресурс доступен всему приложению, можно определить любое количество круглых кнопок. Для целей тестирования создайте два дополнительных элемента управления Button, которые используют этот шаблон (обрабатывать событие Click для них не обязательно): <StackPanel> <Button x:Name="myButton" Width=00" Height=00" Click="myButton_Click" Template="{StaticResource RoundButtonTemplate}"></Button> <Button x:Name="myButton2" Width=00" Height=00" Template="{StaticResource RoundButtonTemplate}"></Button> <Button x:Name="myButton3" Width=00" Height=00" Template="{StaticResource RoundButtonTemplate}"></Button> </StackPanel> Включение визуальных подсказок с использованием триггеров При определении специального шаблона все визуальные подсказки удаляются. Например, шаблон кнопки по умолчанию содержит разметку, которая информирует элемент управления о том, как ему выглядеть по наступлению определенных событий пользовательского интерфейса, таких как получение фокуса, щелчок кнопкой мыши, включение (или отключение) и т.д. Пользователи достаточно привыкли визуальным подсказкам подобного рода, поскольку они придают элементу управления некоторую ощутимую реакцию. Однако в шаблоне RoundButtonTemplate не определено какой-либо предназначенной для этого разметки, так что вид элемента управления будет одинаков, независимо от действий мыши. В идеале элемент должен выглядеть несколько иначе, когда на нем производится щелчок (возможно, его цвет изменяется или появляется тень); это уведомляет пользователя об изменении визуального состояния. Сразу после появления WPF единственным способом организации таких визуальных подсказок было добавление к шаблону любого количества триггеров, которые обычно изменяют значения объектных свойств либо запускают раскадровку анимации (либо делают то и другое), когда условие того или иного триггера истинно. Для примера об-
1194 Часть VI. Построение настольных пользовательских приложений с помощью WPF новите RoundButtonTemplate следующей разметкой, что обеспечит изменение цвета элемента управления на синий, а цвет Foreground — на желтый, когда курсор мыши наведен на его поверхность: <ControlTemplate x:Key="RoundButtonTemplate" TargetType="Button"> <Grid x:Name="controlLayout"> <Ellipse x:Name="buttonSurface" Fill="LightBlue" /> <Label x:Name="buttonCaption" Content="OK>" FontSize=0" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> <ControlTemplate.Triggers> <Trigger Property = "IsMouseOver" Value = "True"> <Setter TargetName = "buttonSurface" Property = "Fill" Value = "Blue"/> <Setter TargetName = "buttonCaption" Property = "Foreground" Value = "Yellow"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> Вновь запустив эту программу, вы обнаружите, что цвет переключается в зависимости от того, находится ли курсор мыши в области Ellipse. Ниже показан другой триггер, в котором размер Grid (и, следовательно, дочерних элементов) сокращается, когда кнопка нажата с помощью мыши. Добавьте следующую разметку в коллекцию <ControlTemplate.Triggers>: <Trigger Property = "IsPressed" Value="True"> <Setter TargetName="controlLayout" Property="RenderTransformOrigin" Value=.5,0.5"/> <Setter ТаrgetName="controlLayout" Property="RenderTransform"> <Setter.Value> <ScaleTransform ScaleX=.8" ScaleY=.8"/> </Setter.Value> </Setter> </Trigger> В результате получился специальный шаблон с несколькими визуальными подсказками, включаемыми с помощью триггеров WPF. Как вы увидите в следующем примере этой главы, в .NET 4.0 появился альтернативный способ включения визуальных подсказок с использованием Visual State Manager (Диспетчер визуальных состояний). Прежде чем заняться им, давайте рассмотрим роли расширения разметки {TempiateBinding} и класса ContentPresenter. Роль расширения разметки {TempiateBinding} Построенный специальный шаблон может быть применен только к элементам управления Button, и потому имеется причина того, что должен существовать способ установки свойств элемента <Button> так, чтобы заставить шаблон визуализировать себя уникальным образом. Например, прямо сейчас свойство Fill элемента Ellipse жестко закодировано в синий цвет, а свойство Content элемента Label всегда установлено в значение ОК. Естественно, возникает необходимость иметь кнопки другого цвета и с другими текстовыми значениями, поэтому давайте попробуем определить такие кнопки в главном окне: <StackPanel> <Button x:Name="myButton" Width=00" Height=00" Background="Red" Content="Howdy!" Click="myButton_Click" Template="{StaticResource RoundButtonTemplate}" />
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1195 <Button x:Name="myButton2" Width=00" Height=00" Background="LightGreen" Content="Cancel'" Template="{StaticResource RoundButtonTemplate}" /> <Button x:Name="myButton3" Width=00" Height=00" Background="Yellow" Content="Format" Template="{StaticResource RoundButtonTemplate}" /> </StackPanel> Однако, независимо от того факта, что для каждого элемента Button задаются уникальные значения в свойствах Background и Content, все равно получаются три синих кнопки с текстом ОК. Проблема в том, что элемент управления, использующий шаблон (Button), имеет свойства, которые не соответствуют в точности элементам шаблона (например, свойство Fill элемента Ellipse). Кроме того, хотя элемент Label имеет свойство Content, значение, определенное в контексте <Button>, не маршрутизируется автоматически на внутреннее содержимое шаблона. Описанную проблему можно решить, применив при построении шаблона расширение разметки {TemplateBonding}. Это позволит захватывать настройки свойств, определенные элементом управления, который использует шаблон, и использовать их для установки свойств в самом шаблоне. Ниже приведена переработанная версия RoundButtonTemplate, в которой теперь применяется это расширение разметки для отображения свойства Background элемента Button на свойство Fill элемента Ellipse; также здесь обеспечивается действительная передача значения Content элемента Button в свойство Content элемента Label. <ControlTemplate x:Key="RoundButtonTemplate" TargetType="Button"> <Grid x:Name="controlLayout"> <Ellipse x:Name="buttonSurface" Fill = " {TemplateBindmg Background <Label x:Name="buttonCaption" Content=" {TemplateBindmg Content}" FontSize=0" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> <ControlTemplate.Triggers> }"/> </ControlTemplate.Triggers> </ControlTemplate> После такого обновления можно создавать кнопки разных цветов и с разным текстом (рис. 31.13). * Fun with Templates Howdy! Cancel! Format LlMJ Рис. 31.13. Привязки шаблона позволяют передавать значения во внутренние элементы управления
1196 Часть VI. Построение настольных пользовательских приложений с помощью WPF Роль класса ContentPresenter При проектировании шаблона для отображения текстового значения элемента управления использовался элемент Label. Подобно Button, элемент Label поддерживает свойство Content. Поэтому, если применяется расширение разметки {TemplateBinding}, то можно определить элемент Button со сложным содержимым, а не с простой строкой. Например: <Button x:Name="myButton4" Width=00" Height=00" Background="Yellow" Template="{StaticResource RoundButtonTemplate}"> <Button.Content> <ListB6x Height=0" Width=5"> <ListBoxItem>Hello</ListBoxItem> <ListBoxItem>Hello</ListBoxItem> <ListBoxItem>Hello</ListBoxItem> </ListBox> </Button.Content> </Button> Для этого конкретного элемента управления все работает, как ожидалось. Но что если нужно передать сложное содержимое члену шаблона, который не имеет свойства Content? Когда требуется определить обобщенную область отображения содержимого в шаблоне, в противоположность специфическому типу элемента (Label или TextBox) можно использовать класс ContentPresenter. В данном примере делать это не нужно, однако ниже показана простая разметка, иллюстрирующая построение специального шаблона, который использует ContentPresenter для показа значения свойства Content элемента управления, использующего шаблон: <!-- Этот шаблон кнопки отобразит то, что установлено в Content принимающей кнопки --> <ControlTemplate x:Key="NewRoundButton" TargetType="Button"> <Grid> <Ellipse Fill="{TemplateBinding Background}"/> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </ControlTemplate> Включение шаблонов в стили Сейчас шаблон просто определяет базовый вид элемента управления Button. Тем не менее, за процесс установки базовых свойств элемента управления (содержимое, размер шрифта, его толщина и т.п.) отвечает сам элемент Button: <!-- Сейчас Button должен устанавливать базовые значения свойств, а не шаблон —> <Button x:Name ="myButton" Foreground ="Black" FontSize =0" FontWeight ="Bold" Template ="{StaticResource RoundButtonTemplate}" Click ="myButton_Click"/> При желании можно устанавливать эти значения в шаблоне. Подобным образом, по сути, создается внешний вид по умолчанию. И как, возможно, уже понятно — это работа для стилей WPF. Когда строится стиль (включающий базовые установки свойств), можно определить шаблон внутри стиля! Ниже приведен обновленный ресурс приложения из App.xaml, который помечен ключом RoundButtonSyle: <!-- Стиль, содержащий шаблон --> <Style x:Key ="RoundButtonStyle" TargetType ="Button"> <Setter Property ="Foreground" Value ="Black"/> <Setter Property ="FontSize" Value =4"/>
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1197 <Setter Property ="FontWeight" Value ="Bold"/> <Setter Property="Width" Value=00"/> <Setter Property="Height" Value=00"/> <!— А это — сам шаблон! —> <Setter Property ="Template"> <Setter.Value> <ControlTemplate TargetType ="Button"> <Grid x:Name="controlLayout"> <Ellipse x:Name="buttonSurface" Fill=" {TemplateBindmg Background} "/> <Label x:Name="buttonCaption" Content =" {TemplateBindmg Content}" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> <ControlTemplate.Triggers> <Trigger Property = "IsMouseOver" Value = "True"> <Setter TargetName = "buttonSurface" Property = "Fill" Value = "Blue"/> <Setter TargetName = "buttonCaption" Property = "Foreground" Value = "Yellow"/> </Trigger> <Trigger Property = "IsPressed" Value="True"> <Setter ТаrgetName="controlLayout" Property="RenderTransformOrigin" Value=.5,0. 5"/> <Setter TargetName="controlLayout" Property="RenderTransform"> <Setter.Value> <ScaleTransform ScaleX=.8" ScaleY=.8"/> </Setter.Value> </Setter> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> После такой модификации появляется возможность создавать кнопочные элементы управления, устанавливая свойство Style следующим образом: <Button х:Name="myButton" Background="Red" Content="Howdy'" Click="myButton_Click" Style="{StaticResource RoundButtonStyle } "/> Хотя визуализация и поведение кнопки идентичны, преимущество внедрения шаблонов в стили состоит в том, что это позволяет предоставить готовый набор значений для общих свойств. На этом обзор использования Visual Studio 2010 при построении специальных шаблонов для элементов управления завершен. Но прежде чем перейти к миру веб-разработки с помощью ASP.NET, давайте посмотрим, как посредством Expression Blend не только генерировать шаблоны элементов управления, но также создавать специальные элементы UserControl. Исходный код. Проект ButtonTemplate доступен в подкаталоге Chapter 31. Построение специальных элементов UserControl с помощью Expression Blend Во время рассмотрения свойств зависимости была кратко представлена концепция UserControl. В некоторых отношениях UserControl — это следующий логический шаг после шаблона элемента управления. Вспомните, что при построении шаблона элемента управления, по сути, применяется обложка (skin), которая изменяет физический
1198 Часть VI. Построение настольных пользовательских приложений с помощью WPF внешний вид элемента управления WPF. Специальный элемент UserControl, однако, позволяет буквально строить новый тип класса, который может иметь уникальные члены (методы, события, свойства и т.п.). К тому же многие специальные UserControl используют шаблоны и стили; допустимо также определять дерево элементов управления непосредственно в контексте <UserControl>. Вспомните, что элемент проекта UserControl можно добавлять к любому приложению WPF в Visual Studio 2010, включая создание проекта библиотеки UserControl, которая представляет собой сборку *.dll, не содержащую ничего, помимо коллекции UserControl для использования между проектами. В последнем разделе этой главы с использованием инструмента Expression Blend будут продемонстрированы некоторые очень интересные приемы разработки, включая создание нового элемента управления из геометрии, работу с редактором анимации и включение визуальных подсказок с помощью .NET 4.0 Visual State Manager (VSM). Изучая эти темы, помните, что аналогичных результатов можно достичь и в Visual Studio 2010; это просто потребует ручного ввода большего объема разметки. Создание проекта библиотеки UserControl Целью финального примера этой главы будет построение приложения WPF, которое представляет простую игру джек-пот Хотя всю логику вполне можно поместить в новую исполняемую программу WPF, вместо этого вынесем специальные элементы управления в отдельный проект библиотеки. Запустите Expression Blend, выберите пункт меню File^New Project (Файл^Новый проект) и создайте новый проект типа WPF Control Library по имени MyCustomControl (рис. 31.14). Рис. 31.14. Создание нового проекта WPF Control Library в Expression Blend Переименование начального элемента UserControl Проект этого типа предоставляет начальный элемент UserControl по имени MainControl. Давайте переименуем этот элемент управления в SpinControl. Для этого начните с изменения имени файла MainControl.xaml на SpinControl.xaml через вкладку Projects (Проекты). Затем откройте редактор XAML для начального элемента управления и измените атрибут х:Class, как показано ниже (кроме того, полностью
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1199 удалите атрибут x:Name). Установите для свойств Height и Width элемента UserControl значение 150. <UserControl xmlns="http: //schemas .microsoft. com/wmfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/200 8" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2 00 6" mc:Ignorable="d" x :Class="MyCustomControl. SpmControl" Width=50" Height=50"> </UserControl> Откройте файл кода С#, который соответствует XAML-документу, и измените имя класса (и конструктор) на SpinControl: public partial class SpinControl { public SpinControl () { this.InitializeComponent(); } } Чтобы удостовериться в отсутствии опечаток, полностью пересоберите проект. Проектирование элемента SpinControl Целью специального SpinConrtol является циклический перебор трех файлов графических изображений в случайном порядке при вызове метода Spin(). В состав кода примеров для этой главы входят три файла (Cherries.png, Jackpot.jpg и Limesopg), которые представляют возможные изображения. Добавьте их к текущему проекту, используя пункт меню Projects Add Existing Item (Проекте Добавить существующий элемент). При добавлении этих изображений к проекту Blend они автоматически конфигурируются как встроенные в результирующую сборку. Визуальный дизайн SpinControl достаточно прост. Используя Assets Library, добавьте элемент управления Border, который использует значение BorderThickness, равное 5, а в редакторе кистей выберите цвет BorderBrush по своему вкусу. Затем поместите внутрь Grid элемент управления Image (по имени imgDisplay) и установите для свойства Stretch значение Fill. После этого Grid должен быть сконфигурирован так: <Grid x:Name="LayoutRoot"> <Border BorderBrush="#FFD51919" BorderThickness="/> <Image x:Name="imgDisplay" Margin="8" Stretch="Fill"/> </Grid> Наконец, установите для свойства Source элемента управления Image одно из трех упомянутых выше изображений. Поверхность визуального конструктора теперь должна выглядеть примерно, как показано на рис. 31.15. Добавление начального кода С# Теперь, когда разметка готова, воспользуйтесь вкладкой Events (События) окна Properties (Свойства) в Expression Blend, чтобы обработать событие Loaded элемента управления, и укажите метод по имени SpinControl Loaded. В окне кода объявите массив из трех объектов Bitmaplmage, который будет заполняться встроенными файлами двоичных изображений при наступлении события Loaded:
1200 Часть VI. Построение настольных пользовательских приложений с помощью WPF Рис. 31.15. Пользовательский интерфейс элемента SpinControl public partial class SpinControl { // Массив объектов Bitmaplmage. private Bitmaplmage[] images = new Bitmaplmage[3]; public SpinControl () { this.InitializeComponent(); } private void SpinControl_Loaded (object sender, System.Windows.RoutedEventArgs e) { // Заполнить массив ImageSource изображениями. images [0] = new Bitmaplmage (new Uri ("Cherries .png" , UnKind. Relative) ) ; images [1] = new Bitmaplmage (new Uri ( "Jackpot. jpg", UnKind.Relative) ) ; images [2] = new Bitmaplmage (new Uri ("Limes . jpg" , UnKind.Relative) ) ; } } Определите общедоступный член по имени Spin(), реализованный так, чтобы отображать в элементе управления Image случайно выбранный один из трех объектов Bitmaplmage и возвращать значение случайного числа: public int Spin () { // Поместить в элемент управления Image случайно выбранное изображение. Random г = new Random(DateTime.Now.Millisecond); int randomNumber = r.NextC); this.lmgDisplay.Source = images[randomNumber]; return randomNumber; } Определение анимации с помощью Expression Blend На этом элемент SpinControl почти готов. А теперь сделаем картину более привлекательной для пользователя, воспользовавшись Expression Blend для определения анимации, которая заставит Image переворачиваться, создавая иллюзию "перелистывания" графического изображения. Логику анимации можно было бы реализовать в коде С#, однако в этом примере мы с помощью интегрированного редактора анимации Expression Blend определим раскадровку XAML. Активизируйте поверхность визуального конструктора SpinControl в проекте. В окне Object and Timeline выберите элемент Image и щелкните на кнопке New (Создать), как показано на рис. 31.16.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1201 В открывшемся диалоговом окне назовите раскадровку SpinlmageStotyboard. После щелчка на кнопке ОК окно Objects and Timeline изменит свой внешний вид. Появится редактор временной шкалы, который можно сделать более наглядным, нажав клавишу <F6> для перевода IDE-среды в режим редактора анимации (для возврата к предыдущему представлению еще раз нажмите <F6>). Этот редактор позволяет указать, как будет меняться объект за единицы времени, именуемые ключевыми кадрами (keyframe). Щелкните на желтой стрелке временной шкалы и перетащите ее на метку 0,5 секунды. После этого щелкните на кнопке Record Keyframe (Записать ключевой кадр), находящейся прямо над меткой нулевой секунды. Редактор временной шкалы должен выглядеть, как показано на рис. 31.17. Рис. 31.16. Создание новой раскадровки с использованием Expression Blend Рис. 31.17. Определение нового ключевого кадра Несложно заметить, что визуальный редактор теперь находится в режиме записи, на что указывает красная рамка вокруг него. В это время можно изменять любое свойство объекта в окне Properties. По мере внесения изменений, они будут записываться IDE-средой, и принимать форму инструкций анимации XAML. Для текущего примера найдите редактор Transform (Трансформация) в окне Properties среды Expression Blend и укажите опцию Flip Y Access (Переворот вокруг оси Y), как показано на рис. 31.18. Теперь вернитесь в окно Objects and Timeline и щелкните на ресурсе временной шкалы spinlmageStoryboard (рис. 31.19). Выбрав временную шкалу для редактирования, можно конфигурировать разнообразные установки на самой временной шкале, такие как поведение автореверса или повтора, используя для этого редактор Properties. Для данной временной шкалы отметьте флажок AutoReverse (Автореверс), как показано на рис. 31.20. Теперь все готово к тестированию анимации. Щелкните на кнопке Play (Воспроизведение), как показано на рис. 31.21. Вы должны увидеть, что изображение перелистыва- ется, затем возвращается к своему исходному виду. Рис. 31.18. Применение трансформации к элементу управления Image
1202 Часть VI. Построение настольных пользовательских приложений с помощью WPF Objects and Timeline I soirlrrageStoryboard ^ ^% [UserControl] LayootRoot ■ [Border! * SI imgOtsplay Рис. 31.19. Выбор ресурса spinlmage Storyboard для редактирования Рис. 31.20. Включение автореверса анимации | soirlrrageStoryboard ■ [Border] ■ *1 imgDtsplay n —9U Рис. 31.21. Запуск тестирования анимации Таким образом, в результате определена простая анимация, которая выполняется примерно за одну секунду (полсекунды на переворот изображения и еще полсекунды для обратной трансформации). Теперь можно выйти из редактора анимации, щелкнув на кнопке записи в визуальном конструкторе (рис. 31.22). Рис. 31.22. Выход из редактора анимации На заметку! Сейчас анимация изменяет одиночное свойство единственного объекта. Имейте в виду, что раскадровка может изменять любое количество свойств (для любого числа объектов) по разным ключевым кадрам. Для этого просто перетаскивайте желтую метку в новую временную позицию, щелкайте на кнопке Add Keyframe (Добавить ключевой кадр) и изменяйте свойства объекта, выбранного в Objects and Timeline.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1203 Взглянув на полученную XAML-разметку, вы увидите, что инструмент Expression Blend добавил новый элемент <Storyboard> в словарь ресурсов UserControl: <UserControl.Resources> ''Storyboard x : Key="SpinImageStoryboard11 AutoReverse=llTrue"> <J?ointAnimationUsingKeyFrames BeginTime=0:00:00" Storyboard.ТаrgetName="imgDisplay" Storyboard . TargetProperty=" (UIElement. RenderTransf ormOngin) "> <SplinePointKeyFrame KeyTime=0:00:01" Value=.5,0.5"/> </PointAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime=0:00:00" Storyboard.TargetName="imgDisplay" Storyboard.TargetProperty= 11 (UIElement. PenderTransf orm) . (Transf ormGroup . Children) [0] . (ScaleTransform.ScaleY)"> <SplineDoublpKeyFrame KeyTime=0:00 : 01" Value="-l"/> </DoubleAniiT' itionUsingKeyFrames> </Storyboard^ </UserControl. Rigources> Программный запуск раскадровки Прежде чем перейти к конструированию приложения WPF, в котором используется специальная кнопка, понадобится выполнить еще одну задачу. Редактор анимации Expression Blend по умолчанию добавляет триггер, который запускает временную шкалу при загрузке в память UserControl (или Window — в случае проекта приложения WPF): ^UserControl . Ti i< iqers> •-EventTrigger I >utedEvent="FrameworkElement. Loaded"> • EeginStor;,!- nd Storyboard=" { StaticPesource SpmlmageStoryboard} "/> •'/EventTrigg^i "/UserControl . '1 i iL|gprs> Чтобы проверить это, щелкните на редакторе Triggers (Триггеры), который можно открыть через меню Windows (Окна) среды Expression Blend (рис. 31.23). Рис. 31.23 Удаление автоматически добавленного триггера В дополнение к переворачиванию при загрузке элемента управления, то же самое должно выполняться и при вызове метода Spin(). Импортируйте пространство имен System.Windows.Media.Animation в файл кода С# и модифицируйте метод Spin() для вызова временной шкалы, как показано ниже: public mt Spin () { // Случайно выбирать изображения в элемент управления Image. Random r = new Pandom(DateTime.Now.Millisecond); int randomNumbur = r.NextC); this.lmgDisplay.Source = images[randomNumber];
1204 Часть VI. Построение настольных пользовательских приложений с помощью WPF // Запустить анимацию. ((Storyboard)Resources["SpinImagestoryboard"]).Begin(); return randomNumber; } На этом библиотека специального элемента управления готова. Соберите проект, удостоверившись в отсутствии ошибок. Теперь давайте применим этот элемент управления в приложении WPF. Исходный код. Проект MyCustomControl доступен в подкаталоге Chapter 31. Создание WPF-приложения JackpotDeluxe Создайте новый проект WPF-приложения по имени JackpotDeluxe, используя для этого Expression Blend, и установите ссылку на сборку MyCustomControl.dll с помощью пункта меню Projects Add Reference... (Проект^ Добавить ссылку). Поскольку элемент управления определен во внешней сборке, добавьте в открывающий элемент Window определение нового пространства имен XML под названием custom, которое должно отображаться на пространство имен MyCustomControl: <Window xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:custom="clr-namespace:MyCustomControl;assembly=MyCustomControl" x:Class="JackpotDeluxe.MainWindow" x: Name="Window" Title="MainWindow" Width=40" Height=,,438"> </Window> Перед построением полного пользовательского интерфейса этого окна необходимо создать еще один специальный элемент управления. Извлечение UserControl из геометрических объектов Инструмент Expression Blend поддерживает ряд очень полезных сокращений, которые существенно упрощают процесс построения специальных элементов управления. Очень многие специальные элементы управления начинают свою жизнь в виде простой графики, созданной художником. Для целей иллюстрации выберите элемент Grid в редакторе Objects and Timeline и активизируйте инструмент Pencil (Карандаш) в панели инструментов (который можно увидеть, если щелкнуть и удержать Рис. 31.24. Уникальная кнопку Реп). С помощью этого инструмента нарисуйте какое- фигура, построенная с нибудь интересное изображение. На рис. 31.24 показано звездо- использованием инст- ~ ~ „ ,, р .. образное изображение, для свойства Fill которого установлена кисть оранжевого цвета, а для свойства Stroke — кисть синего цвета. Если теперь посмотреть на сгенерированную XAML-разметку, можно заметить, что инструмент Expression Blend определил объект Path. Предположим, что эту геометрию необходимо использовать в качестве отправной точки для построения специального элемента управления. Щелкните правой кнопкой мыши на объекте Path в визуальном конструкторе и выберите в контекстном меню пункт Make Into UserControl... (Превратить в пользовательский элемент управления), как показано на рис. 31.25.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1205 Рис. 31.25. Трансформация геометрии в новый элемент UserControl Будет предложено ввести имя элемента управления, в примере это StarButton (для всех прочих настроек в этом диалогом окне можно оставить значения по умолчанию). Expression Blend создаст новый класс, унаследованный Ст UserControl, с соответствующей XAML-разметкой и файлом кода С#. К тому же исходный объект Path в Window будет заменен экземпляром этого нового элемента управления, который отображен на новое пространство имен XML в файле MainWindow.xaml: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2 00 6/xaml" xmlns:custom="clr-namespace:MyCustomControl;assembly=MyCustomControl" xmlns:local="clr-namespace:JackpotDeluxe" x:Class="JackpotDeluxe.MainWindow" x: Name="Window11 Title="MainWindow" Width=40" Height = ,,438"> <Grid> <local:StarButton HorizontalAlignment="Left" Margin=02, 41, 0, 0" VerticalAlignment="Top11 Width=36" Height=31"/> </Grid> </Window> Оставьте этот элемент на месте до построения реального пользовательского интерфейса для Window. Роль визуальных состояний .NET 4.0 Вспомните, что одной из важнейших задач при разработке специальных элементов управления является добавление визуальных подсказок для конечного пользователя. В ранних примерах этой главы они добавлялись к специальному шаблону Button с использованием триггеров WPF. Триггеры можно применять и для StarButton, однако WPF в .NET 4.0 поддерживает альтернативный способ делать это — через группы визуальных состояний.
1206 Часть VI. Построение настольных пользовательских приложений с помощью WPF На заметку! Концепция визуальных состояний впервые появилась в API-интерфейсе Silverlight. Многие отметили, что визуальные состояния (и связанный с ними диспетчер визуальных состояний (Visual State Manager)) стали более простой альтернативой триггерам WPF, и потому этот альтернативный подход был включен в API-интерфейс WPF, начиная с версии .NET 4.0. Благодаря механизму визуальных состояний WPF, появляется возможность определить группу взаимосвязанных состояний, в которых элемент управления может находиться в каждый заданный момент времени. Воспринимайте группу визуальных состояний как просто именованный контейнер взаимосвязанных подсказок пользовательского интерфейса. Вообще говоря, имена создаваемых групп — полностью ваше дело; тем не менее, существует ряд распространенных имен групп, вроде MouseStateGroup, FocusedStateGroup или EnabledStateGroup. После определения набора групп визуальных состояний необходимо определить индивидуальные состояния для каждой конкретной группы. Например, MouseStateGroup может определять три возможных состояния мыши под названиями MouseEnterStar, MouseExitStar и MouseDownStar. Группа FocusedStateGroup может определить два состояния — GainFocusStar и LoseFocusStar. Как только состояния для группы определены, создаются раскадровки, представляющие подсказки пользовательского интерфейса, которые появляются, когда элемент управления переходит в заданное состояние. Например, можно построить раскадровку для состояния MouseEnterStar, которое заставит элемент управления сменить цвет. Вторая раскадровка для состояния MouseDownStar заставит элемент управления сжаться (или вырасти) в размерах. Для быстрого определения таких раскадровок можно использовать интегрированный редактор анимации в Expression Blend. После определения всех состояний необходимо заставить элемент управления перейти в любое заданное состояние. Для этого предусмотрено два подхода. Для перехода между состояниями в коде вызывается метод GetState () класса VisualStateManager. Просто укажите имя состояния и соответствующая раскадровка запустится. На заметку! Для перехода между состояниями с использованием только разметки потребуется определить триггеры, которые переводят элемент из одного состояния в другое, используя XAML-элемент GoToStateAction. Определение визуальных состояний для элемента управления StatButton Чтобы добавить визуальные состояния к элементу управления tarButton, сначала сделайте активным окно визуального редактора для элемента UserControl по имени StarButton в IDE-среде Expression Blend. Найдите вкладку States (Состояния), которая может быть открыта через меню Windows среды Expression Blend, и щелкните на кнопке Add State Group (Добавить группу состояний), как показано на рис. 31.26. Измените имя сгенерированной группы (сейчас именуемой VisualStateGroup) на более подходящее — MouseStateGroup. После этого щелкните на кнопке Add State (Добавить состояние), как показано на рис. 31.27. Рис. 31.26. Добавление новой группы визуальных состояний
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1207 С использованием кнопки Add State добавьте в группу MouseStateGroup три состояния под названиями MouseEnterStar, MouseExitStar и MouseDownStar (рис. 31.28). Рис. 31.27. Группа MouseStateGroup Рис. 31.28. Состояния в группе MouseStateGroup Каждое состояние теперь может быть определено в терминах раскадровки. Выберите состояние MouseEnti-. i Star. С помощью окна Properties задайте новый цвет для свойства Fill (например, темно-оранжевый). При желании измените другие свойства элемента управления, такие как форма элемента, перемещая его точки в визуальном редакторе или изменяя свойство StrokeThickness. Выберите состояние MouseExitStar и обратите внимание, что оно вернет цвет Fill в исходное значение, что как раз подходит для рассматриваемого примера. Выберите состояние MouseDownStar и воспользуйтесь окном Properties для применения простой трансформации к элементу управления (например, небольшой скос). Определение времени выполнения переходов между состояниями Итак, вы успешно определили группу состояний для одного значения, которая состоит из трех состояний. По умолчанию переход между состояниями происходит мгновенно, потому что значение Default transition (Переход по умолчанию) установлено в О секунд (это значение можно увидеть непосредственно под именем каждой группы состояний на вкладке States). При желании можно указать специальные периоды времени, в течение которых должен осуществляться переход между состояниями. Давайте настроим состояние MouseDownStar так, что оно будет выполнять раскадровку в течение 2 секунд. Для этого щелкните на кнопке Add Transition. Отобразится список всех возможных переходов из текущего состояния. Выберите первый элемент, который представляет акт перехода из текущего состояния в состояние MouseDownStar (рис. 31.29). Рис. 31.29. Изменение времени перехода между состояниями
1208 Часть VI. Построение настольных пользовательских приложений с помощью WPF Определите односекундный интервал времени на переход (рис. 31.30). Просмотр сгенерированной XAML-разметки Это и все, что нужно было сделать для группы визуальных состояний. Щелкните на вкладке XAML для элемента управления и просмотрите разметку, которую создала IDE-среда. В зависимости от того, сколько свойств было изменено, объем XAML-разметки может быть довольно значительным. Однако основной скелет будет выглядеть примерно так, как показано Рис. 31.30. Определение односе- „„we. нинсе. кундного интервала на выполнение данного перехода <VisualStateManager.VisualStateGroups> <VisualStateGroup x: Name=llMouseStateGroupM> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration=M00 : 00 : 01" To=IIMouseDownStar"/> </VisualStateGroup.Transitions> <VisualState x:Name="МоиseEnterStar"> <Storyboard> < !-- Раскадровка для MouseEnterStar —> </Storyboard> </VisualState> <VisualState x:Name="MouseExitStarll/> <VisualState x :Name="MouseDownStarll> <Storyboard> < ■-- Раскадровка для MouseDownStar --> </Storyboard> </VisualState> </VisualstateGroup> </VisualStateManager.VisualstateGroups> Теперь, имея все необходимые состояния, все, что осталось сделать — это переходить между ними в надлежащее время. Изменение визуальных состояний в коде с использованием класса VisualStateManager Чтобы завершить построение элемента StarButton, воспользуемся классом VisualStateManager для организации переходов между состояниями. С помощью вкладки Events окна Properties обработайте события MouseDown, MouseEnter и MouseLeave специального элемента управления UserControl. Реализуйте в каждом обработчике переход в соответствующее состояние, как показано ниже: public partial class StarButton : UserControl { private void StarControl_MouseDown (object sender, System.Windows.Input.MouseButtonEventArgs e) { // Параметр 1 : С каким элементом управления производится работа? // Параметр 2 : В какое состояние требуется перейти? // Параметр 3 : Нужно ли использовать время перехода? VisualStateManager.GoToState (this, "MouseDownStar", true); 1 private void StarControl_MouseEnter (object sender, System.Windows.Input.MouseEventArgs e) {
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1209 VisualStateManager.GoToState(this, "MouseEnterStar", true); } private void StarControl_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) { VisualStateManager.GoToState(this, "MouseExitStar", true); } } На этом конструирование второго специального элемента управления для данного проекта завершено. Запустив проект, можно проверить корректность работы визуальных состояний, взаимодействуя со StarButton в начальном окне. Завершение приложения JackpotDeluxe Имея готовые специальные элементы управления, завершить приложение JackpotDeluxe можно довольно быстро. Удалите текущий элемент StateControl из Window и переопределите корень компоновки как StackPanel вместо Grid. На рис. 31.31 показан возможный вариант компоновки окна. Рис. 31.31. Компоновка окна приложения JackpotDeluxe Ключевые аспекты этой разметки — это определения трех специальных объектов SpinControl (именуемых lmgFirst, imgSecond и imgThird) и экземпляр элемента StarButton (по имени btnSpin), который обрабатывает событие MouseDown. Вдобавок используются три элемента управления TextBlock (с именами txtlnstructions, txtScore и txtAttempts), которые служат для отображения пользователю текущего состояния, количества попыток и общего текстового сообщения. Ниже приведена полная разметка корневой панели StackPanel, поддерживающей несколько вложенных объектов StackPanel: <StackPanel x:Name=,,LayoutRoot" Background=,,#FF0F0202" Orientation=MVerticalM> <TextBlock x:Name=,,txtInstructions" Width=M639M Height=,,96" Foreground="Yellow" HorizontalAlignment=,,Left" FontSize=,,2 4" TextAlignment="Center" Text="Try to Score 100 Points in 20 Attempts'"/> <StackPanel Height=84" Width=39" Orientation="Horizontal">
1210 Часть VI. Построение настольных пользовательских приложений с помощью WPF <StackPanel.Background> <LinearGradientBrush EndPoint=.5,1" StartPoint=.5, ПМ> <GradientStop Color=M#FF000000M/> <GradientStop Color=M#FFB08282M Offset-"l"/> </LinearGradientBrush> </StackPanel.Background> <•-- Элементы SpinControl --> <custom:SpinControl x:Name=,,imgFirst" Height=M125M Margin=n, 0, 0, 0" Width=25"/> <custom: SpinControl x :Name="imgSecond11 Height=2511 Margin=0,0, 0, 0" Width=25"/> <custom:SpinControl x:Name=,,imgThird" Height-25" Margin=0.u,0,0" Width=M125M/> </StackPanel> <StackPanel Height=ll120" Orientation=llHorizontal"> <!-- Элемент StarButton --> < local: StarButton x :Name="btnSpin11 HorizontalAlignment = "T tft" Margin=02,8,0,0" VerticalAlignment=,,Top" Width=00" Height=0R" MouseDown=llbtnSpin_MouseDown"/> <TextBlock x:Name="txtScore" Text="Score: 0" FontFamily="Comic Sans MS" Width=40" Height=0" FontWeight=,,Bold" FontSize=,,24" Foreground=,,#FF6F0269" Margin="80,0,0,0" Л> <TextBlock x:Name=,,txtAttempts" Text="Attempts: 0" Height=9" Width=,,82" Foreground="#FF28EA16" Margin=0,0,0,0"/> </StackPanel> </StackPanel> В файле кода С# определите три приватных переменных-члена для отслеживания текущего количества очков, набранных игроком, текущего числа попыток и максимального количества разрешенных вращений: public partial class MainWindow : Window { private int totalPoints = 0; private int totalAttempts = 0; private int MaxAttempts = 20; } Определите два приватных вспомогательных метода, которые будут вызываться в случае выигрыша (определенного как получение 100 очков за 20 или менее попыток): private void DoLosingCondition () { // Изменить текст при проигрыше. this.txtlnstructions.Text = "YOU LOSE!"; this.txtlnstructions.FontSize = 80; this.txtlnstructions.Foreground = new SolidColorBrush(Colo?s.Gray) ; // Отключить кнопки, игра окончена! this.btnSpin.IsEnabled = false; } private void DoWinningCondition() { // Изменить текст при выигрыше. this.txtlnstructions.Text = "YOU WIN!"; this.txtlnstructions.FontSize = 80; this.txtlnstructions.Foreground = new SolidColorBrush(Colors.Orange) ; // Отключить кнопки, игра окончена1 this.btnSpin.IsEnabled = false; }
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1211 Как видите, оба эти метода устанавливают для свойства Text в txtlnstructions соответствующее сообщение и отключают кнопку вращения. Остальную часть кода составляет логика, выполняемая по щелчку на кнопке StarButton. Рассмотрим следующую реализацию обработчика событий MouseDown: private void btnSpin_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { // Добавить 1 к числу попыток. this.txtAttempts.Text = string.Format("Attempts: {0}", (++totalAttempts).ToString()); // Последняя попытка? if (totalAttempts >= MaxAttempts) { DoLosingCondition (); } // Повернуть каждый элемент управления. int randomOne = this.lmgFirst.Spin () ; int randomTwo = this.lmgSecond.Spin (); int randomThree = this.lmgThird.Spin () ; // Вычислить новый рекорд. Для простоты пользователь получает // очки, только если все три изображения идентичны. if (randomOne == randomTwo && randomTwo == randomThree) { // Уточнить количество очков. totalPoints += 10; this.txtScore.Text = string.Format("Score: {0}", totalPoints.ToString()); // Получено 100 или более очков? if (totalPoints >= 100) { DoWinningCondition(); } } } Представленная логика очень проста. Каждый раз, когда выполняется щелчок на StarButton, производится проверка, достигнуто ли максимальное число попыток, и если да, отображается сообщение о проигрыше. Если попытки еще остались, выполняется поворот каждого объекта SpinControl и проверка совпадения трех изображений. Если они идентичны, игроку добавляются очки и проверяется условие выигрыша. На рис. 31.32 показан конечный результат Рис. 31.32. Готовое приложение JackpotDeluxe
1212 Часть VI. Построение настольных пользовательских приложений с помощью WPF На этом рассмотрение построения настольных приложений с использованием API- интерфейса Windows Presentation Foundation завершено. На протяжении последних пяти глав вы узнали довольно много об этом аспекте разработки .NET; однако и осталось узнать немало. Кроме того, при построении множества примеров приложений использовался инструмент Expression Blend. Подобно Visual Studio 2010, Expression Blend имеет выделенную систему документации. Для серьезного изучения возможностей Expression Blend обращайтесь к встроенной справочной системе, открывающейся по нажатию клавиши <F1> (рис. 31.33). В ней вы найдете десятки превосходных руководств, которые углубят понимание этого ключевого аспекта разработки приложений WPF (и Silverlight). ]]$ Microsoft Expression Blend User Guide lolB Hide Back Print Options Contents 1 index | Search | It*! .jE S LJ Adjusting your workspace S Q] Working with solutions, projects, and files Ш L3 Working with objects and properties S СЛ Prototyping with SketchFlow Ш LJ Drawing objects Ш LJ Arranging objects В t3 Animating objects S Ql Working with storyboards and timelines S LJ Working with keyframes [S) Controlling when your storyboard runs [§] Try it: Create overlapping animations ji] Try it: Playing with handoff and nonhandoff animations Ш CJ Displaying data a LJ Styling objects В LJ Responding to mouse clicks and other events И |_J Creating custom controls S LJ References Getting started Welcome to Microsoft Expression Blend, a full-featured professional га design tool for creating engaging and sophisticated user interfaces for Microsoft Windows applications that are built on Windows Presentation Foundation (WPF), and for web applications that are built IJ on Microsoft Silverlight. Expression Blend lets designers focus on creativity while letting developers focus on programming. & Important The Silverlight runtime viewer is installed along with the Silverlight SDK when you install Expression Blend. Expression Blend uses this version of the runtime to display your project in Design view, but when you test your Stjverfight application, the application will be rendered using the version of Silverlight that your browser uses. Your browser might use a version of the runtime that is more recent than the version that was installed with Expression Blend if you have visited a website that required a newer version of the runtime. I Рис. 31.33 Регулярно обращайтесь к справочной системе Expression Blend Резюме В этой главе рассматривалось множество связанных с WPF тем, которые были ориентированы на построение специальных пользовательских элементов управления. Мы начали с исследования того, как в WPF задействованы традиционные программные примитивы .NET — свойства и события. Было показано, что механизм свойств зависимости позволяет создавать свойства, интегрируемые в набор служб WPF (анимации, привязки данных, стили и т.п.). Связанное с ними понятие маршрутизируемых событий обеспечивает передачу событий вверх и вниз по дереву разметки. Затем рассматривались отношения между логическим и визуальным деревьями. Логическое дерево однозначно отображается на разметку, описывающую корневой элемент WPF. За этим логическим деревом находится гораздо более глубокое визуальное дерево, которое содержит детальные инструкции визуализации. В главе также была рассмотрена роль шаблона по умолчанию. Помните, что при построении специальных шаблонов, по сути, отбрасывается все (или часть) визуального дерева элемента управления и заменяется собственной специальной реализацией. Было описано множество путей построения специальных классов UserControl, включая применение .NET 4.0 Visual State Manager, Expression Blend и интегрированного редактора анимации.
ЧАСТЬ VII Построение веб-приложений с использованием ASP.NET В этой части... Глава 32. Построение веб-страниц ASP.NET Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET Глава 34. Управление состоянием в ASP.NET
ГЛАВА 32 Построение веб-страниц ASP.NET Все рассмотренные до сих пор примеры приложений были либо консольными, либо настольными с графическим интерфейсом пользователя, построенными с применением API-интерфейса WPF. В последующих главах вы узнаете о том, как платформа .NET облегчает построение браузерных уровней представления с использованием технологии, именуемой ASP.NET. Для начала будет дан краткий обзор концепций веб- разработки (HTTP, HTML, сценарии клиентской и серверной стороны) и описана роль коммерческого веб-сервера Microsoft (IIS), а также веб-сервера разработки ASP.NET. После рассмотрения краткого примера внимание будет сосредоточено на структуре веб-страниц ASP.NET (включая однофайловую модель и модель отделенного кода) и на рассмотрении функциональности базового класса Page. В главе также будут описаны элементы управления ASP.NET, структура каталогов веб-сайта ASP.NET и применение файла Web.conf ig для управлением работой веб-сайта с помощью XML-инструкций. На заметку! Чтобы воспользоваться любым из проектов веб-сайтов ASP NET, входящих в состав кода примеров для этой главы, запустите Visual Studio 2010 и выберите пункт меню File^Open^Web Site... (Файл^Открыть^Веб-сайт). В открывшемся диалоговом окне щелкните на кнопке File System (Файловая система), расположенной в левой части окна, и выберите папку, содержащую файлы веб-проекта. Содержимое текущего веб-приложения загрузится в IDE-среду Visual Studio. Роль протокола HTTP Веб-приложения — сущность совершенно иного рода, чем настольные приложения с графическим интерфейсом. Первое очевидное отличие состоит в том, что профессиональное веб-приложение всегда подразумевает существование как минимум двух машин, объединенных в сеть: на одной развернут веб-сайт, а с другой просматриваются данные с помощью веб-браузера. Разумеется, во время разработки вполне допускается, чтобы роли браузерного клиента и веб-сервера, обслуживающего содержимое, исполнялись на одной машине. Учитывая природу веб-приложений, объединенные в сеть машины должны договориться об используемом сетевом протоколе для определения способа отправки и получения данных. Сетевой протокол, соединяющий эти компьютеры, называется HTTP (Hypertest Transfer Protocol — протокол передачи гипертекста).
Глава 32. Построение веб-страниц ASP.NET 1215 Цикл запрос/ответ HTTP Когда клиентская машина запускает веб-браузер (такой как Opera, Mozilla Firefox, Apple Safari или Microsoft Internet Explorer), выполняется HTTP-запрос для доступа к определенному ресурсу (обычно веб-странице) на удаленной серверной машине. HTTP представляет собой основанный на тексте протокол, построенный на базе стандартной парадигмы запрос/ответ. Например, если осуществляется переход к http://www. facebook.com, то программное обеспечение браузера полагается на веб-технологию, имеющую название DNS [Domain Name Service — служба доменных имен), которая преобразует запрошенный URL в 32-битное числовое значение, состоящее из четырех частей и именуемое IP-адресом. И в этот момент браузер открывает сокетное соединение (обычно через порт 80 для незащищенных подключений) и посылает HTTP-запрос для обработки на целевом сайте. Веб-сервер принимает входящий запрос HTTP и может обработать любые отправленные клиентом входные значения (такие как значения, введенные в текстовых полях, флажки, переключатели и т.п.), формирующие правильный HTTP-ответ. Веб- программисты могут использовать любое количество технологий (CGI, ASP, ASP.NET, JSP и т.п.) для динамической генерации содержимого, помещаемого в HTTP-ответ. После этого браузер клиентской стороны визуализирует HTML-разметку, отправленную вебсервером. На рис. 32.1 показан базовый цикл запроса/ответа HTTP. Браузер клиентской стороны Отображает HTML, полученный из ответа HTTP Входящий HTTP-запрос Исходящий HTTP-ответ Веб-сервер Веб-приложение (любое количество ресурсов серверной стороны, таких как файлы *.aspx, *.asp и *.html) Рис. 32.1. Цикл запроса/ответа HTTP HTTP - протокол без поддержки состояния Другой аспект веб-разработки, который заметно отличает ее от программирования традиционных настольных приложений, состоит в том, что HTTP — это, по сути, сетевой протокол без поддержки состояния. Как только веб-сервер отправил ответ клиентскому браузеру, все их предыдущее взаимодействие забывается. Это определенно не так, как в традиционном настольном приложении, где состояние исполняемой программы остается актуальным и изменяется в процессе взаимодействия до тех пор, пока пользователь не закроет главное окно приложения. Учитывая это, на вас, как веб-разработчика, возлагается ответственность за "запоминание" информации (наподобие выбранных товаров в корзине покупок, номеров кредитных карт, домашнего адреса и т.д.) о пользователях, которые в данный момент зарегистрированы на сайте. Как будет показано в главе 34, ASP.NET предлагает многочисленные способы поддержки состояния, которые используют такие технологии, как переменные сеанса, cookie-наборы и кэш приложения, а также API-интерфейс управления профилями ASP.NET.
1216 Часть VII. Построение веб-приложений с использованием ASP.NET Веб-приложения и веб-серверы Веб-приложение можно понимать как коллекцию файлов (*.htm, *.aspx, файлы изображений, файлы данных XML и т.п.), а также связанных с ними компонентов (таких как библиотека кода .NET), которые хранятся в определенном наборе каталогов на вебсервере. Как будет показано в главе 34, веб-приложения ASP.NET обладают специфическим жизненным циклом и предоставляют многочисленные события (вроде начального запуска или финального останова), которые можно перехватывать для выполнения специализированной обработки во время операций с веб-сайтом. Веб-сервер — это программный продукт, отвечающий за размещение веб-приложений. Он обычно предоставляет множество взаимосвязанных служб, таких как интегрированная безопасность, FTP (File Transfer Protocol — протокол передачи файлов), почтовый обмен и т.д. Службы Internet Information Services (IIS) — это веб-серверный продукт производственного уровня от Microsoft, который обладает встроенной поддержкой веб- приложений классического ASP и ASP.NET. Предполагая, что службы IIS установлены, взаимодействовать с IIS можно через папку Administrative Tools (Администрирование) панели управления, для чего нужно дважды щелкнуть на значке Internet Information Services Manager (Диспетчер служб IIS). На рис. 32.2 показан узел Default Web Site (Веб-сайт по умолчанию) в диспетчере служб IIS 7, в котором находится большинство деталей конфигурации (в предшествующих версиях IIS диспетчер выглядит по-другому). it Information Services (IIS) Manager i © \ !<$ ► ANDREWPC View Help Default Web Site | ANDREWPC (AndrewPOAndj «3 Application Pools i jaj Sites s ^ Default Web Site aspnet_client _J MSMQ £ WCFService Filter. Default Web Site Home -И*» '^ShowAll Groopby: Area ASP.NET Щ # LB * .NET .NET .NET Error .NET .NET Profile .NET Roles Authorraat... Compilation Pages Globalization ■T Trust .NET Use .evels а. у ■■31 .NET Trust Levels * NET Users Application Connection Machine Key Pages and Settings Strings Controls Providers Session State SMTP E-mail ! '-1 Features View :, Content View Рис. 32.2. Диспетчер IIS позволяет конфигурировать поведение времени выполнения служб Microsoft IIS Роль виртуальных каталогов I IS Единственная установленная копия IIS может размещать многочисленные веб-приложения, каждое из которых располагается в своем виртуальном каталоге. Каждый виртуальный каталог отображается на физический каталог на жестком диске. Например, если создается новый виртуальный каталог по имени CarsAreUs, то внешнему миру он доступен по адресу вроде http://www.MyDomain.com/CarsAreUs (предполагая, что IP- адрес сайта зарегистрирован в DNS как www.MyDomain.com). "За кулисами" этот виртуальный каталог отображается на физический корневой каталог на веб-сервере, в котором хранится содержимое веб-приложения CarsAreUs.
Глава 32. Построение веб-страниц ASP.NET 1217 Как будет показано далее в главе, при создании веб-приложения ASP.NET в Visual Studio 2010 можно заставить IDE-среду сгенерировать новый виртуальный каталог для текущего веб-сайта автоматически. Создать виртуальный каталог можно и вручную, щелкнув правой кнопкой мыши на узле Default Web Site в диспетчере IIS и выбрав в контекстном меню пункт Add Virtual Directory (Добавить виртуальный каталог). Веб-сервер разработки ASP.NET До выхода .NET 2.0 разработчики ASP.NET обязаны были создавать виртуальные каталоги IIS во время разработки и тестирования веб-приложений. Во многих случаях такая тесная зависимость от IIS излишне усложняла командную разработку, не говоря уже о том, что многие сетевые администраторы были не в восторге от необходимости установки IIS на машину каждого разработчика. К счастью, теперь появилась возможность пользоваться легковесным веб-сервером, который называется ASP.NET Development Web Server (Веб-сервер разработки ASP.NET). Эта утилита позволяет разработчикам размещать веб-приложения ASP.NET за пределами IIS. Используя данный инструмент, можно строить и тестировать веб-страницы из любого каталога на машине. Это довольно удобно в сценариях командной разработки для построения веб-приложений ASP.NET под управлением версий Windows, которые не поддерживают установку IIS. В большинстве примеров в настоящей книге применяется ASP.NET Development Web Server (через соответствующую опцию проекта Visual Studio 2010) вместо развертывания веб-содержимого в виртуальном каталоге IIS. Хотя данный подход может упростить разработку веб-приложений, имейте в виду, что этот веб-сервер не предназначен для размещения веб-приложений производственного уровня. Он предназначен только для целей разработки и тестирования. Как только веб-приложение готово в первом приближении, сайт должен быть скопирован в виртуальный каталог IIS. На заметку! Проект Mono (см приложение Б) предлагает бесплатное дополнение ASP.NET для веб-сервера Apache. Это позволяет строить и развертывать веб-приложения ASP.NET под управлением операционных систем, отличных от Microsoft Windows. Подробную информацию ищите по адресу http://www.mono-project.com/ASP.NET. Роль языка HTML Теперь, когда настроен каталог для размещения веб-приложения и выбран веб-сервер для использования в качестве хоста, нужно создать само содержимое. Вспомните, что веб-приложение — это просто набор файлов, составляющих функциональность сайта. Многие из этих файлов будут содержать операторы HTML (Hypertext Markup Language — язык разметки гипертекста). HTML является стандартным языком разметки, используемым для описания того, как литеральный текст, графические образы, внешние ссылки и различные элементы управления HTML отображаются внутри браузера клиентской стороны. Хотя современные IDE-среды (включая Visual Studio 2010) и платформы веб-разработки (такие как ASP.NET) генерируют большую часть HTML автоматически, при работе с ASP.NET без основательных знаний HTML все же не обойтись. На заметку! Вспомните из главы 2, что Microsoft предлагает ряд бесплатных IDE-сред, принадлежащих к семейству продуктов Express (например, Visual C# Express). Если вас интересует веб- разработка, возможно, стоит загрузить Visual Web Developer Express. Эта бесплатная IDE-среда специально ориентирована на конструирование веб-приложений ASP.NET.
1218 Часть VII. Построение веб-приложений с использованием ASP.NET В этом разделе рассматриваются некоторые основы HTML. Это поможет лучше понимать разметку, генерируемую программной моделью ASP.NET. Структура HTML-документа Файл HTML состоит из набора дескрипторов, описывающих внешний вид и поведение веб-страницы. Базовая структура HTML-документа имеет тенденцию оставаться постоянной. Например, файлы *.htm (или *.html) открываются и закрываются дескрипторами <html> и </html>, обычно определяют раздел <body>, и т.д. Имейте в виду, что традиционный HTML-код не чувствителен к регистру символов. Поэтому с точки зрения принимающего браузера <HTML>, <html> и <Html> — это одно и то же. Чтобы проиллюстрировать некоторые основы HTML, откройте Visual Studio 2010, создайте пустой файл страницы HTML, используя пункт меню File1^New■=> File (Файл1^ Создать1^Файл); обратите внимание, что веб-проект пока не создается, а просто открывается пустой файл HTML для редактирования. Сделав это, сохраните файл под именем default.htm в удобном месте. В нем будет находиться следующая начальная разметка: <!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> <title>Untitled Page</title> </head> <body> </body> </html> Прежде всего, обратите внимание, что этот файл HTML открывается инструкцией D0CTYPE. Она информирует IDE-среду, что содержащиеся в файле дескрипторы HTML должны соответствовать стандарту XHTML. Как упоминалось, традиционный HTML был очень либерален в отношении синтаксиса. Помимо нечувствительности к регистру символов, было допустимо определять открывающий элемент (такой как <br> для перевода строки), который не имел соответствующего закрывающего элемента (</br> в данном случае). Стандарт XHTML представляет собой спецификацию W3C, которая добавляет некоторую самую необходимую строгость к базовому языку разметки HTML. На заметку! По умолчанию Visual Studio 2010 проверяет все документы HTML на соответствие схеме XHTML 1.0 Transitional, чтобы обеспечить согласованность разметки со стандартом XHTML. Если хотите указать альтернативную схему верификации (такую как HTML 4.01), выберите пункт меню Tools^Options (Сервис=>Параметры). В открывшемся диалоговом окне раскройте узел Text Editor (Текстовый редактор), затем узел HTML и выберите узел Validation (Проверка достоверности). Если просматривать предупреждения верификации не требуется, просто снимите отметку с флажка Show Errors (Показывать ошибки). Дескрипторы <html>H</html> используются для пометки начала и конца документа. Обратите внимание, что открывающий дескриптор <html> дополнительно снабжен атрибутом xmlns (пространство имен XML), квалифицирующим различные дескрипторы, которые могут встречаться внутри этого документа (по умолчанию эти дескрипторы, опять-таки, основаны на стандарте XHTML). Веб-браузеры используют эти конкретные дескрипторы для того, чтобы понять, где следует начинать применять форматы визуализации, указанные в теле документа. Область <body> — место, в котором определена большая часть действительного содержимого. Чтобы немного приукрасить начальную страницу, добавим к ней заголовок:
Глава 32. Построение веб-страниц ASP.NET 1219 <head> <title>This is my simple web page</title> </head> He удивительно, что дескрипторы <title> применяются для указания текстовой строки, которая должна быть помещена в заголовке окна вызывающего веб-браузера. Роль формы HTML Реальная функциональность большинства файлов *.html находится в области элементов <form>. Форма HTML— это просто именованная группа взаимосвязанных элементов пользовательского интерфейса, обычно служащих для получения пользовательского ввода. Не путайте форму HTML с общей областью отображения браузера. В действительности форма HTML — это скорее логическая группа графических элементов управления, помещенная между дескрипторами <f orm> и </f orm>: <html xmlns="http://www.w3.org/1999/xhtml" > <head> <title>This is my simple web page</title> </head> <body> <form id="defaultPage"> <!— Сюда вставить содержимое пользовательского интерфейса --> </form> </body> </html> Форме назначены идентификатор "defaultPage". Обычно открывающий дескриптор <form> включает атрибут action, в котором указан URL, куда отправляются данные формы, а также метод передачи данных (POST или GET). Давайте ознакомимся с разновидностями элементов, помещаемых в формы HTML (помимо простого литерального текста). Инструменты визуального конструктора HTML в Visual Studio 2010 В Visual Studio 2010 имеется вкладка HTML панели инструментов (Toolbox), которая позволяет выбирать любой элемент управления HTML для помещения на поверхность визуального конструктора HTML (рис. 32.3). На заметку! При построении веб-страниц ASP.NET эти элементы управления HTML для создания пользовательского интерфейса использоваться не будут! Вместо них применяются элементы управления ASP.NET, которые визуализируются в виде корректного HTML-кода без вашего участия. Подобно процессу построения приложения Windows Forms или WPF, эти элементы управления HTML могут перетаскиваться на поверхность визуального конструктора HTML. Если щелкнуть на кнопке Split (Разделить) внизу окна, нижняя панель редактора HTML будет отображать визуальную компоновку HTML, а верхняя — соответствующий код разметки. Другое преимущество этого редактора состоит в том, что при выборе разметки или HTML-элемента пользовательского интерфейса соответствующее представление подсвечивается (рис. 32.4). 1 Toolbox *HTML 1 ^ Pointer CD Input (Button) гй§ Input (Reset) gj?) Input (Submit) |dbjj Input (Text) («ьЗ Input (File) S3 Input (Password) 0 Input (Checkbox) ® Input (Radio) *bi; Input (Hidden) i*U Textarea J Table tj8 Image Ц Select — Horizontal Rule ill D'v 1 a General ^ nxj д1 у ч Рис. 32.3. Вкладка HTML панели инструментов
1220 Часть VII . Построение веб-приложений с использованием ASP.NET default.htm X Client Objects & Events (No Events) <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML l.e Transitional/ZEN" "http://www.w3.org/T^j -J<htel Xfldns="http://www.w3.org/1999/xhtml" > '-<head> ! <title>This is my simple web page</title> [</head> <body> <fgp» id-",defaultPage"'> <!-- Insert web UI content here --> </form> </body> | </htral> 9 Design |d 5рЙГ] S Source j |<l] | < html >|< body > Рис. 32.4. HTML-редактор Visual Studio 2010 [Properties I DOCUMENT mn\ i ALmk Background Charset Class Id Link Style Text Title VLink 1 BgColor 1 Document background » п x| T ¥%l^:^\^^ 4 IP tffffcc !...|| ! This b my simple web page color. Рис. 32.5. Окно Properties в Visual Studio можно использовать для конфигурирования разметки HTML Среда Visual Studio 2010 позволяет редактировать общий вид файла *.htm или определенного элемента управления HTML в <form> с использованием окна Properties (Свойства). Например, выбрав DOCUMENT в раскрывающемся списке окна Properties, можно конфигурировать различные аспекты HTML-страницы, такие как цвет фона, фоновое изображение (если оно есть), заголовок и т.д. (рис. 32.5). При использовании окна Properties для конфигурирования какого-то аспекта веб-страницы IDE- среда соответствующим образом обновит HTML- разметку. Ниже показано минимальное изменение для страницы, которое обеспечивает установку цвета фона всего документа: <html xmlns="http://www.w3.org/1999/xhtml" > <head> <title>This is my simple web page</title> </head> <body bgcolor="#ffffcc"> <form id=lldefaultPage"> <!-- Insert web UI content here </form> </body> </html> Построение формы HTML Модифицируйте область <body> файла default.html для отображения литерального текста, который приглашает пользователя ввести какое-нибудь сообщение (имейте в виду, что можно вводить и форматировать литеральное текстовое содержимое, набирая его непосредственно в визуальном конструкторе HTML). Здесь используется дескриптор
Глава 32. Построение веб-страниц ASP.NET 1221 <hl> для установки "веса" заголовка, <р> — для блока абзаца и <i> — для выделения текста курсивом: <html xmlns="http://www.w3.org/1999/xhtml" > <head> <title>Thls is my simple web page</title> </head> '"body bgcolor=MNava]oWhiteM> <!— Приглашение пользовательского ввода --> <hl align=,,center">Simple HTML Page</hl> <p align=llcenterM> <br/> <i>Please enter a message</i>. </p> <form id="defaultPagell> <7form> </body> </html> Теперь давайте построим саму форму HTML. В общем случае каждый элемент управления HTML описывается с использованием атрибута id (применяемого для программной идентификации элемента) и атрибута type (применяемого для указания того, какой именно элемент пользовательского интерфейса необходимо поместить в объявление <form>). В зависимости от того, какой элемент управления пользовательского интерфейса объявлен, появляются дополнительные специфические атрибуты, которые могут быть модифицированы в окне Properties. Пользовательский интерфейс, который здесь строится, будет содержать одно текстовое поле и две кнопки. Первая кнопка будет служить для запуска сценария клиентской стороны, а другая — для сброса входных полей формы к своим значениям по умолчанию. Модифицируйте форму HTML следующим образом: <!-- Форма для получения информации о пользователе --> <form id=MdefaultPageM> <р align="centerll> Your Message: <input id="txtUserMessage11 type="textll/></p> <p align=llcenterM> <input id="btnShow11 type="button11 value=llShowl "/> <input id="btnReset11 type="reset11 value=llResetM/> </p> </form> Обратите внимание, что каждому элементу управления назначены подходящий идентификатор (txtUserMessage, btnShow и btnReset). Кроме того, каждый элемент ввода имеет дополнительный атрибут по имени type, помечающий его как элемент пользовательского интерфейса, который автоматически сбрасывает все поля в их начальные значения (type="reset"), принимает текстовый ввод (type="text") или функционирует как простая кнопка клиентской стороны, не выполняющая отправки веб-серверу (type="button"). На рис. 32.6 показана эта страница в браузере Google Chrome. На заметку! В случае выбора опции View in Browser (Просмотр в браузере) Visual Studio 2010 автоматически запускает веб-сервер разработки ASP.NET для размещения содержимого. Роль сценариев клиентской стороны В дополнение к HTML-элементам пользовательского интерфейса, файл *.html может содержать блоки кода сценариев, которые будут обработаны запрашивающим браузером.
1222 Часть VII. Построение веб-приложений с использованием ASP.NET С fl Ф http://localhostl596/S*mpieWebPag ► О' /" Simple HTML Page Please enter a message. Your Message: Рис. 32.6. Простая страница default.htm Есть две главные причины, вызывающие необходимость в сценариях клиентской стороны: • проверка достоверности пользовательского ввода перед отправкой данных обратно веб-серверу; • взаимодействие с объектной моделью документов (Document Object Model — DOM) браузера. Относительно первой причины следует понимать, что неизбежное зло веб-приложений состоит в необходимости частых обратных отправок (postback) на сервер для обновления HTML-разметки, визуализируемой в браузере. Хотя обратные отправки неизбежны, всегда нужно стремиться к минимизации трафика в сети. Один из приемов, позволяющих сэкономить на обратных отправках, как раз и состоит в применении сценариев клиентской стороны для проверки достоверности пользовательского ввода перед передачей данных формы веб-серверу. Если обнаруживается ошибка (вроде отсутствующих данных в обязательном поле), можно уведомить пользователя об этом, избегая накладных расходов, связанных с обратной отправкой на веб-сервер. (В конце концов, нет ничего более досадного для пользователя, чем необходимость обратной отправки по медленному соединению только для того, чтобы получить в ответ подсказку об ошибках ввода.) На заметку! Имейте в виду, что даже при выполнении проверки достоверности на стороне клиента (для сокращения времени реакции), проверка все равно должна также выполняться и на стороне самого веб-сервера. Это поможет гарантировать, что данные не будут подделаны и останутся именно такими, какие были отправлены клиентом. Элементы управления проверкой достоверности ASP.NET автоматически выполняют как клиентскую, так и серверную проверку (более подробно они рассматриваются в следующей главе). Сценарии клиентской стороны также могут использоваться для взаимодействия с лежащей в основе объектной моделью (DOM) самого браузера. Большинство коммерческих браузеров представляют набор объектов, которые позволяют управлять поведением браузера. Когда браузер разбирает HTML-страницу, он строит дерево объектов в памяти, представляющее все содержимое веб-страницы (формы, элементы ввода, и т.п.). Браузеры поддерживают API-интерфейс под названием DOM, который представляет дерево объектов и позволяет модифицировать его содержимое программно. Например, можно написать код JavaScript, который выполняется в браузере для получения значений определенных элементов управления, изменения цвета элемента, динамического добавления новых элементов на страницу и т.д.
Глава 32. Построение веб-страниц ASP.NET 1223 Разочаровывает тот факт, что разные браузеры склонны предоставлять сходные, ко не идентичные объектные модели. Поэтому если вы пишете блок сценария клиентской стороны, который взаимодействует с DOM, он может работать не идеально во всех браузерах (а потому тестирование всегда обязательно). ASP.NET предоставляет свойство HttpRequest.Browser, которое позволяет определять во время выполнения возможности браузера и устройства, отправившего текущий запрос. Эту информацию можете использоваться для стилизации формируемого HTTP- ответа в более оптимальной манере. Но вам редко придется беспокоиться об этом, если только вы не реализуете специальные элементы управления, поскольку все стандартные веб-элементы управления в ASP.NET автоматически знают, как визуализировать себя надлежащим образом, в зависимости от типа браузера. Эта ценная способность известна как адаптивная визуализация, и она реализована в готовом виде для всех стандартных элементов управления ASP NET. Существует много языков сценариев, предназначенных для написания кода сценариев клиентской стороны. Двумя наиболее популярными являются VBScript и JavaScript. Язык VBScript представляет собой подмножество языка программирования Visual Basic 6.0. Имейте в виду, что Microsoft Internet Explorer — единственный браузер, обладающий встроенной поддержкой VBScript клиентской стороны (в других браузерах могут быть предусмотрены дополнительные подключаемые модули, но не обязательно). Поэтому, если вы хотите, чтобы HTML-страницы корректно работали в любом коммерческом веб-браузере, не используйте VBScript для реализации логики сценариев клиентской стороны. Другой популярный язык сценариев — JavaScript. Важно помнить, что JavaScript не является ни формой, ни подмножеством языка Java. Хотя у Java и JavaScript отчасти сходный синтаксис, JavaScript не является полноценным объектно-ориентированным языком, и потому он менее мощный, чем Java. Положительной же стороной JavaScript является то, что все современные веб-браузеры поддерживают его, что делает его естественным кандидатом на реализацию логики сценариев клиентской стороны. Пример сценария клиентской стороны Чтобы проиллюстрировать роль сценариев клиентской стороны, давайте сначала рассмотрим, как перехватываются события, посылаемые элементами управления HTML пользовательского интерфейса стороны клиента. Для перехвата события Click кнопки Show! в раскрывающемся списке в левом верхнем углу визуального конструктора форм HTML выберите btnShow, а в раскрывающемся списке в правом верхнем углу — событие onClick. Это добавит атрибут опСИскк определению кнопки Show!: <input id="btnShow11 type="button11 value="Show! " onclick="return btnShow_onclick ()" /> Visual Studio 2010 также создаст пустую функцию JavaScript, которая будет вызвана при щелчке пользователя на кнопке. Внутри этой заготовки с помощью метода alert () отобразите окно сообщений на стороне клиента, которое содержит значение, введенное в текстовом поле и хранящееся в свойстве value: <script language="javascript" type="text/javascript"> // <! [CDATA[ function btnShow_onclick () { alert(txtUserMessage.value); } // 11> </script>
1224 Часть VII. Построение веб-приложений с использованием ASP.NET Обратите внимание, что блок сценария помещен в конструкцию С DATA. Причина этого проста. Если страница попадет в браузер, который не поддерживает JavaScript, код будет воспринят как блок комментариев и проигнорирован. Разумеется, страница при этом станет менее функциональной, но зато не вызовет ошибку при визуализации в таком браузере. Если вы снова просмотрите страницу в браузере, то сможете ввести сообщение и увидеть его в окне сообщений клиентской стороны (рис. 32.7). Your Message: Testing! 1 2 3! [JhawTj [ Rest J Рис. 32.7. Вызов функции JavaScript клиентской стороны Щелчок на кнопке Reset (Сброс) приводит к очистке текстового поля, поскольку эта кнопка была определена с указанием type="reset". Обратная отправка веб-серверу Эта простая HTML-страница выполняет всю функциональность в принимающем браузере. Реальная веб-страница нуждается в обратной отправке ресурса на веб-сервер, одновременно передавая все введенные данные. Как только ресурс серверной стороны принимает эти данные, он может использовать их для построения правильного ответа HTTP. С помощью атрибута action в открывающем дескрипторе <form> указывается получатель входных данных формы. К возможным получателям относятся почтовые серверы, другие HTML-файлы на веб-сервере, классические файлы (на основе COM) Active Server Pages (ASP), веб-страницы ASP.NET и т.д. Помимо атрибута action, скорее всего, также будете присутствовать кнопка отправки (submit), щелчок на которой передаст данные формы в веб-приложение через запрос HTTP В текущем примере это делать не нужно, тем не менее, ниже приведена модификация файла default.htm, в которой в открывающем дескрипторе <form> указан следующий атрибут: <form id="defaultPage" action="http: //localhost/Cars/ClassicAspPage. asp" method=llGET"> <mput id="btnPostBack11 type="submit11 value="Post to Server! "/> </form> По щелчку на кнопке отправки данные формы посылаются файлу ClassicAspPage.asp по указанному URL. Если указан method="GET" в качестве режима передачи, данные формы присоединяются к строке запроса в виде набора пар "имя/значение", разделенных амперсандами. Скорее всего, ранее вы уже видели данные подобного рода в браузере.
Глава 32. Построение веб-страниц ASP.NET 1225 Например: http://www.google.com/search?hl=en&source=hp&q=vikings&cts=12 64 37 077 3 666&aq= f&aql=&aqi=glg-zlglg-zlglg-zlg4&oq= Другой метод передачи данных формы на веб-сервер задается как method="POST": <form id="defaultPage11 action="http : //localhost/Cars/ClassicAspPage . asp" method=llPOST"> </form> В этом случае данные формы не добавляются к строке запроса. При использовании метода POST данные формы не становятся непосредственно видимыми внешнему миру. Что более важно, данные POST не имеют ограничения по длине строки; многие браузеры ограничивают длину запросов GET. Обратные отправки в ASP.NET При построении веб-сайтов на основе ASP.NET платформа самостоятельно заботится о механизме обратной отправки. Одним из многих преимуществ построения веб-сайта с использованием ASP.NET является уровень программной модели, расположенный поверх стандартной системы запроса/ответа (Request/Response) HTTP, который управляется событиями. Поэтому вместо ручной установки атрибута action и определения HTML-кнопки отправки можно просто обрабатывать события веб-элементов управления ASP.NET с применением стандартного синтаксиса С#. Используя эту управляемую событиями модель, можно очень легко осуществлять обратную отправку на веб-сервер посредством огромного числа элементов управления. При желании выполнять отправку на сервер можно, когда пользователь щелкает на переключателе, элементе окна списка, дне в календаре и т.п. В любом случае вы просто обрабатываете соответствующее событие, а исполняющая среда ASP.NET автоматически формирует и отправляет корректные данные HTML. Исходный код. Веб-сайт SimpleWebPage доступен в подкаталоге Chapter 32. Набор средств API-интерфейса ASP.NET На этом краткий обзор разработки классического веб-приложения завершен, и вы готовы к погружению в ASP.NET. Прежде чем приступать к созданию первого веб-приложения ASP NET, давайте рассмотрим основные средства API-интерфейса веб-разработки .NET, как они выглядели в разных версиях платформы. Основные средства ASP.NET 1.0-1.1 Первый выпуск ASP.NET (версия 1.x) содержал множество средств, позволяющих разработчикам строить веб-приложения в строго типизированной и объектно-ориентированной манере. Ниже перечислены некоторые ключевые средства, поддерживаемые во всех версиях платформы .NET. • Технология ASP.NET предоставляет модель отделенного кода (code-behind), которая позволяет разделять логику презентации (HTML) и бизнес-логику (код С#). • Страницы ASP.NET кодируются с применением языков программирования .NET вместо интерпретируемых языков сценариев серверной стороны. Файлы кода компилируются в действительные сборки .NET *.dll (которые, в свою очередь, транслируются в намного быстрее работающий исполняемый код).
1226 Часть VII. Построение веб-приложений с использованием ASP.NET • Веб-элементы управления позволяют программистам строить графические пользовательские интерфейсы веб-приложений в манере, подобной построению приложений Windows Forms и WPF. • По умолчанию веб-элементы управления ASP.NET автоматически сохраняют свое состояние во время обратных отправок, используя скрытое поле формы по имени VIEWSTATE. • Веб-приложения ASP.NET могут использовать любые сборки из библиотек базовых классов .NET (естественно, использовать в контексте веб-приложения графические API-интерфейсы для настольных приложений вроде Windows Forms не имеет смысла). • Веб-приложения ASP.NET могут легко конфигурироваться с использованием стандартных настроек IIS или конфигурационного файла веб-приложения (Web.config). Первое, что здесь следует подчеркнуть — это тот факт, что пользовательский интерфейс веб-страницы ASP.NET строится из любого количества веб-элементов управления. В отличие от типичного элемента управления HTML, веб-элементы управления выполняются на веб-сервере и генерируют HTTP-ответ в виде корректных HTML-дескрипторов. Одно это является огромным преимуществом ASP.NET, потому что радикально сокращает объем HTML-разметки, который необходимо писать вручную. В качестве быстрого примера предположим, что на веб-странице ASP.NET определен следующий веб-элемент управления ASP.NET: <asp:Button ID="btnMyButton11 runat="server11 Text="Button11 BorderColor="Blue11 BorderStyle="Solid11 BorderWidth=px11 /> Довольно скоро вы ознакомитесь с деталями объявления веб-элементов управления ASP.NET, а пока обратите внимание, что многие атрибуты элемента <asp:Button> выглядят очень похожими на свойства, которые можно встретить в примерах Windows Forms и WPF (BorderColor, Text, BorderStyle и т.д.). То же самое верно для всех веб- элементов управления ASP NET, потому что когда в Microsoft строили инструментарий для веб-элементов, эти графические элементы управления были намеренно спроектированы похожими на свои настольные аналоги. Если теперь браузер обращается к файлу *.aspx, содержащему этот элемент управления, то элемент отвечает выдачей в выходной поток следующего объявления HTML: <mput type="submit11 name="btnMyButton11 value="Button11 id=llbtnMyButton" style= "border-color -.Blue; border-width: 5px; border-style : Solid; " /> Отметьте, что веб-элемент выдает стандартную разметку HTML (или XHTML, в зависимости от настроек), которая может быть визуализирована в любом браузере. Учитывая это, вы должны понимать, что использование веб-элементов управления ASP.NET ни в коем случае не привязывает к семейству операционных систем Microsoft или браузеру Microsoft Internet Explorer. Веб-страницу ASP.NET можно просматривать в среде любой операционной системы или браузера (включая портативные устройства наподобие Apple iPhone или BlackBerry). В приведенном выше списке средств указано, что веб-приложение ASP.NET будет скомпилировано в сборку .NET. Поэтому веб-проекты ничем не отличаются от любой сборки .NET *.dll, построенной в примерах этой книги. Скомпилированное веб-приложение будет состоять из кода CIL, манифеста сборки и метаданных типов. Это дает ряд значительных преимуществ, из которых наиболее важными являются выигрыш в производительности, строгая типизация и способность микроуправления со стороны CLR (сборка мусора и т.п.).
Глава 32. Построение веб-страниц ASP.NET 1227 Наконец, веб-приложения ASP.NET поддерживают программную модель, посредством которой можно отделять разметку страницы от кодовой базы С#, используя файлы кода. Скоро мы вернемся к этой теме, а пока имейте в виду, что это средство устраняет распространенную причину жалоб разработчиков при построении классических (основанных на СОМ) страниц ASP, где типичные файлы *.asp содержали несогласованное сочетание HTML-разметки и кода сценариев. Использование файлов кода позволяет отобразить разметку на полноценную объектную модель, которая объединяется с файлом кода С# через объявления частичных классов. Основные средства ASP.NET 2.0 Версия ASP.NET 1 .х стала значительным шагом в нужном направлении, а ASP.NET 2.0 предоставила дополнительные средства, которые продвинули ASP.NET от создания динамических веб-страниц в направлении построения богатых средствами веб-сайтов. Ниже приведен неполный список ключевых средств. • Появился веб-сервер разработки ASP.NET (это означает, что разработчикам отныне не нужно устанавливать IIS на своих машинах). • Огромное количество новых веб-элементов управления, обрабатывающих множество сложных ситуаций (навигационные элементы, элементы управления безопасностью, новые элементы привязки данных и т.д.). • Мастер-страницы, позволяющие разработчикам присоединять общий фрейм пользовательского интерфейса к набору взаимосвязанных страниц. • Поддержка тем, которые предлагают декларативный способ изменения внешнего вида и поведения всего веб-приложения на веб-сервере. • Поддержка веб-частей (Web Parts), которые позволяют конечным пользователям настраивать внешний вид и поведение веб-страниц с сохранением настроек для последующего использования (подобно порталам). • Веб-ориентированная утилита конфигурирования и управления, которая обслуживает множество файлов Web.config. Помимо веб-сервера разработки ASP.NET, одним из самых значительных новшеств ASP.NET 2.0 было появление мастер-страниц. Большинство веб-сайтов поддерживают согласованный внешний вид для всех страниц сайта. Возьмем коммерческий веб-сайт вроде www.amazon.com. Каждая страница содержит одни и те же элементы, т.е. общий заголовок, общий нижний колонтитул, общее меню навигации и т.д. С помощью мастер-страниц можно моделировать эту общую функциональность и определять места заполнения для подключения других файлов *.aspx. Это существенно облегчает изменение общего вида сайта (положения панели навигации, логотипа и т.п.) простым изменением мастер-страницы, оставляя другие файлы *.aspx неизменными. На заметку! Мастер-страницы настолько удобны, что в Visual Studio 2010 все новые веб-страницы ASP.NET включают их по умолчанию. В ASP.NET 2.0 появилось множество новых веб-элементов управления, включая элементы, которые автоматически содержат общие средства безопасности (элементы входа на сайт, элементы восстановления пароля и т.д.), элементы, которые позволяют наложить навигационную структуру поверх связанных файлов *.aspx, и еще больше элементов для выполнения сложных операций привязки данных, в которых необходимые запросы SQL могут генерироваться с использованием веб-элементов управления ASP.NET.
1228 Часть VII. Построение веб-приложений с использованием ASP.NET Основные средства ASP.NET 3.5 (и .NET 3.5 SP1) В платформе .NET 3.5 веб-приложения ASP.NET получили возможность использовать программную модель LINQ (также появившуюся в .NET 3.5) и следующие веб-ориентированные средства. • Новые элементы управления для поддержки разработки Silverlight (вспомните, что это API-интерфейс на основе WPF для проектирования развитого медиа-содержимого веб-сайта). • Поддержка привязки данных для сущностных классов ADO.NET (см. главу 23). • Поддержка динамических данных ASP.NET Dynamic Data. Это платформа, подобная Ruby on Rails, которая может использоваться для построения веб-приложений, управляемых данными. Она представляет таблицы в базе данных, кодируя их в URI веб-службы ASP.NET, и данные в таблице автоматически визуализируются в виде HTML-разметки. • Интегрированная поддержка разработки в стиле Ajax, которая, по сути, позволяет выполнять "обратные микро-отправки" для обновления части веб-страницы насколько возможно быстро. Одним из наиболее долгожданных средств ASP.NET 3.5 стал новый набор элементов управления, готовых к Ajax-разработке. Этот веб-ориентированный API-интерфейс позволяет максимально эффективно обновлять небольшие части более крупной веб-страницы. Хотя Ajax может применяться любым API-интерфейсом веб-разработки, делать это на сайте ASP.NET очень просто, потому что элементы управления Ajax выполняют всю рутинную работу, генерируя необходимый код JavaScript клиентской стороны. Шаблоны проектов ASP.NET Dynamic Data, представленные в .NET 3.5 Service Pack 1, предлагают новую модель построения сайтов, основанных на реляционной базе данных. Конечно, большинство веб-сайтов в определенной мере нуждаются во взаимодействии с базами данных, но проекты ASP.NET Dynamic Data тесно привязаны к ADO.NET Entity Framework и сосредоточены на быстрой разработке сайтов, управляемых данными (подобных тем, которые можно построить с помощью Ruby). Основные средства ASP.NET 4.0 Теперь мы подобрались вплотную к веб-средствам, поставляемым с текущей версией платформы — .NET 4.O. Помимо общих средств .NET 4.0, доступны следующие ключевых веб-ориентированные средства. • Возможность сжатия данных "состояния представления" с использованием стандарта GZIP • Обновленные определения браузеров для обеспечения корректной визуализации страниц ASP.NET в новых браузерах и устройствах (Google Chrome, Apple IPhone, устройства BlackBerry и т.п.). • Возможность настройки вывода элементов, выполняющих проверку достоверности, с применением каскадных таблиц стилей (CSS). • Включение элемента управления ASP.NET Chart, который позволяет строить страницы ASP.NET, включающие диаграммы для сложного статистического и финансового анализа. • Официальная поддержка шаблона проектов ASPNET Model View Controller, которая сокращает зависимости между уровнями приложения, используя шаблон MVC (Model-View-Controller — модель-представление-контроллер).
Глава 32. Построение веб-страниц ASP.NET 1229 Обратите внимание, что этот список средств ASP.NET 1.0-4.0 ни в коем случае не претендует на полноту, но дает представление о том, что на самом деле представляет собой веб-ориентированный API-интерфейс. По правде говоря, если попытаться охватить все средства ASP.NET, эта книге стала бы вдвое (а то и втрое) толще. Поскольку это нереально, далее рассматриваются базовые средства ASP.NET, которые, скорее всего, будут использоваться в повседневной работе. Исчерпывающие описания всех средств ASP.NET можно найти в документации .NET Framework 4.0 SDK. На заметку! Если нужно хорошее руководство по разработке веб-приложений в ASP.NET, рекомендуется обратить внимание на книгу Microsoft ASP.NET 3.5 с примерами на С# 2008 и Silverlight 2 для профессионалов (ИД "Вильяме", 2009 г.). Построение однофайловой веб-страницы ASP.NET Как было указано, веб-страница ASP.NET может конструироваться на основе двух подходов, один из которых предполагает построение единственного файла *.aspx, содержащего смесь кода серверной стороны и HTML-разметки. Эта модель однофайловой страницы предполагает помещение кода серверной стороны внутрь области <script>, но сам код не является кодом языка сценариев (например, VBScript /JavaScript). Вместо этого код внутри блока <script> пишется на выбранном вами языке .NET (C#, Visual Basic и т.п.). Если создаваемая веб-страница содержит очень немного кода (но значительный объем статической HTML-разметки), то модель однофайловой страницы более удобна для работы, поскольку она позволяет видеть код и разметку в одном файле *.aspx. Вдобавок помещение процедурного кода и HTML-разметки в единый файл *.aspx обеспечивает еще несколько других преимуществ. • Страницы, написанные в соответствии с однофайловой моделью, несколько легче развертывать или отправлять другим разработчикам. • Поскольку нет зависимости между несколькими файлами, такую страницу проще переименовывать. • Упрощается управление файлами в системе управления исходным кодом, поскольку все действия выполняются с единственным файлом. Недостаток модели однофайловой страницы в том, что она порождает те же проблемы, которые существовали в классическом ASP на основе СОМ: единственный файл делает слишком много (определение разметки пользовательского интерфейса и программной логики в одном месте). Тем не менее, начнем путешествие по ASP.NET именно с рассмотрения модели однофайловой страницы. Цель будет заключаться в построении файла *.aspx, который отобразит таблицу Inventory базы данных AutoLot (созданной в главе 21) с использованием подключенного уровня (разумеется, можно было бы также применить и автономный уровень или Entity Framework). Запустите Visual Studio 2010 и создайте новую веб-форму (Web Form), выбрав пункт меню File^New^File (Файл■=>Новый»=>Файл), как показано на рис. 32.8. Сохраните файл под именем Default.aspx в новом каталоге на жестком диске, чтобы позже его можно было легко найти (например, C:\MyCode\SinglePageModel). Ссылка на c6opKyAutoLotDAL.dll Воспользуйтесь проводником Windows, чтобы создать подкаталог bin внутри папки SinglePageModel. Специально именованный подкаталог bin — это зарегистрированное имя для механизма исполняющей среды ASP.NET. В папку \bin корня веб-сайта можно поместить любые приватные сборки, используемые веб-приложением. Для рассматриваемого примера поместите копию AutoLotDAL.dll (см. главу 21) в папку C:\MyCode\SinglePageModel\bin.
1230 Часть VII. Построение веб-приложений с использованием ASP.NET Installed Templates General Performance Web Visual Bask C* Visual C++ Scr.pt 3 Master Page j Web Form ^ fj . Web User Control i %\ HTML Page ф] Web Service dU Class Ш StyleSheet Per user extensions are currently not allowed to load. Рис. 32.8. Создание новой однофайловой страницы ASP.NET На заметку! Как будет показано далее в этой главе, при использовании Visual Studio 2010 для создания полноценного веб-приложения ASP.NET интегрированная среда разработки создает папку \bin автоматически. Проектирование пользовательского интерфейса Выберите вкладку Standard (Стандартные) в панели инструментов Visual Studio 2010 и перетащите элементы управления Button, Label и GridView на поверхность визуального конструктора страницы (виджет GridView находится на вкладке Data (Данные) панели инструментов) между открывающим и закрывающим элементами form. Воспользуйтесь окном Properties для установки различных визуальных свойств и назначения каждому виджету подходящего имени через атрибут ID. На рис. 32.9 показан один из возможных вариантов дизайна. (Пример характерен весьма скромным внешним видом и поведением, чтобы объем сгенерированной разметки элементов управления был минимальным, однако ничего не мешает усложнить его по своему усмотрению.) Ibodvi СКск on the Button to Fffl the Grid \ Column!) Column 1 Column2 1 abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc Fill Grid | 3 Design [a Split"] Ш Source f pi]phtml>| <body> ™ ! i i I Рис. 32.9. Пользовательский интерфейс Default.aspx
Глава 32. Построение веб-страниц ASP.NET 1231 Теперь найдите на странице раздел <form>. Обратите внимание, что каждый веб-элемент управления определен с использованием дескриптора <asp:>. После этого префикса дескриптора указывается имя веб-элемента управления ASP.NET (Label, GridView и Button). Перед закрывающим дескриптором заданного элемента находится серия пар "имя/значение", которые соответствуют настройкам, проведенным в окне Properties: <form id=llforml" runat=llserver"> <div> <asp:Label ID="lblInfo11 runat="server" Text="Click on the Button to Fill the GridM> </asp:Label> <br /> <br /> <asp:GridView ID="carsGridView11 runat="server"> </asp:GridView> <br /> <asp:Button ID=,,btnFillDataM runat="server" Text="Fill Grid" /> </div> </form> Все детали веб-элементов управления ASP.NET будут рассматриваться в главе 33. А пока просто запомните, что веб-элементы управления — это объекты, обрабатываемые На веб-сервере, который автоматически возвращает их HTML-представление в исходящем HTTP-запросе. Помимо этого главного преимущества, веб-элементы управления ASP.NET имитируют модель программирования настольных приложений, при которой имена свойств, методов и событий обычно совпадают с их аналогами из Windows Forms/WPF. Добавление логики доступа к данным Обработайте событие Click для типа Button, используя либо окно Properties среды Visual Studio 2010 (через значок с изображением молнии), либо раскрывающиеся списки в верхней части окна визуального конструктора (как это делалось в разделе с обзором HTML этой главы). Сделав это, вы обнаружите, что определение Button обновлено добавлением атрибута OnClick, которому присвоено имя обработчика события Click: <asp:Button ID="btnFillData11 runat="server11 Text=MFill Grid" OnClick=,,btnFillData_Click,,/> Теперь в блоке <script> серверной стороны внутри файла *.aspx необходимо реализовать обработчик события Click. Добавьте следующий код, следя за тем, чтобы входные параметры в точности соответствовали цели делегата System.EventHandler, который использовался во многих примерах этой книги: <script runat=llserverM> protected void btnFillData_Click(object sender, EventArgs args) { } </script> Следующий шаг связан с наполнением GridView посредством функциональности сборки AutoLoDAL.dll. Для этого с помощью директивы <%@lmport%> нужно указать, что будет использоваться пространство имен AutoLotConnectedLayer. На заметку! Если страница строится в соответствии с однофайловой моделью, нужно будет лишь использовать директиву <%@ Import %>. Если применяется подход по умолчанию на основе файла кода, необходимые пространства имен включаются с помощью ключевого слова С# using. То же самое касается описанной ниже директивы <%@ Assembly%>.
1232 Часть VII. Построение веб-приложений с использованием ASP.NET Вдобавок нужно информировать исполняющую среду ASP.NET о том, что эта однофай- ловая страница ссылается на сборку AutoLoDAL.dll, через директиву <°5@Assemblyi> (подробнее о директивах будет сказано ниже). Ниже показана оставшаяся существенная логика страницы из файла Default.aspx (при необходимости измените строку соединения): <n@ Page Language=,,C#" %> <%@ Import Namespace = "AutoLotConnectedLayer" %> <%@ Assembly Name ="AutoLotDAL" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtmll/DTD/xhtmll-transitional.dtd"> <script runat="server"> void btnFillData_Click(object sender, EventArgs args) { InventoryDAL dal = new InventoryDAL(); dal.OpenConnection(@"Data Source=(local)\SQLEXPRESS;" "Initial Catalog=AutoLot;Integrated Security=True"), carsGridView.DataSource = dal.GetAllInventory (); carsGridView.DataBind(); dal.CloseConnection (); </script> <html xmlns= ■http://www.w3.org/1999/xhtml" Click on the Button to Fill the Gnd СагШ 83 107 555 678 904 1000 1001 1992 Make Ford Ford Ford Yugo \W BMW BMW Saab Color Rust Red Yellow Green Black Black Tan Pmk PetName Rnstv Snake Buzz Clunker Hank гзппшег Daisy Pmkey </html> Прежде чем погрузиться в детали формата файла *.aspx, давайте произведем пробный запуск. Первым делом, сохраните файл *.aspx. Теперь щелкните правой кнопкой мыши в любом месте визуального конструктора *.aspx и выберите в контекстном меню пункт View in Browser (Просмотр в браузере). Это запустит веб-сервер разработки ASP.NET, который, в свою очередь, развернет страницу. После обработки страницы сначала будут видны элементы управления Label и Button. Однако после щелчка на кнопке произойдет обратная отправка на веб-сервер и веб-элементы управления визуализируют соответствующие HTML-дескрипторы. На рис. 32.10 показан вывод, полученные в результате щелчка на кнопке Fill Grid (Заполнить сетку). В нынешнем виде пользовательский интерфейс довольно скромен. Чтобы улучшить текущий пример, выберите элемент управления GridView в визуальном конструкторе и в контекстном меню (открывающемся с помощью крошечной стрелки в левом верхнем углу элемента) выберите опцию Auto Format (Автоформат), как показано на рис. 32.11. В открывшемся диалоговом окне выберите подходящий шаблон (например, Slate). После щелчка на кнопке О К просмотрите сгенерированное объявление элемента управления, которое будет существенно больше предыдущего: <asp : GridView ID=llcarsGridViewM runat="server11 BackColor="White11 BorderColor=M#E7E7FF" BorderStyle=MNoneM BorderWidth="lpx" CellPadding=" GridLines = llHorizontal"> <AlternatingRowStyle BackColor="#F7F7F7M /> <FooterStyle BackColor=M#B5C7DEM ForeColor=M#4A3C8CM /> 1 Fill Gnd 1 Рис. 32.10 ASP.NET поддерживает декларативную модель привязки данных
Глава 32. Построение веб-страниц ASP.NET 1233 <HeaderStyle BackColor=M#4A3C8CM Font-Bold=MTrueM ForeColor=M#F7F7F7M /> <PagerStyle BackColor=M#E7E7FFM ForeColor="#4A3C8CM HorizontalAlign=MRightM /> <RowStyle BackColor=M#E7E7FF" ForeColor=M#4A3C8CM /> <SelectedRowStyle BackColor=M#738A9C" Font-Bold=MTrue" ForeColor=M#F7F7F7" /> <SortedAscendingCellStyle BackColor=M#F4F4FDM /> <SortedAscendingHeaderStyle BackColor=M#5A4C9DM /> <SortedDescendingCellStyle BackColor="#D8D8F0M /> <SortedDescendingHeaderStyle BackColor="#3E3277M /> </asp:GridView> CKck on the Button to Fffl the Grid GrtdVtew Tasks ! asp:GridView*carsGrtdViewj СolutunO Colonial С olumu2 . abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc J Set control formatting properties у Edit Columns... Add New Column... ^ ! Edit Templates iS Q Design i n Split j ЕВ Source j \ij j <fcm#forml>j j <div> j <asp:GridView#carsGridView> [►! Рис. 32.11. Более развитый элемент управления GridView Снова запустив приложение и щелкнув на кнопке Fill Grid, вы увидите более интересный пользовательский интерфейс (рис. 32.12), отображаемый в браузере Apple Safari в среде Windows 7. Просто, не правда ли? Как всегда, сложности кроются в деталях, поэтому давайте немного углубимся в композицию этого файла *.aspx, начав с роли директивы <%@Раде ... %>. Имейте в виду, что рассмотренные здесь темы применимы непосредственно к более предпочтительной модели файла кода, которая описана ниже. Ф http://localrrast:7G24^inglePageMod. ■4 Ш I + i: Apple f$ http://locc С Q- Yahoo! Google Ma | СЬск on the Button to Fffl the Grid ■ CarlD Make 8 83 j 107 i 555 1 678 1 904 1 1000 1001 1 1992 Ford Ford Ford Yugo VW BMW BMW Saab [ Fill Grid ] Color PetNaniel Rust Rusty Red Snake YeBow Buzz Green Clunker Black Hank Black Bimmer Tan Daisy Pink Pinkey [pjateil1 Google » к YouTube » j. \ j j I Роль директив ASP.NET Типичный файл *.aspx обычно открывается набором директив. Директивы ASP.NET всегда помечаются маркерами <%@ ... %> и могут быть квалифицированы различными атрибутами, информирующими исполняющую среду ASP.NET о том, как следует обрабатывать каждый атрибут. Каждый файл *.aspx должен иметь, как минимум, директиву <%@Раде%>, которая служит для определения управляемого языка, применяемого внутри страницы (в атрибуте language). Также директива <%@Раде%> может определять имя связанного файла отделенного кода (рассматривается ниже) и т.д. В табл. 32.1 перечислены наиболее интересные атрибуты директивы <%@Раде%>. Рис. 32.12. Простая веб-страница в Apple Safari
1234 Часть VII. Построение веб-приложений с использованием ASP.NET Таблица 32.1. Избранные атрибуты директивы <%@Раде%> Атрибут Назначение CodePage Указывает имя связанного файла отделенного кода EnableTheming Указывает, поддерживают ли элементы управления страницы *.aspx TeMbiASP.NET EnableViewState Указывает, поддерживается ли состояние представления между запросами страницы (подробнее об этом свойстве читайте в главе 33) Inherits Определяет класс в файле отделенного кода, от которого наследуется страница; может быть любым классом, производным от System.Web.UI. Page MasterPageFile Устанавливает мастер-страницу, используемую с текущей страницей *.aspx Trace Признак включения трассировки В дополнение к директиве <%@Раде%>, заданный файл *.aspx может задавать различные директивы <96@Import%> для явной установки пространств имен, необходимых текущей странице, и директив <96@Assembly%> для указания внешних библиотек кода, используемых сайтом (обычно размещаемых в папке \bin веб-сайта). В данном примере мы указываем, что применяются типы из пространства имен AutoLotConnectedLayer сборки AutoLotDAL.dll. Как и можно было предположить, если необходимо использовать дополнительные пространства имен .NET, нужно просто указать несколько директив <%@Import%>/< b@Assembly%>. При вашем нынешнем уровне знаний .NET может возникнуть вопрос: как удается избежать указания дополнительных директив <?6@Import%> для получения доступа к пространству имен System, чтобы иметь доступ к типам System.Object и System. EventHandler (помимо прочих)? Дело в том, что все страницы *.aspx автоматически имеют доступ к набору ключевых пространств имен, определенных в файле web. con fig в пути установки платформы .NET. Внутри этого XML-файла присутствует множество автоматически импортируемых пространств имен: <pages> <namespaces> <add namespace=llSystem"/> <add namespace="System.Collections"/> <add namespace="System.Collections.Generic"/> <add namespace="System.Collections.Specialized"/> <add namespace="System.Configuration"/> <add namespace="System.Data.Entity.Design" /> <add namespace="System.Data.Ling" /> <add namespace="System.Ling" /> <add namespace="System.Text"/> <add namespace="System.Text.RegularExpressions"/> <add namespace="System.Web"/> <add namespace="Systern.Web.Caching"/> <add namespace="System.Web.SessionState"/> <add namespace="System.Web.Security"/> <add namespace="System.Web.Profile"/> <add namespace="System.Web.UI"/> <add namespace="System.Web.UI.WebControls"/> <add namespace="System.Web.UI.WebControls.WebParts"/> <add namespace="System.Web.UI.HtmlControls"/> </namespaces> </pages>
Глава 32. Построение веб-страниц ASP.NET 1235 Помимо упомянутых < u@Pagen6>, <uc@Import°6> и <%@Assembly%>, в ASP.NET определено множество других директив, которые могут появляться в файле *.aspx; их описание будет дано позже. Примеры применения других директив вы найдете далее в главе. Анализ блока script При однофайловой модели страницы файл *.aspx может содержать логику сценариев серверной стороны, которая выполняется на веб-сервере. Учитывая это, критически важно, чтобы все блоки кода серверной стороны были определены для выполнения на сервере с помощью атрибута runat="server". Если атрибут runat="server" не указан, исполняющая среда предполагает, что пишется блок сценария клиентской стороны, встраиваемый в ответ HTTP, и она сгенерирует исключение. Ниже показано, как будет выглядеть блок <script>. На заметку! Все веб-элементы управления ASP.NET должны иметь атрибут runat="server" в своем открывающем объявлении. В противном случае они не будут визуализировать HTML- разметку в выходной ответ HTTP. <script runat="server"> void btnFillData_Click(object sender, EventArgs args) { InventoryDAL djl = new InventoryDAL(); dal.OponConnection(@"Data Source=(local)\SQLEXPRESS;" + "Initial Cjtalog=AutoLot;Integrated Secunty=True") ; carsGridView.UataSource = dal.GetAllInventory (); carsGndView. LataBind () ; dal. Clc jeConri1 rtion(); } </script Сигнатура этого вспомогательного метода должна показаться до странности знакомой. Вспомните, что обработчик событий каждого элемента управления должен соответствовать шаблону, определенному связанным делегатом .NET. Делегатом является System.EventHandler, который может вызывать только методы, принимающие System.Object в первом параметре и System.EventArgs — во втором. Анализ объявлений элементов управления ASP.NET Последний момент, который нас интересует в этом первом примере — объявление веб-элементов управления Button, Label и GridView. Подобно классическому ASP и чистому HTML, веб-виджеты ASP.NET помещаются в область элементов <f orm>. На этот раз, однако, открывающий дескриптор <form> помечен атрибутом runat="server" и квалифицирован префиксом asp:tag. Любой элемент, который получает этот префикс, является членом библиотеки элементов управления ASP.NET и имеет соответствующее представление в виде класса С# в определенном пространстве имен .NET библиотеки базовых классов .NET. Вот что здесь находится: <form id=nforml" runat="server"> <div> <asp:Label ID="lbllnfo" runat="server" Text="Click or. the Button to Fill the Grid"> </asp:Label> <br A <br /> <asp:GridView ID="carsGridView" runat="server"> </asp:GridView>
1236 Часть VII. Построение веб-приложений с использованием ASP.NET <Ьг /> <asp:Button ID="btnFillDatan runat="server" Text="Fill Grid" OnClick="btnFillData_Click"/> </div> </form> Пространство имен System.Web.Ul.WebControls сборки System.Web.dll содержит большинство веб-элементов управления ASP.NET. Открыв браузер объектов Visual Studio 2010, можете найти там, например, элемент управления Label (рис. 32.13). ШШШШшШШ V AddAttributesToRender(System.VVeb.Ul.HtmlTeictWriter) v AddParsedSubObject(object) * Lab*l() j* LoadViewState(object) J* RenderContentstSystem.Web.ULHtmrrextWriter) 5P AssociatedControlID _5* SupportsDisabledAttnbute !!§ TagKey S"Tort public class Label : * Systeff.Web.UI.WebControlsWebControl [sf Member of System.Web UI.WebControlt Summary: Represents a label control, which displays text on a Web Pacle- Рис. 32.13. Все объявления элементов управления ASP.NET отображаются на типы классов .NET Как видите, веб-элемент управления ASP.NET имеет в самой вершине своей цепочки наследования System.Object. Родительский класс WebControl — это общая база всех элементов управления ASP.NET, и он определяет все общие свойства пользовательского интерфейса, которых можно было ожидать (BackColor, Height и т.д.). Класс Control также очень распространен, однако в нем определены члены, больше ориентированные на инфраструктуру (привязка данных, состояние представления и т.п.), а не на внешний вид дочерних элементов. Больше об этих классах вы узнаете в главе 33. Цикл компиляции для однофайловых страниц В случае использования однофайловой модели страницы HTML-разметка, блоки <script> серверной стороны и определения веб-элементов управления динамически компилируются в класс, унаследованный от System.Web.UI.Page. Имя этого класса основано на имени файла *.aspx с добавлением суффикса aspx (т.е. страница под названием MyPage.aspx становится типом класса по имени MyPageaspx). На рис. 32.14 показан базовый процесс. Эта динамически скомпилированная сборка развертывается в определенный исполняющей средой подкаталог под корневым каталогом C:\WINDOWS\Microsoft.NET\ Framework\v4.0\Temporary ASP.NET Files. Конкретный путь ниже этого корня будет отличаться в зависимости от множества факторов (хеш-коды и т.п.), но если хорошо поискать, то в конечном итоге обнаружится нужный файл *.dll (вместе с файлами поддержки). На рис. 32.15 показана сгенерированная сборка для примера SinglePageModel, приведенного ранее в этой главе. Открыв эту сборку в такой утилите, как ildasm.exe или reflector.exe, скорее всего, вы обнаружите там класс, расширяющий Page, который каждый веб-элемент управления определяет как член класса. * Й> Base Types л +% WebControl s *$ Control ,-> ** {Component • *° KontrolBuilderAccessor 5> *° IControlDesignerAccessor f» *"° IDataBindingsAccessor j> -o {Disposable t> *"° lExpressionsAccessor и «о IParserAccessor > -° RMResoJutionService > Ш Object л ~o IAttributeAccessor : -° ITextControl
Глава 32. Построение веб-страниц ASP.NET 1237 Автоматически сгенерированная сборка, размещенная ПОД aspnet_wp.ехе Sys tem. Web. UI. Page (базовый класс для всех классов *_aspx) MyPage_aspx (динамически определенный и скомпилированный тип класса) Компилятор времени выполнения MyPage.aspx <html> </html> Рис. 32.14. Модель компиляции однофайловых страниц ^fcM « 9f6clb2e Organize ▼ [ЦП} Open ■ ~"~ J щ Favorites ■ Desktop |j] Recent Places #§ Libraries [j Documents J> Music h.: Pictures ► 2695e349 ► with... Burn a w ** 1 ■^earc^ ^695e34S New folder |£E * Q| Name ju assembly 1 hash App_Web_aibvxsuy.dll [j App_Web_aibvxsuy.dll.delete Й App_Web.b3daqmtk.dll _V] App_Web_default.aspx.cdcab7d2.h_g6va... Ш p\ ~®П Da1 * L ;;I 1/2 1!2 _. I Рис. 32.15. Автоматически сгенерированная сборка ASP.NET Имейте в виду, что вряд ли когда-либо понадобится вручную искать или модифицировать эти автоматически сгенерированные сборки. Однако важно отметить, что в случае изменения любого аспекта файла *.aspx он будет динамически перекомпилирован, когда браузер запросит веб-страницу. Исходный код. Веб-сайт SinglePageModel доступен в подкаталоге Chapter 32. Построение веб-страницы ASP.NET с использованием файлов кода Хотя однофайловая модель иногда полезна, подход по умолчанию, принятый в Visual Studio 2010 (при создании нового веб-проекта) состоит в применении техники отделенного кода (code behind), которая позволяет разделить код программы и логику HTML- представления, используя для них два разных файла. Эта модель работает достаточно хорошо, когда страница содержит существенный объем кода или когда несколько разработчиков создают один веб-сайт.
1238 Часть VII. Построение веб-приложений с использованием ASP.NET Модель отделенного кода предоставляет следующие преимущества. • Поскольку страницы с отделенным кодом обеспечивают четкое разделение HTML- разметки и кода, можно организовать параллельную работу дизайнеров над разметкой и программистов — над кодом С#. • Код не предоставляется дизайнерам страниц или прочему персоналу, который имеет дело только с разметкой страницы (как и можно было предположить, тем, кто занимается HTML-разметкой, не всегда интересно видеть внутренности кода С#). • Файлы кода можно использовать с несколькими файлами *.aspx. Выбранный подход никак не отражается на производительности. В действительности многие веб-приложения ASP.NET выигрывают от построения сайтов с применением обоих подходов. Чтобы проиллюстрировать модель с отделенным кодом, давайте пересоздадим предыдущий пример, используя чистый шаблон Web Site в Visual Studio 2010 (обратите внимание, что Visual Studio 2010 не обязывает строить страницы с отделенным кодом). Активизируйте пункт меню File^New^Web Site (Файл1^ Создать1^ Веб-сайт) и выберите шаблон Empty Web Site (Пустой веб-сайт), как показано на рис. 32.16. WCF Service Visual C# ASP.NET Reports Web Site Visual С* pf Dynamic Data Linq to SQL Web Site Visual C* Рис. 32.16. Выбор шаблона Empty Web Site На рис. 32.16 видно, что можно выбирать местоположение нового сайта. Выбор в списке Web location (Веб-расположение) варианта File System (Файловая система) обеспечивает помещение файлов с содержимым в локальный каталог с развертыванием страниц в веб-сервере разработки ASP.NET. В случае выбора варианта FTP or HTTP (FTP или HTTP) сайт будет развернут в новом виртуальном каталоге, обслуживаемом IIS. В рассматриваемом примере не имеет значения, какой вариант будет выбран, но для простоты выберите File System и укажите новую папку по имени C:\CodeBehmdPageModel. На заметку! Шаблон проекта Empty Web Site автоматически включает файл web. con fig, который похож на файл App.conf ig для настольного приложения. Формат этого файла рассматривается далее в этой главе. Теперь, выбрав пункт меню Website^Add New Item... (Веб-сайт1^Добавить новый элемент), вставьте новую веб-форму по имени Default.aspx. Обратите внимание, что по умолчанию флажок Place code in separate file (Поместить код в отдельный файл) автоматически отмечен, что и требуется (рис. 32.17).
Глава 32. Построение веб-страниц ASP.NET 1239 Sort by: •-'__._-] □ # i_y щ ill Web Form Master Page Web User Control XSLT File Web Configuration File XML Schema 3 QD Visual C# Visual C# Visual C# Visual C* Visual C# Visual C# Ш Search installed Templates '. Type: Visual C# = ! A form for Web Applications :odt in separate fileK Select master page Рис. 32.17. Вставка новой веб-формы с разделением кода Воспользуйтесь визуальным конструктором для построения пользовательского интерфейса, состоящего из элементов Label, Button и GridView, и в окне Properties настройте его по собственному усмотрению. Как видите, директива <%@Раде%> дополнена новыми атрибутами: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits=' Default" %> Атрибут С ode File используется для указания соответствующего внешнего файла, который содержит логику кода для этой страницы. По умолчанию файлы отделенного кода именуются с добавлением суффикса .cs к имени файла *.aspx (в данном примере — Default.aspx.cs). Заглянув в Solution Explorer, вы увидите, что файл отделенного кода обнаруживается в подузле со значком веб-формы (рис. 32.18). Открыв файл отделенного кода, вы найдете в нем частичный класс, унаследованный от System.Web. Ul.Page, с поддержкой обработки события Load. Обратите внимание, что имя этого класса идентично атрибуту Inherits внутри директивы <%@Раде%>: _Default : System.Web.UI.Page Solution Explorer * 3 Solution 'CodeBehindPageModel' A project) A j H:Y.ACodeBeh«ndPaaeModel\ л Ш Default.aspx *£ Default.aspx.es j» web.config $5 Solution Explo.. Рис. 32.18. Ассоциированный файл отделенного кода для заданного файла *.aspx public partial class { protected void Page_Load(object sender, EventArgs e) { } Ссылка на c6opKyAutoLotDAL.dll Как упоминалось ранее, при создании проекта веб-приложения с использованием Visual Studio 2010 не приходится вручную создавать подкаталог \bin и копировать в него приватные сборки. Для целей данного примера откройте диалоговое окно Add Reference (Добавить ссылку), выбрав соответствующий пункт меню, и укажите ссылку на AutoLotDAL.dll. После этого в Solution Explorer появится новая папка \bin (рис. 32.19).
1240 Часть VII. Построение веб-приложений с использованием ASP.NET I Solution Explorer * В X I I , ^ d , ' Я\ £• I ^33 Solution 'CodeBehindPageModeC (I project) I ^ # №U\CodeBehtndPageModel\ л ._,■ Bin j:. AutoLotDALdll; л Щ Default.aspx S|| Defauft.aspx.cs (isji web.config Я *Щ Solution Exp... I Рис. 32.19. Visual Studio обычно поддерживает специальные папки ASP.NET Обновление файла кода Обработаем событие Click для элемента Button, дважды щелкнув на Button в визуальном конструкторе. Как и ранее, к определению Button добавляется атрибут OnClick. Однако обработчик события серверной стороны теперь не помещается в область <script> файла *.aspx, а становится методом типа класса Default. Для завершения этого примера добавьте в файл отделенного кода оператор using для AutoLotConnectedLayer и реализуйте обработчик, используя прежнюю логику: using AutoLotConnectedLayer; public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btnFillData_Click(object sender, EventArgs e) { InventoryDAL dal = new InventoryDAL(); dal.OpenConnection(@"Data Source=(local)\SQLEXPRESS;" + "Initial Catalog=AutoLot;Integrated Security=True"); carsGridView.DataSource = dal.GetAllInventory(); carsGridView.DataBind(); dal.CloseConnection (); } } Теперь можно запустить веб-сайт, нажав комбинацию клавиш <Ctrl+F5>. Как и прежде, при этом запустится веб-сервер разработки ASP.NET и передаст страницу в принимающий браузер. Цикл компиляции многофайловых страниц Процесс компиляции страницы, построенной на основе модели отделенного кода, подобен компиляции однофайловых страниц. Однако тип, унаследованный от System. Web.UI.Page, здесь состоит из трех файлов вместо ожидаемых двух. Вспомните, что файл Default.aspx был подключен к частичному классу по имени Default в файле отделенного кода. В дополнение третий аспект сгенерированного частичного класса содержит код в памяти, который корректно устанавливает свойства и события веб-элементов управления. В любом случае, как только сборка создана по начальному HTTP-запросу, она будет повторно использоваться всеми последующими запросами, и потому в перекомпиляции не нуждается. Понимание этого факта объясняет, почему первый запрос страницы *.aspx выполняется дольше, а все последующие запросы той же страницы — намного быстрее.
Глава 32. Построение веб-страниц ASP.NET 1241 На заметку! Под управлением ASP.NET теперь можно перекомпилировать все страницы (или подмножество страниц) веб-сайта, используя инструмент командной строки по имени aspnet_ compiler.exe. Подробную информацию ищите в документации .NET Framework 4.0 SDK. Отладка и трассировка страниц ASP.NET При построении веб-проектов ASP.NET можно применять те же приемы отладки, что и в любых других типах проектов Visual Studio 2010, те. размещать точки останова в файле отделенного кода (а также во встроенных блоках <script> внутри файла *.aspx), запускать сеанс отладки (по умолчанию клавишей <F5>) и пошагово выполнять код. Однако чтобы отлаживать веб-приложения ASP.NET, сайт должен содержать свойство, сконфигурированное в файле Web. con fig. По умолчанию все проекты Visual Studio 2010 автоматически получают свой файл Web.conf ig. Однако поддержка отладки в них изначально отключена (поскольку это снижает производительность). Когда вы начнете сеанс отладки, IDE-среда предупредит о необходимости модификации Web. con fig для включения отладки. Согласившись сделать это, элемент <compilation> в файле Web.config будет обновлен следующим образом: <compilation debug="true" targetFramework=.0"/> Кроме того, также можно включить поддержку трассировки для файла *.aspx, установив атрибут Trace в true внутри директивы <%@Раде%> (также возможно включить трассировку для всего сайта, внеся изменения в файл Web.config): <и@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.es" Inherits="_Default" Trace="true" %> После этого генерируемая HTML-разметка будет содержать множество деталей о предыдущем запросе/ответе HTTP (серверные переменные, переменные сеанса и приложения, запросы/ответы и т.п.). Чтобы вставить собственные сообщения трассировки в эту смесь, можно воспользоваться свойством Trace из типа System.Web.UI.Page. Всякий раз, когда нужно протоколировать специальное сообщение (из блока <script> или файла исходного кода С#), просто вызывайте статически метод Trace.Write(). Первый аргумент представляет имя специальной категории, а второй — сообщение трассировки. Для иллюстрации модифицируйте обработчик Click элемента Button следующим образом: protected void btnFillData_Click(object sender, EventArgs e) { Trace.Write("CodeFileTracelnfо!", "Filling the grid!"); } Запустите проект снова и щелкните на кнопке. В журнале будет присутствовать специальная категория и специальное сообщение. На рис. 32.20 обратите внимание на подсвеченное сообщение, отображающее информацию трассировки. К настоящему моменту вы узнали, как строится веб-страница ASP.NET с использованием однофайлового подхода и подхода на основе файла кода. Оставшийся материал этой главы посвящен композиции веб-проекта ASP.NET, способам взаимодействия с запросами/ответами HTTP и жизненному циклу класса, унаследованного от Page. Однако прежде чем двигаться дальше, следует прояснить разницу между веб-сайтом ASP.NET и веб-приложением ASP.NET Исходный код. Веб-сайт CodeBehindPageModel доступен в подкаталоге Chapter 32.
1242 Часть VII. Построение веб-приложений с использованием ASP.NET W Q http://loc»l [ <- С I aspx.page I aspx.page 1 aspx.page I aspx.page I aspx.page I aspx.page I aspx.page aspx.page I aspx.page '• -■ - SostlS Ub/Lu... ^R •& http://localhost:1946/CodeBeh;ndPageModei/Defaultaspx End Raise ChangedEvents Begin Raise PostBackEvent End Raise PostBackEvent Begin LoadComplete End LoadComplete Begin PreRender End PreRender Begin PreRenderComplete End PreRenderComplete 0.0O0338293434419456 0.000348277122608907 0.000749544590223358 0.00184659832704105 0.00186579772740538 0.00187693337961669 0.00188653307979885 0.00190S73248016318 0.0019164841443672 0.0019268S182056394 ► D- A- 0.000010 * I 0.000010 0.000401 =\ 0.001097 0.000019 0.000011 0.000010 0.000019 0.000011 0.000010 - I Рис. 32.20. Протоколирование специальных сообщений трассировки Веб-сайты и веб-приложения ASP.NET Перед построением нового веб-приложения ASP.NET необходимо сделать выбор между двумя форматами проектов, а именно: ASP.NIZT Web Site (Веб-сайт ASP.NET) или ASP.NET Web Application (Веб-приложение ASP.NET). От этого выбора зависит способ, которым Visual Studio организует и обрабатывает стартовые файлы веб-приложения, тип начальных файлов проекта и степень контроля над результирующей композицией скомпилированной сборки .NET. Когда технология ASP.NET впервые появилась в составе .NET 1.0, единственным выбором было веб-приложение. Эта модель обеспечивает прямой контроль над именем и местоположением компилированной выходной сборки. Кроме того, в этой модели находящийся в памяти частичный класс, который содержит объявления элемента управления и конфигурации, на самом деле расположен не в памяти, а в другом физическом файле кода С#. Веб-приложения удобны, когда нужно выполнить миграцию старых веб-сайтов .NET 1.1 в проекты .NET 2.0 и последующих версий. Также они удобны, когда требуется построить одно решение Visual Studio 2010, которое может включать в себя несколько проектов (например, веб-приложение и три связанных библиотеки кода .NET). Для построения веб-приложения ASPNET активизируется пункт меню File^New Project (Файл^Создать проект) и выбирается шаблон из категории Web (рис. 32.21). a Visual С# Window Office Cloud Service Reporting SherePoint Sirvertight 'Jfe ASP.NET Web Application Li Qj I A project for creating an application with a t*m чг,,« ._ » . f&WET WcfeAppKcriorT \IJ Web user interface Empty ASP.NET Web Application ' ^ * |* ■ Щ l ASP.NET Web Application L ASPNET MVC 2 Web Application ASP.NET Web Sefvice Application WCF Service Application Visual C# I I Visual C# Visual C* Per user extensions are currently not allowed to load. Ej Name: WebApplication4 Location: Щ GXMyCode Solution names WebAppiicatiorrf Create directory for solution Add to source control Cancel Рис. 32.21. Шаблоны веб-приложений Visual Studio
Глава 32. Построение веб-страниц ASP.NET 1243 Solution Explorer » LU Styles "^1 About, aspx ^ __j Default.aspx "^ Default.aspx.es 4j Default.aspx.designer.es !> ф] Global.asax t> П Site.Master di Web.config $5 Solution Exp. Рис. 32.22. В модели веб-приложения каждая веб-страница состоит из трех файлов Предположим, что был создан новый проект ASP.NET Web Application. В нем находится огромное количество начальных файлов (назначение которых станет ясным в последующих главах). Однако наиболее важно отметить, что каждая веб-страница ASP.NET состоит из трех файлов: *.aspx (разметка), *.Designer.cs (сгенерированный визуальным конструктором код С#) и файл основного кода С# (обработчики событий, специальные методы и т.п.). На рис. 32.22 показан пример. В отличие от этого, шаблоны проектов веб-сайтов Visual Studio 2010 (ASP.NET Web Site), доступные через пункт меню File^New Web Site (Файл1^ Создать вебсайт), скрывают файл *.Designer.cs в пользу находящегося в памяти частичного класса. Более того, проекты ASP.NET Web Site поддерживают множество специально именованных папок, таких как AppCode. В эту папку можно поместить любые файлы кода С# (или VB), которые не отображаются прямо на веб-страницы, и компилятор времени выполнения динамически скомпилирует их при необходимости. Это является значительным упрощением обычного процесса построения выделенной библиотеки кода .NET и указания ссылок в обычных проектах. Кстати, проект веб-сайта может быть перенесен в неименном виде на рабочий веб-сервер без необходимости перекомпиляции сайта, что нужно в случае веб-приложения ASP.NET. В этой книге используются типы проектов ASP.NET Web Site, поскольку они существенно упрощают процесс построения веб-приложений на платформе .NET. Однако, независимо от выбранного подхода, работа производится с одной и той же программной моделью. На заметку! Поскольку шаблоны проектов ASP.NET в Visual Studio 2010 генерируют значительный объем начального кода (мастер-страницы, страницы содержимого, библиотеки сценариев, страницу входа и т.п.), в книге будет применяться только шаблон Blank web site (Пустой вебсайт). Стартовый код, генерируемый в случае выбора проекта веб-сайта ASP.NET, оставляется для самостоятельной проработки. Структура каталогов веб-сайта ASP.NET Новый проект веб-сайта ASP.NET может содержать некоторое количество специфически именованных подкаталогов, каждый из которых имеет определенное значение для исполняющей среды ASP.NET. В табл. 32.2 описаны эти специальные подкаталоги. Таблица 32.2. Специальные подкаталоги ASP.NET Подкаталог Назначение App_Browsers App_Code App_Data Папка для файлов определений браузеров, используемых для идентификации индивидуальных браузеров и определения их возможностей Папка исходного кода для компонентов или классов, которые должны компилироваться как часть приложения. ASP.NET компилирует код в этой папке при запросе страницы. Код в папке App_Code автоматически доступен приложению Папка для хранения файлов Access *.mdb, файлов SQL Express *.mdf, файлов XML или других источников данных App_GlobalResources Папка для файлов *.resx, доступных программно из кода приложения
1244 Часть VII. Построение веб-приложений с использованием ASP.NET Окончание табл. 32.2 Подкаталог Назначение App_LocalResources Папка для файлов *.resx, привязанных к определенной странице App_Themes Папка, содержащая коллекцию файлов, которые определяют внешний вид веб-страниц и элементов управления ASP.NET App_WebRef erences Папка для прокси-классов, схем и прочих файлов, ассоциированных с использованием веб-служб в приложении Bin Папка для скомпилированных приватных сборок (файлов *.dll). Сборки из папки Bin автоматически доступны приложению Чтобы добавить любой из этих известных подкаталогов к текущему веб-приложению, выберите пункт меню Websites Add Folder (Веб-сайт1^ Добавить папку). Во многих случаях IDE-среда автоматически сделает это, когда вы естественным образом добавляете соответствующие файлы к сайту. Например, вставка нового файла класса в проект автоматически добавит папку AppCode в структуру каталогов, если она пока не существует. Ссылаемые сборки Хотя шаблоны Web Site генерируют файл *.sln для загрузки файла *.aspx в IDE- среду, теперь больше нет связанного файла *.csproj. Однако проекты ASP.NET Web Application записывают все внешние сборки в файл *.csproj. Это вызывает очевидный вопрос: куда записываются внешние сборки под ASP.NET? Как уже было показано, при ссылке на приватную сборку Visual Studio 2010 автоматически создает каталог \bin внутри структуры каталогов для хранения локальной копии двоичного кода. Когда базовый код использует типы из этих библиотек кода, они автоматически загружаются по требованию. При ссылке на разделяемую сборку, находящуюся в глобальном кэше сборок (Global Assembly Cache — GAC), Visual Studio 2010 автоматически вставляет файл Web. con fig в текущее веб-решение (если его еще нет) и записывает внешнюю ссылку в элемент <assemblies>. Например, если выбрать пункт меню Website^Add Reference (Веб-сайт1^ Добавить ссылку) и указать разделяемую сборку (такую как System.Data.OracleClient.dll), обнаружится, что файл Web.config изменен следующим образом: <assemblies> <add assembly="System.Data.OracleClient, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7FllD50A3A"/> </assemblies> Как видите, каждая сборка описывается с использованием одной и той же информации для динамической загрузки методом Assembly.Load() (см. главу 15). Роль папки App_Code Папка AppCode используется для хранения файлов кода, которые не привязаны напрямую к определенной веб-странице (как файл отделенного кода), но должны быть скомпилированы для использования веб-сайтом. Код внутри папки AppCode будет автоматически компилироваться на лету по мере необходимости. После этого сборка будет доступна любому другому коду веб-сайта. Таким образом, папка AppCode похожа на папку Bin, за исключением того, что в ней можно сохранять исходный код вместо скомпилированного. Основное преимущество такого подхода заключается в том, что можно определять специальные типы для веб-приложения, не компилируя их отдельно.
Глава 32. Построение веб-страниц ASP.NET 1245 Единственная палка AppCode может содержать код на разных языках программирования. Во время выполнения для генерации необходимой сборки вызывается соответствующий компилятор. Если вместо этого необходимо секционировать код, то можно определить несколько подкаталогов для хранения любого количества файлов управляемого кода (*.vb, *.cs и т.д.). Например, предположим, что папка AppCode добавлена в корневой каталог приложения веб-сайта, имеющего две подпапки (MyCSharpCode и MyVbNetCode), которые содержат файлы, специфические для языка. После этого можно модифицировать файл Web. con fig, указав в нем эти подпапки с использованием элемента <codeSubDirectories>, вложенного в элемент <configuration>: <compilation debug="true" strict="false" explicit="true"> <codeSubDirectories> <add directoryName="MyCSharpCode" /> <add directoryName="MyVbNetCode" /> </codeSubDirectories> </compilation> На заметку! Папка App_Code также используется для хранения файлов, которые не являются языковыми, но, тем не менее, полезны (*.xsd, *.wsdl и т.п.). Помимо Bin и AppCode, папки AppData и AppThemes также являются двумя дополнительными специальными подкаталогами, с которыми необходимо познакомиться; оба они рассматриваются в последующих главах. Как всегда, за подробной информацией относительно остальных подкаталогов ASP.NET обращайтесь в документацию .NET Framework 4.0 SDK. Цепочка наследования типа Page Как уже было показано, все веб-страницы .NET в конечном итоге наследуются от System.Web.UI.Page. Подобно любому базовому классу, этот тип предоставляет полиморфный интерфейс всем производным типам. Однако тип Page — не единственный член иерархии наследования. Если вы найдете тип Page (внутри сборки System.Web.dll), используя браузер объектов Visual Studio 2010, то обнаружите, что Page "является" TempiateControl, который "является" Control, который "является" Object (рис. 32.23). ЕВЯИ Object Browser X Browse: .NET Framework4 ' < Search > - M л '£% Base Types л *t$ TemplateControl л *f$ Control l> *o IComponent i> -<> IControlBuilderAccessor > *"° IControlDesignerAccessor "° IDataBindingsAccessor t> ■*<> Disposable t> *•*> lExpressionsAccessor t "& IParserAccessor i> *-o lUriResolutionService -• Ш Object * *"° IFirterResolutionServtce ь "О INamingContainer !> *<> IHttpHandler ■ -• AddOnPreRenderCompleteAsync(System.Wefa.BegmEvent *1 -♦ AddOnPreRenderCompleteAsync(System.Web.B«ginEventri 1* CreateHtmlTextWrrter(SystemJO.TextWriter) Ф Creater-ftmlTextWriterFromType(System.IO.TextWriter# Sys ■,j* DeterminePostBackModeO -'♦ ExecuteRegrsteredAsyncTasksO ♦ FindContro {(string) .0 Framev/orklnitialtzeO • GetDataltemfi public class Page : System.Web.UI.TemplatcControl *■ Member of System.Web.UI J8| Summary: Represents an .aspx file, also known as a Web Forms page, requested from a server that hosts an ASP.NET Web application. - ] Рис. 32.23. Цепочка наследования для типа Page
1246 Часть VII. Построение веб-приложений с использованием ASP.NET Каждый из этих базовых классов добавляет свою часть функциональности к любому файлу *.aspx. В большинстве проектов используются члены, определенные внутри родительских классов Page и Control. Функциональность, полученная от класса System.Web.UI.TemplateControl, интересна только в случае построения специальных элементов управления Web Forms или для вмешательства в процесс визуализации. Первый родительский класс, представляющий интерес — это собственно Page. Здесь вы найдете многочисленные свойства, которые позволяют взаимодействовать с веб-примитивами, такими как переменные приложения и сеанса, запрос/ответ HTTP, поддержка тем и т.д. В табл. 32.3 описаны некоторые (но, конечно же, не все) основные свойства. Таблица 32.3. Избранные свойства класса Page Свойство Назначение Application Cache ClientTarget IsPostBack MasterPageFile Request Response Server Session Theme Trace Позволяет взаимодействовать с данными, которые доступны по всему вебсайту всем пользователям Позволяет взаимодействовать с объектом кэша для текущего веб-сайта Позволяет указать, как данная страница должна визуализировать себя в зависимости от запрашивающего браузера Получает значение, указывающее, была ли загружена страница в ответ на клиентскую обратную отправку или же она загружена при первоначальном обращении Устанавливает мастер-страницу для текущей страницы Предоставляет доступ к текущему запросу HTTP Позволяет взаимодействовать с исходящим ответом HTTP Обеспечивает доступ к объекту HttpServerUtility, который содержит различные вспомогательные функции серверной стороны Позволяет взаимодействовать с данными сеанса, используемыми текущей страницей Получает или устанавливает тему, используемую текущей страницей Обеспечивает доступ к объекту TraceContext, который позволяет протоколировать специальные сообщения во время сеансов отладки Взаимодействие с входящим запросом HTTP Как уже было показано в этой главе, базовый поток веб-приложсния начинается с запроса веб-страницы, возможного ввода информации пользователя и щелчка на кнопке Submit (Отправить) для отправки данных HTML-формы заданной веб-странице на обработку. В большинстве случаев в открывающем дескрипторе оператора form присутствуют атрибуты action и method, указывающие файл на веб-сервере, которому будут отправлены данные из различных виджетов HTML, и метод отправки данных (GET или POST): <form name^'defaultPage" id="defaultPage" action="http://localhost/Cars/ClassicAspPage.asp" met I "GET"> </form> Все страницы ASP.NET поддерживают свойство System.Web.UT.Fage.Request, которое обеспечивает доступ к экземпляру класса HttpRequest (основные члены этого класса перечислены в табл. 32.4).
Глава 32. Построение веб-страниц ASP.NET 1247 Таблица 31.4. Члены типа HttpRequest Член Назначение ApplicationPath Получает виртуальный корневой путь приложения ASP.NET на сервере Browser Предоставляет информацию о возможностях клиентского браузера Cookies Получает коллекцию cookie-наборов, присланную клиентским браузером FilePath Показывает виртуальный путь для текущего запроса Form Получает коллекцию переменных формы HTTP Headers Получает коллекцию заголовков HTTP HttpMethod Показывает метод передачи данных, используемый клиентом (GET/POST) IsSecureConnection Показывает, является ли соединение HTTP защищенным (т.е. HTTPS) QueryString Получает коллекцию переменных строки запроса HTTP RawUrl Получает "сырой" URL текущего запроса RequestType Показывает тип текущего запроса HTTP ServerVariables Получает коллекцию переменных веб-сервера UserHostAddress Получает IP-адрес хоста удаленного клиента UserHostName Получает имя хоста удаленного клиента В дополнение к этим свойствам в классе HttpRequest определен набор полезных методов, включая описанные ниже. • MapPath (). Отображает виртуальный путь запрошенного URL на физический путь на сервере для текущего запроса. • Save As (). Сохраняет детали текущего запроса HTTP в файле на веб-сервере, что полезно для целей отладки. • Validatelnput(). Если средство проверки достоверности включено с помощью атрибута Validate директивы Page, этот метод может быть вызван для проверки всех введенных пользователем данных (включая cookie-набор) по заранее определенному списку потенциально опасных входных данных. Получение статистики браузера Первый интересный аспект типа HttpRequest — свойство Browser, которое обеспечивает доступ к лежащему в основе объекту HttpBrowserCapabilities. В свою очередь, HttpBrowserCapabilities предоставляет многочисленные члены, которые позволяют программно исследовать статистику браузера, приславшего входной запрос HTTP. Создайте новый веб-сайт ASP.NET по имени FunWithPageMembers (опять выберите вариант File System). Первая задача состоит в построении пользовательского интерфейса, который позволит пользователю щелкнуть на веб-элементе управления Button (по имени btnGetBrowserStats) для просмотра различной статистики вызывающего браузера. Эта статистика будет сгенерирована динамически и присоединена к типу Label (по имени lblOutput). Ниже показан код обработчика события Click для кнопки. protected void btnGetBrowserStats_Click(object sender, EventArgs e) { string thelnfo = ""; thelnfo += string.Format("<li>Is the client AOL? {0}</li>", Request.Browser.AOL); // Клиент AOL?
1248 Часть VII. Построение веб-приложений с использованием ASP.NET thelnfo += string.Format("<li>Does the client support ActiveX? {0}</li>M, Request.Browser.ActiveXControls); // Поддерживает ли ActiveX? thelnfo += string.Format("<li>Is the client a Beta? {0}</li>M, Request.Browser.Beta); // Бета-версия? thelnfo += string.Format("<li>Does the client support Java Applets? {0}</li>M, Request.Browser.JavaApplets); // Поддерживает ли Java-аплеты? thelnfo += string.Format("<li>Does the client support Cookies? {0}</li>M, Request.Browser.Cookies); // Поддерживает ли cookie-наборы? thelnfo += string.Format("<li>Does the client support VBScript? {0}</li>M, Request.Browser.VBScript); // Поддерживает ли VBScript? lblOutput.Text = thelnfo; } Здесь проверяется множество возможностей браузера. Очень полезно проверить возможность поддержки браузером элементов управления ActiveX, Java-аплетов и кода VBScript клиентской стороны. В случае если вызывающий браузер не имеет поддержки определенной веб-технологии, страница *.aspx тогда сможет предпринять какие-то альтернативные действия. Доступ к входным данным формы В классе HttpResponse определены также свойства Form и QueryString. Они позволяют исследовать входные данные формы, используя пары "имя/значение". Эти свойства вполне можно использовать для доступа на веб-сервере к данным, введенным клиентом в форму, однако ASP.NET предлагает для этого более элегантный объектно- ориентированный подход. С учетом того, что ASP.NET обеспечивает веб-элементами управления серверной стороны, HTML-элементы пользовательского интерфейса можно трактовать как настоящие объекты. Таким образом, вместо того, чтобы получать значение, введенное в текстовом поле, следующим образом: protected void btnGetFormData_Click (object sender, System.EventArgs e) { // Получить значение виджета с идентификатором txtFirstName. string firstName = Request.Form("txtFirstName"); // Использовать полученное значение на странице... } можно просто опросить виджет серверной стороны непосредственно через свойство Text, чтобы использовать его в программе: protected void btnGetFormData_Click (object sender, System.EventArgs e) { // Получить значение виджета с идентификатором txtFirstName. string firstName = txtFirstName.Text; // Использовать полученное значение на странице... } Этот подход не только соответствует принципам объектно-ориентированного программирования, но также не придется беспокоиться о способе передачи данных формы (GET или POST) перед получением значений. Более того, работа с виджетом напрямую намного более безопасна к типам, учитывая тот факт, что ошибки неверного обращения к типу обнаруживаются во время компиляции, а не во время выполнения. Разумеется, это не значит, что вы никогда не должны использовать свойства Form или QueryString в ASP.NET; просто теперь потребность в них значительно снижена. Свойство IsPostBack Еще одним очень важным членом HttpRequest является свойство IsPostBack. Вспомните, что обратная отправка означает, что веб-страница отправляет данные об-
Глава 32. Построение веб-страниц ASP.NET 1249 ратно по тому же самому URL на веб-сервере. Согласно такому определению, IsPostBack вернет true, если текущий HTTP-запрос был послан пользователем в текущем сеансе, и false, если пользователь обращается к странице впервые. Обычно необходимость в определении того, является ли текущий запрос HTTP обратной отправкой, чаще всего возникает, когда нужно организовать выполнение блока кода только при первом обращении к данной странице. Например, может потребоваться наполнить объект DataSet из ADO.NET при первом обращении пользователя к файлу *.aspx и кэшировать этот объект для последующего использования. Когда вызывающий клиент вернется на ту же страницу, можно избежать излишнего обращения к базе данных (конечно, некоторые страницы могут требовать обновления DataSet в каждом запросе, но это другое дело). Предположим, что файл *.aspx обрабатывает событие страницы Load (подробно рассматривается далее в главе); тогда запрограммировать проверку условия обратной отправки можно следующим образом: protected void Page_Load(object sender, System.EventArgs e) { // Заполнить DataSet только при первом // входе пользователя на страницу. if ( ' IsPostBack) { // Заполнить DataSet и кэшировать его! } // Использовать кэшированный DataSet. } Взаимодействие с исходящим ответом HTTP Ознакомившись с тем, как тип Page позволяет взаимодействовать с входящим запросом HTTP, теперь нужно научиться взаимодействовать с исходящим HTTP-ответом. В ASP.NET свойство Response класса Page предоставляет доступ к экземпляру типа HttpResponse. В этом типе определен набор свойств, которые позволяют форматировать ответ HTTP, отправляемый обратно клиентскому браузеру. В табл. 32.5 перечислены основные его свойства. Таблица 32.5. Свойства типа HttpResponse Свойство Назначение Cache Возвращает семантику кэширования веб-страницы (см. главу 34) ContentEncoding Получает или устанавливает набор символов HTTP для выходного потока ContentType Получает или устанавливает тип HTTP MIME для выходного потока Cookies Получает коллекцию HttpCookie, отправленную текущим запросом Output Позволяет выполнять специальный вывод в тело исходящего содержимого HTTP OutputStream Позволяет выполнять двоичный вывод в тело исходящего содержимого HTTP StatusCode Получает или устанавливает код состояния HTTP вывода, возвращае- , мого клиенту StatusDescription Получает или устанавливает строку состояния HTTP вывода, возвращаемого клиенту SuppressContent Получает или устанавливает значение, указывающее, что HTTP не будет отправлен клиенту
1250 Часть VII. Построение веб-приложений с использованием ASP.NET В табл. 32.6 приведен неполный список методов, поддерживаемых типом HttpResponse. Таблица 32.6. Методы типа HttpResponse Метод Назначение Clear () Очищает все заголовки и вывод содержимого из буфера потока End () Посылает весь буферизованный вывод клиенту, после чего закрывает сокетное соединение Flush () Посылает весь текущий буферизованный вывод клиенту Redirect () Перенаправляет клиента по новому URL Write () Записывает значения в выходной поток НТТР-содержимого WriteFile () Записывает файл непосредственно в выходной поток НТТР-содержимого Выдача HTML-содержимого Возможно, наиболее известным аспектом типа HttpResponse является способность записывать содержимое непосредственно в выходной поток HTTP. Метод HttpResponse. Write () позволяет передавать любые HTML-дескрипторы и/или текстовые литералы. Метод HttpResponse.WriteFile() продвигает эту функциональность еще на шаг вперед, в том смысле, что можно указывать имя физического файла на веб-сервере, содержимое которого должно быть визуализировано в выходной поток (это удобно для быстрой отправки содержимого существующего файла *.htm). Для примера предположим, что к текущему файлу *.aspx добавлен еще один элемент Button со следующим обработчиком события Click серверной стороны: protected void btnHttpResponse_Click(object sender, EventArgs e) { Response .Write ("<b>My name is : </bxbr>") ; Response.Write(this.ToString()); Response. Write ("<br><br><b>Here was your last request: </bxbr>") ; Response.WriteFile("MyHTMLPage.htm"); } Роль этой вспомогательной функции (которая, как можно предположить, будет вызываться некоторыми обработчиками событий серверной стороны) довольно проста. Единственное, что здесь представляет интерес — это тот факт, что метод HttpResponse. WriteFile() теперь издает содержимое файла *.htm, находящегося в корневом каталоге веб-сайта. Хотя всегда можно воспользоваться подходом в духе старой школы, визуализируя дескрипторы и HTML-содержимое с помощью метода Write(), этот способ намного меньше применяется в ASP.NET, чем в классическом ASP Причина связана с появлением веб-элементов управления серверной стороны. Таким образом, чтобы визуализировать блок текстовых данных в браузере, достаточно присвоить нужную строку свойству Text виджета Label. Перенаправление пользователей Еще одним интересным аспектом класса HttpResponse является способность перенаправления пользователей на новый URL:
Глава 32. Построение веб-страниц ASP.NET 1251 protected void btnWasteTime_Click(object sender, EventArgs e) { Response.Redirect("http://www.facebook.com"); } Если этот обработчик событий вызывается через обратную отправку клиентской стороны, пользователь автоматически перенаправляется по указанному URL. На заметку! Метод HttpResponse.RedirectO всегда будет вызывать обратный переход в клиентский браузер. Если просто необходимо передать управление файлу *.aspx из того же виртуального каталога, более эффективным будет применение метода HttpServerUtility. Transfer (), доступного через унаследованное свойство Server. Представленного материала вполне достаточно для оценки функциональности System,Web.UI.Page. Роль базового класса System.Web.UI.Control рассматривается в следующей главе. А теперь давайте немного коснемся жизненного цикла объекта, унаследованного от Page. Исходный код. Веб-сайт FunWithPageMembers доступен в подкаталоге Chapter 32. Жизненный цикл веб-страницы ASP.NET Каждая веб-страница ASP.NET имеет фиксированный жизненный цикл. Когда исполняющая среда ASP.NET принимает входящий запрос определенного файла *.aspx, в памяти размещается ассоциированный с ней объект унаследованного от System.Web. Ul.Page типа с помощью конструктора по умолчанию этого типа. Затем платформа автоматически запускает последовательность событий. По умолчанию инициируется событие Load, в обработчик которого можно поместить свой специальный код: public partial class Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Response. Write ("Load event fired!11); } } Помимо события Load, данная страница Page может перехватывать любое из основных событий, описанных в табл. 32.7, которые перечислены в порядке их появления (подробную информацию о событиях, которые могут возникать во время жизни страницы, ищите в документации .NET Framework 4.0 SDK). Таблица 32.7. Избранные события типа Page Событие Назначение Prelnit Платформа использует это событие для размещения в памяти всех веб- элементов управления, применения тем, установки мастер-страницы и настройки профилей пользователей. Это событие можно перехватить, чтобы вмешаться в процесс In it Платформа использует это событие для установки свойств веб-элементов в их предыдущие значения через обратную отправку или данные состояния представления
1252 Часть VII. Построение веб-приложений с использованием ASP.NET Окончание табл. 32.7 Событие Назначение Load Когда возникает это событие, страница и ее элементы управления полностью инициализированы и их предыдущее состояние восстановлено. В этот момент можно безопасно взаимодействовать с каждым веб-виджетом Событие, иниции- Конечно, события с таким именем нет. Это "событие" просто ссылается ровавшее обратную на любое событие, инициированное браузером для выполнения обратной отправку отправки на веб-сервер (вроде щелчка на элементе Button) PreRender Вся привязка данных к элементам управления и конфигурирование пользовательского интерфейса произошло, и элементы управления готовы для визуализации своих данных в выходящий ответ HTTP Unload Страница и ее элементы управления завершили процесс визуализации, и объект страницы готов к уничтожению. В этот момент попытка взаимодействия с исходящим ответом HTTP вызовет ошибку. Однако это событие можно перехватить для выполнения любой очистки на уровне страницы (закрыть файл или соединение с базой данных, провести необходимое протоколирование, освободить объекты и т.д.) Вызывает некоторое удивление, что IDE-среда не поддерживает обработку других событий помимо Load. В этом случае требуется вручную реализовать в файле кода метод по имени Раде_ИмяСобытия. Например, ниже показано, как можно обработать событие Unload: public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Response.Write("Load event fired!"); } protected void Page_Unload(object sender, EventArgs e) { // Больше нельзя добавлять данные в ответ HTTP, // поэтому будем писать в локальный файл. System.IO.File.WriteAllText(@"C:\MyLog.txt", "Page unloading!"); На заметку! Каждое событие класса Page работает в сочетании с делегатом System. Event Handler, поэтому подпрограммы, которые обрабатывают эти события, всегда принимают Object в первом параметре и EventArgs — во втором. Роль атрибута AutoEventWireup Чтобы организовать обработку событий для своей страницы, необходимо обновить блок <script> или файл отделенного кода, добавив соответствующий обработчик. Однако если вы посмотрите на директиву <%@Раде%>, то заметите специфический атрибут по имени AutoEventWireup, который по умолчанию установлен в true: <о@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" ^> При таком поведении по умолчанию каждый обработчик события уровня страницы будет автоматически добавляться, когда вводится соответствующий именованный метод. Если же установить атрибут AutoPageWireup в false, как показано ниже, то события уровня страницы больше перехватываться не будут:
Глава 32. Построение веб-страниц ASP.NET 1253 <Т,@ Page Language="C#" AutoEventWireup="false" CodeFile="Default .aspx.cs" Inherits=II_Defaultn %> Этот атрибут (будучи включенным) приводит к генерации необходимой оснастки для событий внутри автоматически сгенерированного частичного класса, описанного ранее в этой главе. Если отключить AutoEventWireup, все равно можно будет обрабатывать события уровня страницы, используя логику обработки событий С#. Например: public _Default() { // Явно привязаться к событиям Load и Unload. this.Load += new Page_Load; this.Unload += new Page_Unload; } Как и можно было ожидать, обычно AutoEventWireup оставляется включенным. Событие Error Еще одним событием, которое может произойти во время жизненного цикла страницы, является Error. Это событие возникает, когда метод унаследованного от Page типа инициирует исключение, которое не было явно обработано. Предположим, что обрабатывается событие Click для данного элемента Button на странице, и внутри обработчика события (который называется btnGetFile _ Click) предпринимается попытка вывести содержимое локального файла в ответ HTTP. Также предположим, что проверка существования этого файла с помощью стандартной структурированной обработки исключений не производится. Регистрация обработчика события Error в конструкторе по умолчанию предоставляет финальный шанс справиться с проблемой на этой странице, прежде чем конечный пользователь получит невнятное сообщение об ошибке. Взгляните на следующий код: public partial class _Default : System.Web.UI.Page { void Page_Error(object sender, EventArgs e) { Response.Clear (); Response.Write("I am sorry...I can't find a required flie.<br>"); Response.Write(string.Format ("The error was: <b>{0}</b>", Server.GetLastError().Message)); Server.ClearError (); } protected void Page_Load(object sender, EventArgs e) { Response.Write("Load event fired!"); } protected void Page_Unload(object sender, EventArgs e) { // Больше нельзя добавлять данные в ответ HTTP, // поэтому будем писать в локальный файл. System.10.File.WriteAllText(@"C:\MyLog.txt", "Page unloading!"); } protected void btnPostback_Click(object sender, EventArgs e) { //Здесь ничего не происходит; это нужно, чтобы обеспечить обратную отправку страницы } protected void btnTriggerError_Click(object sender, EventArgs e) { System.10.File.ReadAllText(@"C:\IDontExist.txt"); } }
1254 Часть VII. Построение веб-приложений с использованием ASP.NET Обратите внимание, что обработчик событий Error начинается с очистки любого содержимого, имеющегося в ответе HTTP, и выдачи обобщенного сообщения об ошибке. Чтобы получить доступ к определенному объекту System.Exception, можно воспользоваться методом HttpServerUtility.GetLastError (), который представлен унаследованным свойством Server: Exception e = Server.GetLastError() ; И, наконец, перед выходом из этого обобщенного обработчика ошибок явно вызывается HttpServerUtility.ClearError () через свойство Server. Это обязательно, поскольку информирует исполняющую среду, что обработка проблемы завершена, и дальнейшая обработка не требуется. Если вы забудете сделать это, конечный пользователь получит страницу ошибки времени выполнения. На рис. 32.24 показан результат работы этой логики перехвата ошибок. «- С Л & http://localhost:2169/f ► Q- А- I am sorry I cato't find a required file. The error was: Comld мог find file 'Ci^JDoMtExbt.fart". Рис. 32.24. Обработка ошибок на уровне страницы К этому моменту вы должны понимать композицию типа ASP.NET Page. Имея такую основу, можно приступать к исследованию роли веб-элементов управления ASP.NET, тем и мастер-страниц — т.е. материала последующих глав. В завершение данной главы давайте рассмотрим роль файла Web. с on fig. Исходный код. Веб-сайт PageLifeCycle доступен в подкаталоге Chapter 32. Роль файла Web.conf ig По умолчанию все веб-приложения С# ASP.NET, созданные в Visual Studio 2010, автоматически снабжаются файлом Web. con fig. Если возникла необходимость добавить этот файл к веб-сайту вручную (например, когда работа велась с однофайловой моделью и не было создано веб-решение), выберите пункт меню Website^Add New Item (Веб-сайт1^ Добавить новый элемент). В файл Web. con fig можно добавить установки, управляющие поведением веб-приложения во время выполнения. Вспомните, что во время рассмотрения сборок .NET (в главе 15) вы узнали о том, что клиентское приложение может использовать основанный на XML конфигурационный файл, чтобы инструктировать CLR о том, как обрабатывать запросы привязки, проверять сборки и реализовать прочие детали времени выполнения. То же самое верно для веб-приложений ASP.NET, но с тем заметным отличием, что веб-ориентированные конфигурационные файлы всегда называются Web. con fig (в отличие от конфигурационных файлов *.ехе, которые называются по имени соответствующей клиентской исполняемой программы). Структура Web. con fig по умолчанию довольно многословна. В табл. 32.8 представлен обзор ряда наиболее интересных подэлементов, которые можно найти в файле Web.conf ig.
Глава 32. Построение веб-страниц ASP.NET 1255 Таблица 32.8. Избранные элементы файла Web.config Элемент Назначение <appSettings> <authentication> <authorization> <connectionStrings> <customErrors> <globalization> <namespaces> <sessionState> <trace> Этот элемент служит для установки специальных пар "имя/значение", которые могут программно читаться в память для использования страницами через тип Conf igurationManager Этот элемент, относящийся к безопасности, используется для определения режима аутентификации для веб-приложения Этот элемент, связанный с безопасностью, используется для определения, какие пользователи к каким ресурсам имеют доступ на веб-сервере Этот элемент служит для хранения строк внешних соединений, используемых веб-сайтом Этот элемент позволяет сообщить исполняющей среде о том, как следует отображать ошибки, возникающие во время функционирования веб-приложения Этот элемент используется для конфигурирования установок глобализации для веб-приложения Этот элемент документирует все пространства имен, которые нужно включить, если веб-приложение предварительно компилируется с использованием нового инструмента командной строки aspnet_compiler.exe Этот элемент используется для управления тем, как и где данные о состоянии сеанса будут храниться исполняющей средой .NET Этот элемент позволяет включать и отключать поддержку трассировки для данного веб-приложения Помимо набора, представленного в табл. 32.8, файл Web.config может содержать дополнительные подэлементы. Подавляющее большинство этих элементов относятся к безопасности, а остальные полезны только в более сложных сценариях применения ASP.NET, таких как создание специальных заголовков HTTP или специальных модулей HTTP (эти темы здесь не рассматриваются). Утилита администрирования веб-сайтов ASP.NET Хотя содержимое файла Web.config можно модифицировать непосредственно в Visual Studio 2010, для веб-проектов ASP.NET предусмотрен удобный инструмент, который позволяет графически редактировать многочисленные элементы и атрибуты файла Web.config проекта. Чтобы запустить этот инструмент, выберите пункт меню Web Site<=> ASP.NET Configuration (Веб-сайт^Конфигурация ASP.NET). Щелкая на вкладках в верхней части страницы, легко заметить, что большая часть функциональности этого инструмента служит для установки настроек безопасности веб-сайта. Однако этот инструмент также позволяет добавлять настройки к элементу <appSettings>, определять установки отладки и трассировки, а также указывать страницу ошибок по умолчанию. Вы еще увидите этот инструмент в действии, когда это будет необходимо, а сейчас имейте в виду, что эта утилита не позволяет добавлять все возможные установки к файлу Web.config. Почти наверняка будут возникать ситуации, когда этот файл придется обновлять вручную в обычном текстовом редакторе.
1256 Часть VII. Построение веб-приложений с использованием ASP.NET Резюме Построение веб-приложения требует другого склада мышления, чем в случае создания традиционных настольных приложений. В этой главе вы начали краткое изучение некоторых основных тем, включая HTML, HTTP, роль сценариев клиентской стороны, а также сценариев серверной стороны с использованием классического ASP. Большая часть главы была посвящена архитектуре страницы ASP.NET. Как вы видели, файл *.aspx в проекте имеет ассоциированный с ним класс, унаследованный от System. Web. UI. Page. С помощью этого объектно-ориентированного подхода ASP NET позволяет строить многократно используемые объектно-ориентированные системы. После рассмотрения некоторой ключевой функциональности цепочки наследования страницы, в этой главе также обсуждалось то, как страницы в конечном итоге компилируются в действительную сборку .NET. Вдобавок были освещены роли файла Web. con fig и инструмента администрирования веб-сайтов ASPNET
ГЛАВА 33 Веб-элементы управления, мастер-страницы и темы ASP.NET В предыдущей главе внимание было сосредоточено на композиции и поведении объектов Page в ASP.NET. В этой главе мы погрузимся в детали веб-элементов управления, которые составляют пользовательский интерфейс страницы. После рассмотрения общей природы веб-элемента управления ASP.NET вы получите представление о том, как правильно использовать некоторые элементы пользовательского интерфейса, включая элементы управления проверкой достоверности и элементы управления привязкой данных. Значительная часть этой главы будет посвящена роли мастер-страниц. Будет показано, как они обеспечивают упрощенный способ определения общего скелета пользовательского интерфейса, который повторяется среди страниц веб-сайта. С темой мастер-страниц тесно связано использование элементов управления навигацией по сайту (а также файл *.sitemap), которые позволяют определять навигационную структуру многостраничного сайта с помощью XML-файла серверной стороны. В завершение вы узнаете о роли тем ASP.NET. Концептуально темы служат той же цели, что и каскадные таблицы стилей; однако темы ASP.NET применяются на веб-сервере (в противоположность браузеру клиентской стороны) и потому имеют доступ к ресурсам серверной стороны. Природа веб-элементов управления Главным преимуществом ASP.NET является возможность собирать пользовательский интерфейс страниц, используя типы, определенные в пространстве имен System.Web. Ul.WebControls. Как было показано, эти элементы управления (которые называются серверными элементами управления, веб-элементами управления или элементами управления Web Forms) исключительно полезны в том, что автоматически генерируют необходимую HTML-разметку для запрашивающего браузера и предоставляют набор событий, которые могут быть обработаны на веб-сервере. Более того, поскольку каждый элемент управления ASP.NET имеет соответствующий класс в пространстве имен
1258 Часть VII. Построение веб-приложений с использованием ASP.NET System.Web.UI.WebControls, им можно манипулировать в объектно-ориентированной манере. При конфигурировании свойств веб-элемента управления в окне Properties (Свойства) среды Visual Studio 2010 изменения фиксируются в открывающем дескрипторе объявления данного элемента в файле *.aspx как последовательность пар "имя/значение". Таким образом, если добавить новый элемент Text Box в визуальном конструкторе и изменить его свойства ID, BorderStyle, BorderWidth, BackColor и Text, открывающий дескриптор <asp:TextBox> будет соответствующим образом модифицирован (обратите внимание, что значение Text становится внутренним текстом в области TextBox): <asp:TextBox ID=MtxtNameTextBoxM runat=,,server" BackColor=,,#C0FFC0" BorderStyle=,,Dotted" BorderWidth=M3px">Enter Your Name</asp:TextBox> Учитывая, что это объявление веб-элемента управления в конечном итоге становится переменной-членом из пространства имен System.Web.Ul.WebControls (через цикл динамической компиляции, рассмотренный в главе 32), с членами этого типа можно взаимодействовать внутри блока <script> серверной стороны или в файле отделенного кода. Добавив в файл *.aspx новый элемент управления Button, можно написать обработчик события Click серверной стороны, в котором будет изменяться цвет фона элемента TextBox: partial class _Default : System.Web.UI.Page { protected void btnChangeTextBoxColor_Click (object sender, System.EventArgs e) { // Изменить цвет объекта TextBox в коде. this.txtNameTextBox.BackColor = System.Drawing.Color.DarkBlue; } } Все веб-элементы управления ASP.NET в конечном итоге наследуются от общего базового класса System.Web.Ul.WenControls.WebControl, который, в свою очередь, унаследован от System.Web.UI.Control (а тот — от System.Object). И Control, и WebControl определяют ряд свойств, общих для всех элементов управления серверной стороны. Прежде чем рассмотреть унаследованную функциональность, давайте формализуем понятие обработки событий серверной стороны. Обработка событий серверной стороны Учитывая текущее состояние World Wide Web, нельзя не принимать во внимание фундаментальную природу взаимодействия браузеров с веб-серверами. Всякий раз, когда эти две сущности взаимодействуют, возникает лишенный состояния цикл запроса/ ответа HTTP. Хотя серверные элементы управления ASP.NET выполняют большую работу, изолируя от низкоуровневых деталей протокола HTTP, всегда стоит помнить, что восприятие WWW как управляемой событиями сущности — всего лить маскировка, которую обеспечивает CLR, и не имеет ничего общего с управляемой событиями модели пользовательских интерфейсов на основе Windows. Так, например, хотя пространства имен System.Windows.Forms, System.Windows. Controls и System.Web.Ul.WebControls определяют типы с теми же простыми именами (Button, TextBox, Label и т.д.), они не предоставляют идентичных наборов событий. Например, не существует способа обработать событие серверной стороны MouseMove, когда пользователь перемещает курсор над элементом управления Web Forms типа Button. Очевидно, что это хорошо (вряд ли кому понадобится выполнять обратную отправку на сервер при каждом движении курсора мыши в браузере).
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1259 Отрицательным фактором является то, что данный веб-элемент управления ASP.NET поддерживает ограниченный набор событий, и все они в конечном итоге подразумевают обратную отправку на веб-сервер. Любая необходимая обработка событий клиентской стороны потребует написания массы сценарного кода JavaScript /VBScript клиентской стороны для обработки механизмом выполнения сценариев запросившего браузера. Учитывая то, что ASP.NET — это в первую очередь технология серверной стороны, тема написания сценариев клиентской стороны здесь не рассматривается. На заметку! Обработка события для определенного веб-элемента управления с использованием Visual Studio 2010 может быть выполнена в той же манере, что и для элемента управления Windows Forms. Просто выберите необходимый виджет на поверхности визуального конструктора и щелкните на значке с изображением молнии в окне Properties. Свойство AutoPostBack Стоит также упомянуть, что многие веб-элементы управления ASP.NET поддерживают свойство по имени AutoPostBack (прежде всего, элементы управления Checkbox, RadioButton и TextBox, а также любые элементы, унаследованные от абстрактного класса ListControl). По умолчанию это свойство установлено в false, что отключает немедленную обратную отправку на сервер (даже при наличии обработчика события в файле отделенного кода). В большинстве случаев это именно то поведение, которое нужно, учитывая, что такие элементы, как флажки, обычно не требуют функциональности обратной отправки. Другими словами, не нужно выполнять обратную отправку немедленно после отметки или снятия отметки с флажка, поскольку объект страницы может получить состояние виджета внутри более естественного обработчика события Click для Button. Однако если необходимо заставить любой из этих виджетов немедленно выполнять обратную отправку обработчику события серверной стороны, установите значение AutoPostBack равным true. Такая техника полезна, когда требуется сделать так, чтобы состояние одного виджета автоматически помещало значение в другой виджет на той же странице. Для иллюстрации предположим, что есть веб-страница, содержащая элемент TextBox (по имени txtAutoPostback) и элемент ListBox (по имени IstTextBoxData). Ниже показана соответствующая разметка: <form id="forml11 runat=llserverM> <asp:TextBox ID="txtAutoPostback" runat="server"></asp:TextBox> <br/> <asp:ListBox ID="IstTextBoxData" runat="server"></asp:ListBox> </form> Теперь обработайте событие TextChanged элемента TextBox, и внутри обработчика событий серверной стороны заполните ListBox текущим значением TextBox: partial class _Default : System.Web.UI.Page { protected void txtAutoPostback_TextChanged(object sender, System.EventArgs e) { IstTextBoxData.Items.Add(txtAutoPostback.Text); } } Если вы запустите приложение в том виде, как есть, то при вводе текста в TextBox обнаружите, что ничего не происходит. Более того, если вы введете что-то в TextBox и перейдете к следующему элементу управления нажатием клавиши <ТаЬ>, также ниче-
1260 Часть VII. Построение веб-приложений с использованием ASP.NET го не произойдет. Причина в том, что по умолчанию свойство AutoPostBack элемента TextBox установлено в false. Если его установить в true: <asp:TextBox ID=,,txtAutoPostback" runat="server" AutoPostBack="trueM> </asp:TextBox> то при выходе из TextBox по нажатию <Tab> или <Enter> элемент ListBox автоматически заполнится текущим значением TextBox. Честно говоря, помимо необходимости наполнения элементов одного виджета в зависимости от значения другого, обычно изменять состояние свойства AutoPostBack виджета не придется (и даже в такой ситуации задачу можно решить внутри клиентского сценария, исключая необходимость во взаимодействии с сервером). Базовые классы Control и WebControl Базовый класс System.Web.UI.Control определяет различные свойства и события, которые обеспечивают возможность взаимодействия с основными (обычно не имеющими отношения к графическому интерфейсу) аспектами веб-элемента управления. В табл. 33.1 документированы некоторые члены, представляющие интерес. Таблица 33.1. Избранные члены System.Web.UI.Control Член Назначение Controls DataBindO EnableTheming HasControlsO ID Page Parent SkinID Visible Это свойство получает объект ControlCollection, представляющий дочерние элементы управления внутри текущего элемента Этот метод привязывает источник данных к вызывающему серверному элементу управления и всем его дочерним элементам Это свойство устанавливает, поддерживает ли элемент функциональность тем (значение по умолчанию — true) Этот метод определяет, содержит ли серверный элемент управления какие-то дочерние элементы Это свойство получает и устанавливает программный идентификатор серверного элемента управления Это свойство получает ссылку на экземпляр Page, который содержит данный серверный элемент управления Это свойство получает ссылку на родительский элемент данного серверного элемента управления в иерархии элементов управления страницы Это свойство получает или устанавливает обложку для применения к элементу управления, позволяя определять внешний вид и поведение с использованием ресурсов серверной стороны Это свойство получает и устанавливает значение, указывающее на то, будет ли серверный элемент управления визуализироваться в виде элемента пользовательского интерфейса на странице Перечисление содержащихся элементов управления Первый аспект System.Web.UI.Control, который мы рассмотрим — это тот факт, что все веб-элементы управления (включая Page) наследуют коллекцию специальных элементов (доступную через свойство Controls). Во многом подобно приложениям Windows Forms, свойство Controls предоставляет доступ к строго типизованной кол-
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1261 лекции типов, унаследованных от WebControl. Подобно любой коллекции .NET, имеется возможность динамически добавлять, вставлять и удалять элементы во время выполнения. Хотя технически возможно добавлять новые элементы управления непосредственно к типу-наследнику Page, проще (и надежнее) использовать элемент управления Panel. Класс Panel представляет контейнер виджетов, которые могут или не могут быть видимы конечному пользователю (в зависимости от значений свойств Visible и BorderStyle). Для примера создайте новый пустой веб-сайт по имени DynamicCtrls. С помощью визуального конструктора страниц Visual Studio 2010 добавьте элемент типа Panel (с именем myPanel), который содержит в себе элементы TextBox, Button и HyperLink, названные как угодно (имейте в виду, что визуальный конструктор требует, чтобы внутренние элементы перетаскивались только в пределах пользовательского интерфейса элемента Panel). Затем за пределами Panel разместите виджет Label (по имени lblControlInfo) для помещения в него визуализированного вывода. Ниже показана одна из возможных HTML-разметок. <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Dynamic Control Test</title> </head> <body> <form id="forml11 runat="server"> <div> <hr /> <hl>Dynamic Controls</hl> <asp:Label ID=IllblTextBoxText" runat=Ilserver"></asp :Label> <hr /> </div> <•— Элемент Panel содержит три элемента управления --> <asp: Panel ID=llmyPanelM runat="server" Width=00px11 BorderColor=MBlackM BorderStyle=MSolidM > <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox><br/> <asp:Button ID="Buttonl" runat=,,server" Text=,,Button,,/xbr/> <asp:HyperLink ID="HyperLinkl11 runat="serverll>HyperLink </asp:HyperLink> </asp:Panel> <br /> <br /> <asp:Label ID="lblControlInfo11 runat=llserverllx/asp:Label> </form> </body> </html> С этой разметкой поверхность визуального конструктора страницы будет выглядеть примерно так, как показано на рис. 33.1. Предположим, что в событии Page_Load() необходимо получить детальную информацию об элементах управления, содержащихся внутри Panel, и присвоить результат элементу lblControlInfo типа Label. Ниже приведен соответствующий код С#. public partial class _Default : System.Web.UI.Page { private void ListControlsInPanel () { string thelnfo = ""; thelnfo = string. Format ("<b>Does the panel have controls? {0} </bxbr/>", myPanel.HasControls ());
1262 Часть VII. Построение веб-приложений с использованием ASP.NET // Получить все элементы управления в панели. foreach (Control с in myPanel.Controls) { if (!object.ReferenceEquals(c.GetType(), typeof(System.Web.UI.LiteralControl))) { thelnfo += "***************************<br/>"; thelnfo += string.Format("Control Name? {0} <br/>", c.ToString()); thelnfo += string.Format("ID? {0} <br>", c.ID); thelnfo += string.Format("Control Visible? {0} <br/>", c.Visible); thelnfo += string.Format("ViewState? {0} <br/>", c.EnableViewState) , } } lblControlInfo.Text = thelnfo; } protected void Page_Load(object sender, System.EventArgs e) { ListControlsInPanel (); } Defauft.aspx X Dynamic Controls (IbfTextBoxTextj [ a s p: Pan el *my Pan dl~-~~—~———— J Button j | jHyperLtok ' -а == О 0 [IblControlInfo] I iS Design i О Spfit j £3 Soui Рис. 33.1. Пользовательский интерфейс веб-страницы Dynamic Controls В коде осуществляется проход по всем WebControl, содержащимся в Panel, с проверкой их принадлежности к типу System.Web.UI.LiteralControl; элементы этого типа пропускаются. Класс LiteralControl служит для представления литеральных HTML-дескрипторов и содержимого (такого как <br>, текстовые литералы и т.п.). Если не выполнить такую проверку, в контексте Panel будет найдено намного больше элементов управления (исходя из приведенного выше объявления *.aspx). Предполагая, что элемент управления не является литеральным HTML-содержимым, для него выводятся некоторые статистические данные. Результат можно видеть на рис. 33.2. Динамическое добавление и удаление элементов управления Предположим, что необходимо модифицировать содержимое Panel во время выполнения. Давайте поместим на текущую страницу элемент Button (по имени bntAddWidgets), который будет динамически добавлять к Panel три элемента управления TextBox, и еще один элемент Button (по имени btnRemovePanelItems), который очистит Panel от всех вложенных элементов управления. Обработчики события Click для обеих кнопок показаны ниже.
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1263 protected void btnClearPanel_Click(object sender, System.EventArgs e) { // Очистить содержимое панели, затем заново перечислить элементы. myPanel.Controls.Clear() ; ListControlsInPanel(); } protected void btnAddWidgets_Click(object sender, System.EventArgs e) { for (int 1 = 0; l < 3; i++) { // Присвоить идентификатор, чтобы можно было // позднее получить текстовое значение //с использованием входных данных формы. TextBox t = new TextBoxO ; t.ID= string.Format(MnewTextBox{0}", 1) ; myPanel.Controls.Add(t); ListControlsInPanel (); } } Обратите внимание, что каждому элементу TextBox присваивается уникальный идентификатор (newTextBoxO, newTextBoxl и т. д.). Запустив страницу, можно добавлять новые элементы к элементу управления Panel и полностью очищать содержимое Panel. Q Dynamic Control Test С Л ft http://localhost: ► Q~ Лт Dynamic Controls Button iHyperLink Does the panel have controls? True Control Name System Web.m.WebControls TextBox ID? TextBox 1 Control Visible? True ViewState^ True Control Name? System Web.UI WebControls Button Ш Button 1 Control Visible True \riewState? True Control Name System Web.UI.WebControls Hyperlink ID?H\perLinkl Control Visible? True ViewState? True Рис. 33.2. Перечисление элементов управления во время выполнения Взаимодействие с динамически созданными элементами управления Получать значения из этих динамически сгенерированных элементов TextBox можно различными способами. Добавьте к пользовательскому интерфейсу еще один элемент управления Button (по имени btnGetTextData) и один элемент Label по имени lblTextBoxData, а также обработайте событие Click элемента Button.
1264 Часть VII. Построение веб-приложений с использованием ASP.NET Для доступа к данным в динамически сгенерированных элементах Text Box есть несколько вариантов. Один подход заключается в циклическом проходе по всем элементам, содержащимся во входных данных формы HTML (доступных через HttpRequest. Form) и накоплении текстовой информации в локальной строке System.String. После прохождения всей коллекции эту строку необходимо присвоить свойству Text нового элемента Label. protected void btnGetTextData_Click(object sender, System.EventArgs e) { string textBoxValues = ""; for (int 1=0; l < Request.Form.Count; i++) { textBoxValues += string. Format ("<li> { 0} </hxbr/>", Request .Form [l] ) ; } lblTextBoxData.Text = textBoxValues; } Запустив приложение, вы обнаружите, что содержимое каждого текстового поля можно просматривать в виде довольно длинной (нечитабельной) строки. Эта строка содержит состояние представления (view state) каждого элемента управления на странице. Роль состояния представления рассматривается в главе 34. Для получения более ясного вывода можно разнести текстовые данные по уникально именованным элементам (newTextBoxO, newTextBoxl и newTextBox2). Рассмотрим следующую модификацию: protected void btnGetTextData_Click(object sender, System.EventArgs e) { // Получить текстовые поля по имени. string lableData = string. Format ("<li>{ 0} </lixbr/>", Request.Form.Get("newTextBoxO")); lableData += string.Format("<li>{0}</li><br/>", Request.Form.Get("newTextBoxl")); lableData += string. Format ("<li>{ 0} </lixbr/>", Request.Form.Get(MnewTextBox2")); lblTextBoxData.Text = lableData; } Используя этот подход, вы заметите, что как только запрос обработан, текстовое поле исчезнет. Причина этого кроется в не поддерживающей состояние природе HTML. Чтобы сохранять эти динамически созданные TextBox между обратными отправками, нужно пользоваться приемами программирования состояния ASP.NET (см. главу 34). Исходный код. Веб-сайт DynamicCtrls доступен в подкаталоге Chapter 33. Функциональность базового класса WebControl Как видите, тип Control имеет ряд характеристик поведения, не связанных с графическим пользовательским интерфейсом (коллекцию элементов управления, поддержка автоматической обратной отправки и т.д.). С другой стороны, базовый класс WebControl предоставляет графический полиморфный интерфейс всем виджетам (некоторые свойства WebControl перечислены в табл. 33.2). Почти все эти свойства очевидны, поэтому вместо того, чтобы рассматривать их использование по одиночке, давайте посмотрим на работу сразу множества элементов Web Forms.
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1265 Таблица 33.2. Избранные свойства базового класса WebControl Свойство Назначение BackColor Получает или устанавливает цвет фона для веб-элемента управления BorderColor Получает или устанавливает цвет контура веб-элемента управления BorderStyle Получает или устанавливает стиль контура веб-элемента управления BorderWidth Получает или устанавливает ширину контура веб-элемента управления Enabled Получает или устанавливает значение, указывающее на то, что веб-элемент управления доступен CssClass Позволяет назначить виджету класс, определенный в каскадной таблице стилей Font Получает информацию о шрифте веб-элемента управления ForeColor Получает или устанавливает цвет переднего плана (обычно цвет текста) веб-элемента управления Height, Width Получает или устанавливает высоту и ширину веб-элемента управления Tablndex Получает или устанавливает индекс обхода по клавише <ТаЬ> веб-элемента управления ToolTip Получает или устанавливает всплывающую подсказку веб-элемента управления, появляющуюся при наведении на элемент курсора мыши Основные категории веб-элементов управления ASP.NET Веб-элементы управления ASP. NET можно разделить на несколько обширных категорий, и все они видны в панели инструментов (Toolbox) Visual Studio 2010 (предполагается, что в визуальном конструкторе открыта страница *.aspx), как показано на рис. 33.3. В разделе Standard (Стандартные) панели инструментов находятся наиболее часто используемые элементы управления, включая Button, Label, TextBox и ListBox. В дополнение к этим замечательным элементам пользовательского интерфейса в разделе Standard также доступны и более экзотические веб-элементы управления, такие как Calendar, Wizard и AdRotator (рис. 33.4). Toolbox i Standard > Data » Validation ' Navigation l> Login f WebParts > AJAX Extensions > Dynamic Data > Reporting > HTML л General There are no usable controls in this group. Drag an item onto this text to add it to the toolbox. 1 Toolbox 1 л Standard It Pointer l3 *— 1 ® ш и §= m ъ Ш A Л ^Toc AdRotator BulletedList Button Calendar CheckBox CheckBoxList DropDownList FileUpload HiddenField HyperLink Image 'box L ^ nxl -1 Рис. 33.3. Категории веб-элементов управления ASP.NET Рис. 33.4. Веб-элементы управления ASP.NET из раздела Standard
1266 Часть VII. Построение веб-приложений с использованием ASP.NET Раздел Data (Данные) — это место, где можно найти набор элементов управления, используемых для операций привязки данных, включая новый элемент управления ASP.NET Chart, который позволяет визуализировать графики (круговые диаграммы, гистограммы и т.п.), обычно как результат операции привязки (рис. 33.5). Элементы управления проверкой достоверности (находящиеся в области Validation (Проверка достоверности) панели инструментов) очень интересны в том плане, что их можно конфигурировать для генерации блоков кода JavaScript клиентской стороны, которые проверяют входные поля на предмет корректности данных. Если случится ошибка проверки достоверности, пользователь увидит сообщение об ошибке и не сможет выполнить обратную отправку на сервер, пока не исправит ошибку. Раздел Navigation (Навигация) панели Toolbox — место, где находится небольшой набор элементов управления (Menu, SiteMapPath и TreeView), которые обычно работают в сочетании с файлом *. sitemap. Как уже кратко упоминалось ранее в этой главе, элементы управления навигацией позволяют описывать структуру многостраничного сайта, используя дескрипторы XML. По-настоящему экзотическим набором веб-элементов управления ASP NET можно назвать элементы из раздела Login (Вход), показанные на рис. 33.6. 1 Toolbox I !> Stan л Data Р К № <г> т 4- р J3 т |&Тос terd Pointer AccessDataSource Chart DataList DataPager DetailsView EntityDataSource FormView GridView LinqDataSource Ibox 1 - п x] ' „. 1 [Toolbox 1 I» Standard 1 Data 1 t> Validation 1 r> Navigation 1 * Login ^ Pointer Щщ ChangePassword §§ CreateUserWizard Si L°gin S*j LoginName ftyj LoginStatus jjjgj LoginView ^ PasswordRecovery >C Toolbox Д " ПХ| * 1 «.1 Рис. 33.5. Веб-элементы управления ASP.NET, ориентированные на данные Рис. 33.6. Веб-элементы управления ASP.NET, связанные с безопасностью Эти элементы могут радикально упростить включение в веб-приложения базовых средств безопасности (восстановление пароля, экраны входа и т.п.). Фактически эти элементы управления настолько мощны, что даже динамически создают выделенную базу данных для хранения регистрационных данных (в папке AppData веб-сайта), если нет специфической базы данных для безопасности. На заметку! Остальные категории веб-элементов управления, представленные в панели инструментов Visual Studio (WebParts (Веб-части), AJAX Extensions (Расширения AJAX) и Dynamic Data (Динамические данные)), предназначены для решения более специфичных задач программирования и здесь не рассматриваются. Краткая информация о System.Web.UI.HtmlControls В действительности, в ASP.NET поставляются два разных набора инструментов веб-элементов управления. В дополнение к веб-элементам управления ASP.NET (внутри пространства имен System.Web.UI.HtmlControls) библиотеки базовых классов также предлагают элементы управления HTML.
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1267 Элементы управления HTML — это коллекция типов, позволяющих использовать традиционные элементы управления HTML на странице веб-формы. Однако в отличие от простых HTML-дескрипторов, эти элементы являются объектно-ориентированными сущностями, которые могут быть сконфигурированы для запуска на сервере и потому поддерживают обработку событий серверной стороны. В отличие от веб-элементов управления ASP.NET, элементы HTML довольно просты по своей природе и предлагают незначительную функциональность сверх стандартных HTML-дескрипторов (HtmlButton, HtmllnputControl, HtmlTable и т.д.). Элементы управления HTML могут быть удобны, если команда четко разделена на тех, кто строит пользовательский интерфейс HTML, и разработчиков .NET. Специалисты по HTML могут пользоваться своим веб-редактором, имея дело со знакомыми дескрипторами разметки и передавая готовые HTML-файлы команде разработки. После этого разработчики .NET могут конфигурировать эти элементы управления HTML для выполнения в качестве серверных элементов управления (щелкая правой кнопкой мыши на элементе управления HTML в Visual Studio 2010). Это позволит разработчикам обрабатывать события серверной стороны и программно работать с виджетами HTML. Элементы управления HTML предоставляют общедоступный интерфейс, который имитирует стандартные атрибуты HTML. Например, для получения информации из области ввода используется свойство Value вместо принятого у веб-элементов свойства Text. Учитывая, что элементы управления HTML не столь богато оснащены, как веб- элементы управления ASP.NET, далее в этой книге они не рассматриваются. Документация по веб-элементам управления На протяжении оставшейся части книги у вас будет шанс поработать с множеством веб-элементов управления ASP.NET; однако вы определенно должны найти время, чтобы ознакомиться со сведениями о пространстве имен System.Web.UI.WebControls в документации .NET Framework 4.0 SDK. Там вы найдете объяснения и примеры кода для каждого члена пространства имен (рис. 33.7). System.Web.ULWebControis Namespace AccessDataSource Class AccessDataSourceView Class AdCreatedEventArgs Class AdCreatedEventHandfer Delegate AdRotator Class AssociatedControlConverier Class AuthenticateEventArgs Class AuthentkateEventHandler Delegate AutoCompleteType Enumeration AutoGeneratedField Class AutoGeneratedFieldProperties Class BaseCompareValidator Class BaseDataBoundControl Class BaseDataList Class BaseValidator Class BorderStyle Enumeration BoundColumn Class BoundField Class BulletedList Class BulletedListDisplayMode Enumeration BulletedListEventArgs Class BulleteclListEventHandler Delegate BulletStyfe Enumeration Button Class ButtonColumn Class ButtonColumnType Enumeration ButtonField Class Рис. 33.7. Все веб-элементы управления ASP.NET описаны в документации .NET Framework 4.0 SDK
1268 Часть VII. Построение веб-приложений с использованием ASP.NET Построение веб-сайта ASP.NET Cars Учитывая, что очень много "простых" элементов управления выглядят и ведут себя подобно своим аналогам из Windows Forms, детали базовых виджетов (Button, Label, TextBox и т.д.) подробно рассматриваться не будут. Вместо этого давайте построим веб-сайт, в котором иллюстрируется работа с некоторыми из наиболее экзотичных элементов управления, а также моделью мастер-страницы ASP.NET и особенностями механизма привязки данных. В частности, в приведенном ниже примере будут продемонстрированы следующие приемы: • работа с мастер-страницами; • работа с навигацией посредством карты сайта; • работа с элементом управления GridView; • работа с элементом управления Wizard. Для начала создайте проект Empty Web Site по имени AppNetCarsSite. Обратите внимание, что мы пока не создаем новый проект ASP.NET Web Site, поскольку в нем добавляется множество начальных файлов, которые пока еще не рассматривались. В данном проекте все необходимое будет добавляться вручную. Работа с мастер-страницами Многие веб-сайты обеспечивают согласованный внешний вид и поведение, распространяющийся на множество страниц (общая система навигации с помощью меню, общее содержимое заголовков и нижних колонтитулов, логотип компании и т.п.). В ASP.NET 1.x разработчики интенсивно использовали элементы UserControl и специальные веб-элементы управления для определения веб-содержимого, которое присутствовало на многих страницах. Хотя UserControls и специальные веб-элементы управления по-прежнему работают в ASP NET, также имеется концепция мастер-страниц, которая дополняет существующие технологии. Просто говоря, мастер-страница — это всего лишь страница ASP NET, имеющая расширение файла *. master. Сами по себе мастер-страницы не видимы в браузере клиентской стороны (фактически, исполняющая среда ASP.NET не обслуживает веб-содержимое такого рода). Вместо этого мастер-страницы определяют общую компоновку пользовательского интерфейса, разделяемую всеми страницами (или их подмножеством) сайта. Кроме того, страница *.master определяет различные области-заполнители содержимого, которые устанавливают область пользовательского интерфейса, куда могут подключаться другие файлы *.aspx. Как будет показано, файлы *.aspx, которые включают свое содержимое в мастер-страницу, выглядят и ведут себя немного иначе, чем те файлы *.aspx, которые рассматривались до сих пор. В частности, файлы *.aspx этого типа называются страницами содержимого (content page). Страницы содержимого — это файлы *.aspx, в которых не определен HTML-элемент <form> (это работа мастер- страницы). Однако с точки зрения конечного пользователя запрос осуществляется к заданному файлу *.aspx. На веб-сервере соответствующие файлы *.master и *.aspx смешиваются вместе — в единое объявление унифицированной HTML-страницы. Чтобы проиллюстрировать использование мастер-страниц и страниц содержимого, начните со вставки новой мастер-страницы в веб-сайт через пункт меню Website^Add New Item (Веб-сайт1^Добавить новый элемент); на рис. 33.8 показано результирующее диалоговое окно.
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1269 Рис. 33.8. Вставка нового файла *.master Начальная разметка файла MasterPage.master выглядит следующим образом: <%@ Master Language="C#11 AutoEventWireup="true11 CodeFile= "MasterPage.master.cs" Inherits="MasterPage11 %> <'DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtmll/DTD/xhtml1-transitional.dtd"> <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="ContentPlaceHolder1" runat="server"> </asp:ContentPlaceHolder> </div> </form> </body> </html> Первое, что здесь представляет интерес — новая директива <?d@Masternu>. В основном эта директива поддерживает те же атрибуты, что и директива <u@Pagenu>, описанная в предыдущей главе. Подобно типам Page, мастер-страница наследуется от определенного базового класса, которым в данном случае является MasterPage. Открыв соответствующий файл кода, вы увидите там следующее определение класса: public partial class MasterPage : System.Web.UI.MasterPage { protected void Page_Load(object sender, EventArgs e) } } Другим интересным моментом в разметке мастер-страницы является определение <asp:ContentPlaceHolderX В эту область мастер-страницы подключаются видже- ты пользовательского интерфейса связанного файла содержимого *.aspx, а не содержимое, определенное самой мастер-страницей. Если вы намерены подключить файл *.aspx к этой области, контекст внутри дескрипторов <asp:ContentPlaceHolder> и
1270 Часть VII. Построение веб-приложений с использованием ASP.NET </asp:ContentPlaceHolder> обычно оставляется пустым. Тем не менее, эту область можно заполнить разнообразными веб-элементами управления, которые функционируют как пользовательский интерфейс по умолчанию, если заданный файл *.aspx не предоставит специфическое содержимое. Для данного примера предположим, что каждая страница *.aspx сайта имеет специальное содержимое, и потому элементы <asp:ContentPlaceHolder> будут пустыми. На заметку! В странице *.master может быть определено произвольное количество заполнителей содержимого. К тому же отдельная страница *.master может иметь вложенные страницы *. master. Общий интерфейс файла *.master можно строить с помощью тех же визуальных конструкторов Visual Studio 2010, которые используются для создания файлов *.aspx. Для данного сайта будет добавлен описательный элемент Label (служащий в качестве общего приветственного сообщения), элемент управления AdRotator (случайно показывающий одно из двух изображений) и элемент управления TreeView (позволяющий пользователю выполнять навигацию на другие области сайта). Ниже показана возможная разметка. <html xmlns="http://www.w3.org/1999/xhtml"> <head runat=IIserver"> <title>Untitled Page</title> <asp:ContentPlaceHolder id="head11 runat="server"> </asp:ContentPlaceHolder> </head> <body> <form id="forml11 runat="server"> <div> <hr /> <asp: Label ID="Labell11 runat="server" Font-Size="XX-Large11 Text="Welcome to the ASP.NET Cars Super Site'"></asp:Label> <asp: AdRotator ID="myAdRotator11 runat="server"/> &nbsp;<br /> <br /> <asp: TreeView ID="navigationTree11 runat="server"> </asp:TreeView> <hr /> </div> <div> <asp:ContentPlaceHolder id="ContentPlaceHolderl11 runat="server"> </asp:ContentPlaceHolder> </div> </form> </body> </html> На рис. 33.9 показано представление текущей мастер-страницы во время проектирования. Внешний вид элемента управления TreeView можно улучшить, используя встроенный редактор элемента управления и выбрав ссылку Auto Format... (Автоформат). В рассматриваемом примере в результирующем диалоговом окне выбрана тема Arrow, в результате чего для этого элемента управления получена следующая разметка: <asp:TreeView ID="navigationTree" runat="server" ImageSet="Arrows"> <HoverNodeStyle Font-Underline="True11 ForeColor="#5555DD" /> <NodeStyle Font-Names="Verdana" Font-Size="8pt11 ForeColor="Black" HorizontalPadding=px" NodeSpacing="Opx" VerticalPadding="Opx" />
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1271 <ParentNodeStyle Font-Bold=,,False" /> <SelectedNodeStyle Font-Underline=,,True" ForeColor=,,#5555DD" HorizontalPadding="Opx11 VerticalPadding="Opx11 /> </asp:TreeView> MasterPagt-itMBter* X i Welcome to the ASP.NET Cars Super Site!» S Root -(Parent 1 Leaf! Leaf 2 S Parent 2 Leafl Leaf 2 a Design j П Split \ Ш Source Рис. 33.9. Разделяемый пользовательский интерфейс файла *.master Работа с логикой навигации по сайту с помощью элемента управления TreeView ASP.NET поставляется с несколькими веб-элементами управления, поддерживающими навигацию по сайту: SiteMapPath, TreeView*и Menu. Эти веб-виджеты могут быть сконфигурированы различными способами. Например, каждый из этих элементов управления может динамически генерировать свои узлы с применением внешнего XML- файла (или файла *.sitemap на основе XML), программно в коде или через разметку с использованием визуальных конструкторов среды Visual Studio 2010. Создаваемая система навигации будет динамически наполняться через файл *. sitemap. Преимущество такого подхода состоит в том, что можно определить общую структуру веб-сайта во внешнем файле и затем привязать его к элементу управления TreeView (или Menu) на лету. Таким образом, если навигационная структура веб-сайта изменится, достаточно будет просто модифицировать файл *. sitemap и перегрузить страницу. Для начала вставьте новый файл Web. sitemap в проект, выбрав пункт меню Website^Add New Item, в результате чего откроется диалоговое окно, показанное на рис. 33.10. Рис. 33.10. Вставка нового файла Web.sitemap
1272 Часть VII. Построение веб-приложений с использованием ASP.NET Как видите, файл Web. sitemap определяет элемент самого верхнего уровня с двумя подузлами: <?xml version="l. 0" encoding="utf-811 ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url=Iin title=Iin description=IIII> <siteMapNode url=Iin title=Iin description=Iin /> <siteMapNode url=Iin title=Iin description=Iin /> </siteMapNode> </siteMap> Если привязать эту структуру к элементу управления Menu, отобразится меню верхнего уровня с двумя подменю. Таким образом, когда вы захотите определить подменю, просто определите новые элементы <siteMapNode> в контексте существующего <siteMapNode>. В любом случае цель состоит в определении общей структуры веб-сайта в файле Web.sitemap с использованием множества элементов <siteMapNode>. Каждый из этих элементов может определять атрибуты заголовка и URL. Атрибут URL представляет файл *. aspx для навигации, когда пользователь щелкнет на заданном пункте меню (или на узле TreeView). Карта сайта содержит три узла (ниже узда верхнего уровня карты сайта): • Ноте (Домой): Default.aspx • Build a Car (Собрать автомобиль): BuildCar.aspx • View Inventory (Просмотреть склад): Inventory.aspx Система меню имеет единственный элемент верхнего уровня Welcome (Добро пожаловать) с тремя подэлементами. Модифицируем файл Web. sitemap следующим образом (имейте в виду, что каждое значение url должно быть уникальным; в противном случае возникнет ошибка времени выполнения). <?xml version="l .0" encoding="utf-811 ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="" title="Welcome! " description=nil> <siteMapNode url="~/Def ault .aspx" title=,,Home" description="The Home Page" /> <siteMapNode url="~/BuildCar.aspx" title="Build a car" description="Create your dream car" /> <siteMapNode url="~/Inventory.aspx" title="View Inventory" description="See what is in stock" /> </siteMapNode> </siteMap> На заметку! Префикс ~/ перед каждой страницей в атрибуте url представляет собой нотацию, указывающую на корень веб-сайта. Теперь, несмотря на то, что можно было подумать, файл Web.sitemap не ассоциируется непосредственно с элементами Menu или TreeView, используя заданное свойство. Вместо этого файл *.master или *.aspx, содержащий виджет пользовательского интерфейса, который отобразит файл Web.sitemap, должен содержать компонент SiteMapDataSource. Этот тип автоматически загрузит файл Web.sitemap в свою объектную модель при запросе страницы. Типы Menu и TreeView затем установят свои свойства DataSourcelD так, чтобы они указывали на экземпляр SiteMapDataSource. Для добавления нового SiteMapDataSource в файл *.master и автоматической установки свойства DataSourcelD можно использовать визуальный конструктор Visual Studio 2010. Откройте встроенный редактор элемента управления TreeView (щелкнув на небольшой стрелке в правом верхнем углу элемента TreeView) и выберите пункт <New Data Source...> (Новый источник данных), как показано на рис. 33.11.
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1273 MasterPage.master* X Welcome to the ASP.NET Cars Super Site!» I asp:TreeViewtMenul| ~*S Root H TreeView Tasks В Parent 1 Leafl Leaf2(J) § Para* 2 Leafl Leaf 2 ^гг^д :„ Auto Format... : Choose Data Source j(None) i Edrt Nodes-. i H Show Lines Д Deagn j a Split ! 0 Source i [^][<htmi>|pbody>|j <fortw#form2>|| <div>j <asp:TreeView#Menul> @ Рис. 33.11. Добавление нового источника SiteMapDataSource В результирующем диалоговом окне выберите значок Site Map (Карта сайта). В результате будет установлено свойство DataSourcelD элемента Menu, а также к странице добавится компонент SiteMapDataSource. Это все, что понадобится сделать, чтобы сконфигурировать элемент управления TreeView для перехода на дополнительные страницы сайта. Если необходимо выполнить дополнительную обработку при выборе пользователем пункта меню, это можно сделать, обработав событие SelectedNodeChanged. В рассматриваемом примере в этом нет необходимости, но имейте в виду, что можно определить, какой пункт меню был выбран, используя входные аргументы, связанные с событием. Установка навигационных цепочек с помощью типа SiteMapPath Прежде чем перейти к элементу управления AddRotator, добавьте тип SiteMapPath (расположенный на вкладке Navigation (Навигация) панели инструментов) в файл *.master ниже элемента-заполнителя содержимого. Этот виджет автоматически настраивает свое содержимое на основе текущего выбора системы меню. Как вам, возможно, известно, это может предоставить полезную визуальную подсказку конечному пользователю (формально эта техника пользовательского интерфейса называется навигационными цепочками или "хлебными крошками" (breadcrumbs)). По завершении вы заметите, что при выборе пункта меню Welcomed Build a Car (Добро пожаловать1^ Собрать автомобиль) виджет SiteMapPath автоматически обновится. Работа с элементом AddRotator Назначение виджета ASP.NET AdRotator заключается в показе случайно выбранного изображения в одной и той же позиции браузера. Поместите виджет AdRotator на поверхность визуального конструктора. Он отображается как пустой заполнитель. Функционально этот элемент управления не может выполнять свою работу до тех пор, пока в его свойстве AdvertisementFile не будет указан исходный файл, описывающий каждое изображение. Для данного примера источником данных будет простой XML- файл по имени Ads.xml. Чтобы добавить XML-файл к веб-сайту, выберите пункт меню Website^Add New Item (Веб-сайт1^Добавить новый элемент) и укажите опцию XML file (XML-файл). Назовите его Ads .xml и предусмотрите уникальный элемент <Ad> для каждого изображения, которое планируется показывать. Как минимум, в элементе <Ad> должно быть указано графическое изображение для показа (Imagellrl), URL для навигации, если изображение выбрано (TargetUrl), альтернативный текст (AlternateText) и вес показа (Impressions):
1274 Часть VII. Построение веб-приложений с использованием ASP.NET <Advertisements> <Ad> <ImageUrl>SlugBug.jpg</ImageUrl> <TargetUrl>http://www.Cars.com</TargetUrl> <AlternateText>Your new Car?</AlternateText> <Impressions>80</Impressions> </Ad> <Ad> <ImageUrl>car.gif</ImageUrl> <TargetUrl>http://www.CarSuperSite.com</TargetUrl> <AlternateText>Like this Car?</AlternateText> <Impressions>80</Impressions> </Ad> </Advertisements> Здесь указаны два файла с изображениями (car.gif и slugbug.jpg). В результате необходимо обеспечить наличие этих файлов в корне веб-сайта (эти файлы включены в состав кода примеров для этой книги). Чтобы добавить их к текущему проекту, выберите пункт меню Web Site•=>Add Existing Item (Веб-сайт1^Добавить существующий элемент). В этот момент можно ассоциировать XML-файл с элементом управления AdRotator через свойство AdvertisementFile (в окне Properties): <asp:AdRotator ID="myAdRotator11 runat="server" AdvertisementFile="~/Ads.xml"/> Позднее, когда вы запустите приложение и выполните обратную отправку страницы, то получите случайно выбранный из двух файлов изображения. Определение страницы содержимого Default.aspx Теперь, имея готовую мастер-страницу, можно приступить к проектированию индивидуальных страниц *.aspx, которые определят содержимое пользовательского интерфейса для вставки в дескриптор <asp:ContentPlaceHolder> мастер-страницы. Файлы *.aspx, которые объединяются с мастер-страницей, называются страницами содержимого (content page) и имеют несколько ключевых отличий от нормальной, автономной веб-страницы ASP.NET. В своей основе файл *.master определяет раздел <form> финальной HTML-страницы. Поэтому существующая область <form> внутри файла *.aspx должна быть заменена контекстом <asp:Content>. Поскольку можно вручную модифицировать разметку в начальном файле *.aspx, можно добавить к проекту новую страницу содержимого; щелкните правой кнопкой мыши в любом месте на поверхности визуального конструктора файла *.master и выберите в контекстном меню пункт Add Content Page (Добавить страницу содержимого). В результате будет сгенерирован новый файл *.aspx со следующей начальной разметкой: <%@ 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"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolderl" Runat="Server"> </asp:Content> Первым делом, обратите внимание на то, что директива <%@Раде%> дополнена новым атрибутом MasterPageFile, который указывает на файл *.master. Кроме того, вместо элемента <form> имеется контекст <asp:Content> (пока пустой), в котором значение
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1275 ContentPlaceHolderlD установлено идентично компоненту <asp: ContentPlaceHolder> в файле мастер-страницы. Имея эти ассоциации, страница содержимого знает, куда следует подключить ее содержимое, в то время как содержимое мастер-страницы отображается как предназначенное только для Чтения на странице содержимого. Нет необходимости в построении сложного пользовательского интерфейса для области содержимого Default. aspx. Для данного примера просто добавьте некоторый литеральный текст с базовыми инструкциями по сайту, как показано на рис. 33.12 (обратите внимание, что в правом верхнем углу страницы содержимого находится ссылка для переключения к связанной мастер-странице). Defauftaspx* X | Client Objects & Events ' [ (No Events) <X$ Page Language»"C#" MasterPageFile-'WMasterPage.master" AutoEventWireup="true" CodeFi le»"Default. аф] <asp:Corttent ID»uContentl" ContentPlaceHolderID«"head" Runat»"Server"> </asp:Content> ^<a*p:Content ID="Content2" ContentPlaceHolderICM»"ContentPlaceHolderl" Runat="Server"> 3 <p> ! Welcome to our site.&nbsp; Here you can purchase a new car or build your dream car. v моте l> Build a car t> View Inventory SiteMapDataSource - SiteMapDataSourcel MasterPageimaster I ContantPlaceHol der f(Cu~5toniJt£E|. - SVdcome to opt site. Here you can purchase a new car or bufld your dream car Ж Q Design [a Split~] 63 Source j \i\\<asp:Content»Conten^>] <p> Рис. 33.12. Создание первой страницы содержимого Теперь, после запуска проекта вы увидите, что содержимое пользовательского интерфейса файлов *.master и Default.aspx слиты в единый поток HTML. Как показано на рис. 33.13, конечный пользователь понятия не имеет о существовании мастер-страницы. Кроме того, если обновить страницу (нажав <F5>), элемент AdRotator покажет случайно выбранное одно из двух изображений. Welcome to the ASP.NET Cars Super Site! Want a RED slug bug? Come to CarSuperSite.com Welcome* > Home > Build a car > View» Inventory Welcome to our ske. Here you can purchase a new car or buid your dream car- Welcome! : Home Рис. 33.13. Во время выполнения мастер-страницы и страницы содержимого визуализируются в единую форму
1276 Часть VII. Построение веб-приложений с использованием ASP.NET На заметку! Имейте в виду, что мастер-страница объекта Page может быть установлена программно в обработчике события Prelnit унаследованного от Page типа с использованием унаследованного свойства Master. Проектирование страницы содержимого Inventory, aspx Чтобы вставить страницу содержимого Inventory, aspx в текущий проект, откройте страницу *.master в IDE-среде, выберите пункт меню Website^Add Content Page (Веб- сайт1^Добавить страницу содержимого) и переименуйте этот файл в Inventory.aspx. Назначение этой страницы содержимого склада состоит в отображении содержимого таблицы Inventory базы данных AutoLot внутри элемента управления GridView. Элемент управления ASP.NET GridView обладает способностью представлять в разметке данные строки соединения и SQL-операторы Select, Insert, Update и Delete (или, альтернативно — хранимые процедуры). Поэтому вместо написания всего необходимого кода ADO.NET вручную, можно позволить классу SqlDataSource генерировать разметку автоматически. С помощью визуальных конструкторов можно присвоить свойству DataSourcelD элемента GridView соответствующий компонент SqlDataSource. Выполнив несколько простых щелчков кнопкой мыши, можно настроить GridView на автоматическую выборку, обновление и удаление записей лежащего в основе хранилища данных. Хотя образ мышления "нулевого кода" значительно сокращает общий объем кода, следует помнить, что эта простота оплачивается утерей контроля и не всегда может быть лучшим подходом для приложений масштаба предприятия. Эта модель может замечательно подойти для страниц с низким трафиком, прототипирования вебсайта или небольших домашних приложений. Чтобы проиллюстрировать работу с GridView (и логикой доступа к данным) в декларативной манере, начните с обновления страницы содержимого Inventory, aspx, добавив элемент управления Label. Затем откройте инструмент Server Explorer (через меню View (Вид)) и удостоверьтесь, что добавили соединение с данными из базы AutoLot, созданной ранее при изучении ADO.NET (процесс создания соединения с данными описан в главе 21). Теперь выберите таблицу Inventory и перетащите ее на область содержимого файла Inventory.aspx. В ответ на эти действия IDE-среда выполнит следующие шаги. 1. Файл Web.config будет дополнен новым элементом <connectionStrings>. 2. Компонент SqlDataSource будет сконфигурирован необходимой логикой Select, Insert, Update и Delete. 3. Свойство DataSourcelD элемента GridView будет установлено в компонент SqlDataSource. На заметку! В качестве альтернативы конфигурировать виджет GridView можно с помощью встроенного редактора. Выберите <New Data Source> в раскрывающемся списке Choose Data Source (Выберите источник данных). Это активизирует мастер, который проведет через последовательность шагов для подключения этого компонента к необходимому источнику данных. Если вы посмотрите на открывающее объявление элемента управления GridView, то увидите, что свойство DataSourcelD установлено в только что определенный компонент SqlDataSource: <asp:GridView ID="GridViewl" runat="server" AutoGenerateColumns=llFalse11 DataKeyNames = llCarID" DataSourceID="SqlDataSource 1" EmptyDataText="There are no data records to display."> <Columns>
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1277 <asp:BoundField DataField=,,CarID" HeaderText="CarIDM ReadOnly=MTrueM SortExpression="CarID" /> <asp:BoundField DataField=,,Make" HeaderText=MMake" SortExpression=MMakeM /> <asp:BoundField DataField="Color11 HeaderText="Color11 SortExpression="Color11 /> <asp:BoundField DataField=llPetName" HeaderText=llPetName" SortExpression="PetName" /> </Columns> </asp:GridView> Тип SqlDataSource — это место, где происходит большая часть всей деятельности. В приведенной ниже разметке обратите внимание, что этот тип записывает все необходимые операторы SQL (в виде параметризованных запросов) для взаимодействия с таблицей Inventory базы данных AutoLot. Кроме того, используя синтаксис $ свойства ConnectionString, этот компонент автоматически читает значение <connectionString> из Web.config: <asp:SqlDataSource ID="SqlDataSourcel" runat="server11 ConnectionString=M<%$ ConnectionStrings:AutoLotConnectionStringl %>" DeleteCommand="DELETE FROM [Inventory] WHERE [CarlD] = @CarID" InsertCommand="INSERT INTO [Inventory] ( [CarlD], [Make], [Color], [PetName]) VALUES (@CarID, @Make, @Color, @PetName)" ProviderName="<u$ ConnectionStrings:AutoLotConnectionStringl.ProviderName n,>" SelectCommand="SELECT [CarlD], [Make], [Color], [PetName] FROM [Inventory]" UpdateCommand="UPDATE [Inventory] SET [Make] = @Make, [Color] = @Color, [PetName] = @PetName WHERE [CarlD] = @CarID"> <DeleteParameters> <asp: Parameter Name=MCarID" Type="Int3211 /> </DeleteParameters> <UpdateParameters> <asp: Parameter Name="Make11 Type=llString" /> <asp: Parameter Name="Color11 Type="String" /> <asp:Parameter Name="PetName" Type="StringM /> <asp: Parameter Name=,,CarID" Type="Int32" /> </UpdateParameters> <InsertParameters> <asp:Parameter Name=MCarID" Type="Int32" /> <asp:Parameter Name="MakeM Type=MString" /> <asp:Parameter Name="ColorM Type=MString" /> <asp:Parameter Name="PetName" Type="String" /> </InsertParameters> </asp:SqlDataSource> Теперь можете запустить веб-приложение, выбрать пункт меню View Inventory (Просмотреть склад) и просмотреть данные, как показано на рис. 33.14. (Обратите внимание, что элемент DataView имеет уникальный внешний вид, установленный с помощью встроенного визуального конструктора). Включение сортировки и разбиения на страницы Элемент управления GridView можно легко настроить для выполнения сортировки (через гиперссылки имен столбцов) и разбиения на страницы (с помощью числовых гиперссылок или гиперссылок "следующая/предыдущая"). Для этого активизируйте встроенный редактор и отметьте соответствующие флажки, как показано на рис. 33.15. Теперь страница позволяет сортировать данные щелчками на именах столбцов и прокруткой данных через постраничные ссылки (разумеется, если в таблице Inventory присутствует достаточное количество записей).
1278 Часть VII. Построение веб-приложений с использованием ASP.NET Q Untitled Page С Л it http://localhost:3123/AspNetCarsSite/ ► О* А* Welcome to the ASP.NET Cars Super Site! Want a Blue SLUG BUG? Come to Cars com! ▼ Weloome! > Home > Build в car > View Inventory CarlD Make Color PetNa 83 107 555 678 904 Ford Rust Rusty Ford Red Snake Ford Yelow Buzz Yugo Green Chmker VW Black Hank 1000 BMW Black Bimmer 1001 BMW Tan Daisy 1992 Saab Pmk Pmkey 2222 Yugo Bhic Welcome! : View Inventory Рис. 33.14. Модель "нулевого кода" компонента SqlDataSource lnventory.aspx* X | MasrerPagf .master Welcome to the ASP.NET Cars Super Site!» • Welcome' > Home t> Build a car ^ View Inventory :e - SiteMapDstaSourcel fcj !° ; l I 2 C U 5 |6 ^ S |9 rlD \iak< abc abc abc abc abc abc abc abc abc abc Color abc abc abc abc abc abc abc abc abc abc " abc j abc ' abc ! abc abc i abc abc abc abc abc Choose Data Source: ' SqlD Configure Date Source.,. Refresh Schema Edit Columns- Add New Column... Move Column Left Remove Column R£ Enable Paging ■ Enable S- | Enable Edrtii гь~ SqlDataSource - SqIDataSourcel Root Hod* : Panmt Hod» : Carrewt HmtL ' L: Enable Deleting Г.] Enable Selection Edit Templates ^ Enable scrting of rows en the GiidView | 13 Design j П Split j S Source ; f<][<«spiContent*^ontent2> j <asp:GridView*GridViewl > I Рис. 33.15. Включение сортировки и разбиения на страницы
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1279 Включение редактирования "на месте" Последнее, что еще можно сделать на этой странице — это включить поддержку редактирования "на месте" элемента управления GridView. Учитьшая, что SqlDataSource уже имеет необходимую логику Delete и Update, все, что понадобится сделать — отметить флажки Enable Deleting (Разрешить удаление) и Enable Editing (Разрешить редактирование) элемента GridView (см. рис. 33.15). После этого на странице Inventory, aspx можно будет редактировать и удалять записи, как показано на рис. 33.16, обновляя лежащую в основе таблицу Inventory базы данных AutoLot. W Q Untitled P 1* С ge ^ http://localhost3123/AspNetCarsSite/lnventory.aspK ► Q* >*- 1 1 Welcome to the ASP.NET Cars Want a RED slug bug? Come to CarSuperSite.com 1 ▼ Welcome' > Home > Build a car > yiew Inventory Edit Delete Edit Delete Edit Delete EsiBckte Edit Delete EdjtQektc. Edit Delete Edit Delete 1 Welcome 1 : S3 555 678 904 1000 1001 1992 Ford |bmi TH^T" Yugo VW BMW BMW Saab 2222 Yugo View Inventory Rust ^Red Yellow Green Black Black Tan Pmk Blue Super Site! Rusty JSnake 1 1 Buzz Choker Hank Binnner Daisy PWcey Рис. 33.16. Функциональность редактирования и удаления На заметку! Включение редактирования "на месте" для GridView требует наличия в таблице базы данных первичного ключа. Если активизировать эту опцию не удается, скорее всего, столбец CarlD не был установлен в качестве первичного ключа таблицы Inventory базы данных AutoLot. Проектирование страницы содержимого BuildCar.aspx В этом примере осталось еще спроектировать страницу содержимого BuildCar.aspx. Вставьте этот файл в текущий проект (с помощью пункта меню Website^Add Content Раде). Эта новая страница использует веб-элемент управления ASP.NET Wizard, который обеспечивает простой способ для проведения конечного пользователя через последовательность взаимосвязанных шагов. Здесь эти шаги будут эмулировать акт сборки автомобиля для покупки. Поместите в область содержимого элементы управления Label и Wizard. Затем активизируйте встроенный редактор для Wizard и щелкните на ссылке Add/Remove WizardSteps (Добавление и удаление шагов мастера). Добавьте четыре шага, как показано на рис. 33.17.
1280 Часть VII. Построение веб-приложений с использованием ASP.NET Add ^] [ Remove OK Caned Рис. 33.17. Конфигурирование элемента Wizard После определения этих шагов вы заметите, что Wizard предлагает пустую область содержимого, куда можно перетаскивать элементы управления для текущего выбранного шага мастера. Для целей рассматриваемого примера модифицируем каждый шаг, добавив следующие элементы пользовательского интерфейса (не забудьте задать подходящие идентификаторы для каждого элемента в окне Properties): • Pick Your Model (Выбор модели): элемент управления TextBox; • Pick Your Color (Выбор цвета): элемент управления ListBox; • Name Your Car (Название автомобиля): элемент управления TextBox; • Delivery Date (Дата доставки): элемент управления Calendar. Элемент управления ListBox — единственный элемент пользовательского интерфейса внутри Wizard, который требует дополнительного шага. Выберите этот элемент в визуальном конструкторе (не забыв сначала выбрать ссылку Pick Your Color) и заполните виджет набором цветов, используя свойство Items в окне Properties. После этого вы увидите в области определения Wizard разметку вроде следующей: <asp: ListBox ID="ListBoxColors11 runat="server" Width=ll237px"> <asp:ListItem>Purple</asp:Listltem> <asp:ListItem>Green</asp:ListItem> - <asp:ListItem>Red</asp:Listltem> <asp:ListItem>Yellow</asp:ListItem> <asp:ListItem>Pea Soup Green</asp:Listltem> <asp:ListItem>Black</asp:ListItem> <asp:ListItem>Lime Green</asp:Listltem> </asp:ListBox> После определения шагов можно обработать событие FinishButtonClick для автоматически сгенерированной кнопки Finish (ГЪтово). Однако имейте в виду, что кнопка Finish не будет видна вплоть до завершающего шага мастера. Выбрав последний шаг, просто выполните двойной щелчок на кнопке Finish для генерации обработчика событий. Внутри этого обработчика событий серверной стороны извлеките выбор из каждого элемента пользовательского интерфейса и постройте строку описания, которую присвойте свойству Text дополнительного элемента типа Label по имени lblOrder:
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1281 public partial class Default2 : System.Web.UI. Page { protected void Page_Load(object sender, EventArgs e) { } protected void carWizard_FinishButtonClick (object sender, System.Web.UI.WebControls.WizardNavigationEventArgs e) { // Получить каждое значение. string order = string.Format("{0}, your {1} {2} will arrive on {3}.", txtCarPetName.Text, ListBoxColors.SelectedValue, txtCarModel.Text, carCalendar.SelectedDate.ToShortDateString()); // Присвоить результирующую строку метке. lblOrder.Text = order; } } Веб-приложение AspNetCarsSite готово. На рис. 33.18 показан элемент Wizard в действии. С Л £ http://localhost3123/AspNetCarsSite/Bui!dCa ► Q- А' Welcome to the ASP.NET Cars Super Site! Want a RED slug bug? Come to CarSuperSite.com ▼ Welcome' > Home > gU'ltf g СУ > View Inventory l)se this Wizard to build your Dream Car *j»*jj|Jjj^gi I.J.'l.'iiJl.j.lJ 2 Xi U , 2S 1 2 2 4 S 2 12 И ДО 13 15 12 12 IS 12 TIP 22 22 24 25 22 И д s -l ,:- :-.; и Previous Finish j Blush, your Red Yugo will arrive on 2/12/2010. Welcome! : Build a car Рис. 33.18. Элемент управления Wizard в действии На этом рассмотрение различных ASP.NET веб-элементов управления пользовательского интерфейса, мастер-страниц, страниц содержимого и навигации с помощью карты сайта завершено. Далее мы переходим к исследованию функциональности элементов управления проверкой достоверности ASP.NET. Исходный код. Веб-сайт AspNetCarsSite доступен в подкаталоге Chapter 33.
1282 Часть VII. Построение веб-приложений с использованием ASP.NET Роль элементов управления проверкой достоверности Следующий набор элементов управления Web Forms, который мы рассмотрим, известен под общим названием элементы управления проверкой достоверности (validation controls). В отличие от других элементов управления Web Forms, эти элементы не генерируют HTML-разметку для визуализации, а применяются для генерации JavaScript-кода клиентской стороны (и, возможно, связанного с ним кода серверной стороны) в целях проверки достоверности данных формы. Как было показано в начале этой главы, проверка достоверности данных формы клиентской стороны удобна тем, что позволяет на месте проверять данные на предмет соответствия различным ограничениям, прежде чем посылать их обратно веб-серверу, тем самым сокращая дорогостоящий трафик. В табл. 33.3 дана краткая сводка по элементам управления проверкой достоверности ASP. NET. Таблица 33.3. Элементы управления проверкой достоверности ASP.NET Элемент управления Назначение CompareValidator CustomValidator RangeValidator RegularExpressionValidator RequiredFieldValidator ValidationSummary Проверяет значение в элементе ввода на равенство заданному значению другого элемента ввода или фиксированной константе Позволяет строить пользовательскую функцию проверки достоверности, которая проверяет заданный элемент управления Определяет, находится ли данное значение в пределах предварительно заданного диапазона Проверяет значение в ассоциированном элементе ввода на соответствие шаблону регулярного выражения Проверяет заданный элемент ввода на наличие значения (т.е. что он не пуст) Отображает итог по всем ошибкам проверки достоверности страницы в формате простого списка, маркированного списка или одного абзаца. Ошибки могут отображаться встроенным способом и/или во всплывающем окне сообщения Все элементы управления проверкой достоверности в конечном итоге наследуются от одного общего базового класса по имени System.Web.UI.WebControls.BaseValidator и потому обладают набором общих средств. В табл. 33.4 документированы основные члены классов элементов управления проверкой достоверности. Таблица 33.4. Общие свойства элементов управления проверкой достоверности ASP.NET Член Назначение ControlToValidate Display EnableClientScript ErrorMessage ForeColor Получает или устанавливает элемент управления, подлежащий проверке достоверности Получает или устанавливает поведение сообщений об ошибках в элементе управления проверкой достоверности Получает или устанавливает значение, указывающее, включена ли проверка достоверности клиентской стороны Получает или устанавливает текст для сообщения об ошибке Получает или устанавливает цвет сообщения, отображаемого при неудачной проверке достоверности
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1283 Чтобы проиллюстрировать работу с этими элементами управления проверкой достоверности, создайте новый проект Empty Web Site по имени ValidatorCtrls и вставьте в него новую веб-форму по имени Default.aspx. Для начала поместите на страницу четыре (именованных) элемента управления TextBox (с четырьмя соответствующими описательными элементами Label). Затем поместите на страницу рядом с каждым соответствующим полем элементы управления RequiredFieldValidator, RangeValidator, RegularExpressionValidator и CompareValidator. И, наконец, добавьте элементы Button и Label (рис. 33.19). [Fun with ASP.NET Validators Required Field: JPIease enter your name [RcquJredFiddValkiatorl] Raage 0 - 100: [RangeVafcdatorl] EiteryoerUSSSN [RegularExpressionVatidatorl] Vahc<20 [CompareValidator 1 ] PQStback 1 [lblVa&fatioaCan*>lete] 4 Design n Split -Source -4 | *body»]: < *orm*form2 >! <div> j <asp:L3bel=Labelc i> Рис. 33.19. Элементы управления проверкой достоверности обеспечат корректность данных формы перед обратной отправкой Теперь, имея начальный пользовательский интерфейс, рассмотрим процесс конфигурирования каждого элемента управления проверкой достоверности. Класс RequiredFieldValidator Конфигурирование RequiredFieldValidator осуществляется просто. Для этого соответствующим образом установите свойства ErrorMessage и ControlToValidate в Visual Studio 2010. Ниже показана результирующая разметка, которая обеспечивает проверку, что текстовое поле txtRequiredField не является пустым: <asp:RequiredFieldValidator ID="RequiredFieldValidatorl" runat="server" ControlToValidate="txtRequiredField" ErrorMessage="Oops' Need to enter data."> </asp:RequiredFieldValidator> Класс RequiredFieldValidator поддерживает свойство InitialValue. Его можно применять для проверки того, что пользователь ввел в связанном элементе TextBox какое-то значение, отличное от начального. Например, когда пользователь впервые получает страницу, можно сконфигурировать TextBox, чтобы он содержал значение "Please enter your пате". Если не установить свойство InitialValue элемента RequiredFieldValidator, исполняющая среда решит, что значение "Please enter your name" является допустимым. Таким образом, чтобы считать правильным в TextBox введенное значение, отличное от "Please enter your name", настройте виджет, как показано ниже: I
1284 Часть VII. Построение веб-приложений с использованием ASP.NET <asp:RequiredFieldValidator ID="RequiredFieldValidatorl" гunat="server" ControlToValidate="txtRequiredField" ErrorMessage="Oops! Need to enter data." InitialValue="Please enter your name"> </asp:RequiredFieldValidator> Класс RegularExpressionValidator Класс RegularExpressionValidator может использоваться, когда необходимо применить шаблон к символам, вводимым в заданном поле. Например, чтобы гарантировать ввод в заданном поле Text Box корректного номера карточки социального страхования США, виджет можно определить следующим образом: <asp:RegularExpressionValidator ID="RegularExpressionValidatorl" гunat="server" ControlToValidate="txtRegExp" ErrorMessage="Please enter a valid US SSN." ValidationExpression="\d{3}-\d{2}-\d{4}"> </asp:PegularExpressionValidator> Обратите внимание на то, как в RegularExpressionValidator определено свойство ValidationExpression. Если вы ранее не работали с регулярными выражениями, то все, что следует знать для целей рассматриваемого примера — это то, что они используются для проверки соответствия строки определенному шаблону. Здесь выражение M\d{3}-\d{2}-\d{4}" получает стандартный номер карточки социального страхования США в форме ххх-хх-хххх (где х — десятичная цифра). Это конкретное регулярное выражение достаточно очевидно; однако предположим, что требуется проверить правильность телефонного номера, скажем, в Японии. Корректное выражение для этого случая выглядит намного сложнее: " @\d{l,4}-|\ @\d{l,4}\)?)?\d{l,4}-\d{4}M. Удобно то, что при выборе свойства ValidationExpression в окне Properties доступен предварительно определенный список распространенных регулярных выражений (по щелчку на кнопке со стрелкой вниз). На заметку! За программные манипуляции регулярными выражениями в .NET отвечают два пространства имен — System.Text.RegularExpressions и System.Web. RegularExpressions. Класс RangeValidator В дополнение к свойствам MinimumValue и MaximumValue, в классе RangeValidator имеется свойство по имени Туре. Поскольку нужно проверять пользовательский ввод на предмет вхождения в диапазон целых чисел, следует указать тип Integer (это не является установкой по умолчанию): <asp:RangeValidator ID="RangeValidatorl" runat="server" ControlToValidate="txtRange" ErrorMessage="Please enter value between 0 and 100." MaximumValue=M100M MinimumValue=" Type="lnteger"> </asp:RangeValidator> Класс RangeValidator также может использоваться для проверки вхождения в диапазон денежных значений, дат, чисел с плавающей точкой и строковых данных (установка по умолчанию). Класс CompareValidator Наконец, обратите внимание, что CompareValidator поддерживает свойство Operator:
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1285 <asp:CompareValidator ID="CompareValidator1" runat="server" ControlToValidate="txtComparison" ErrorMessage="Enter a value less than 20." Operator="LessThan" ValueToCompare= 0" Type="Integer"> </asp:CompareValidator> Учитывая, что предназначение элементов управления проверкой достоверности состоит в сравнении значения в текстовом поле и другого значения с использованием бинарной операции, не удивительно, что свойство Operator может принимать такие значения, как LessThan, GreaterThan, Equal и NotEqual. В ValueToCompare указывается значение, с которым нужно сравнивать. Обратите внимание, что атрибут Туре установлен в Integer. По умолчанию CompareValidator выполняет сравнения со строковыми значениями. На заметку! Элемент CompareValidator также может быть настроен для сравнения со значением внутри другого элемента управления Web Forms (а не с жестко закодированной константой) посредством свойства ControlToValidate. Чтобы завершить код этой страницы, обработайте событие Click элемента управления Button и информируйте пользователя об успешном прохождении логики проверки достоверности: public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btnPostback_Click(object sender, EventArgs e) { lblValidationComplete.Text = "You passed validation!"; } } Загрузите готовую страницу в браузер. Сначала вы не увидите каких-либо заметных изменений. Однако при попытке щелкнуть на кнопке Submit (Отправить) после ввода неверных данных появится сообщение об ошибке. После ввода правильных данных сообщение об ошибке исчезнет и произойдет обратная отправка. Взглянув на HTML-разметку, визуализированную браузером, вы обнаружите, что элементы управления проверкой достоверности генерируют JavaScript-функцию клиентской стороны, которая использует специфическую библиотеку функций JavaScript, автоматически загружаемую на пользовательскую машину. Как только проверка достоверности прошла, данные формы отправляются обратно на сервер, где исполняющая среда выполняет ту лее самую проверку еще раз на веб-сервере (просто чтобы удостовериться, что данные не были подделаны в пути). Кстати, если HTTP-запрос был прислан браузером, который не поддерживает JavaScript клиентской стороны, то вся проверка достоверности проходит на сервере. Таким образом, программировать элементы управления проверкой достоверности можно, не задумываясь о целевом браузере; возвращенная HTML-страница переадресует обработку ошибок веб-серверу. Создание итоговой панели проверки достоверности Следующая тема, касающаяся проверки достоверности, которую мы рассмотрим здесь — применение виджета ValidationSummary. В данный момент каждый элемент управления проверкой достоверности отображает свое сообщение об ошибке именно в том месте, куда он был помещен во время проектирования. Во многих случаях именно
1286 Часть VII. Построение веб-приложений с использованием ASP.NET это и требуется. Однако в сложных формах с многочисленными виджетами ввода вариант с засорением формы многочисленными надписями красного цвета может не устроить. С помощью типа ValidationSummary можно заставить все типы проверки достоверности отображать свои сообщения об ошибках в определенном месте страницы. Первый шаг состоит в помещении ValidationSummary в файл *.aspx. Дополнительно можно установить свойство Header Text этого типа вместе с DisplayMode, которое по умолчанию отобразит список всех сообщений об ошибках в виде маркированного списка. <asp:ValidationSummary id="ValidationSummaryl" runat="server" Width=53px" HeaderText="Here are the things you must correct."> </asp:ValidationSummary> Затем необходимо установить свойство Display в None для всех индивидуальных элементов управления проверкой достоверности (т.е. RequiredFieldValidator, RangeValidator и т.д.). Это гарантирует отсутствие дублированных сообщений об ошибках при каждой неудаче проверки достоверности (одно — в итоговой панели, а другое — в месте расположения элементы управления проверкой достоверности). На рис. 33.20 показана итоговая панель в действии. Required Field: Please enter your name Range 0 - 100: EBteryoMrUSSSN Andrew Troelsen Value < 20 999999999 I" Post back ] Here are die things you must correct • Oops! Need to enter data. • Please enter value between 0 and 100. • Please enter a valid US SSN. • Enter a value less than 20. Рис. 33.20. Использование итоговой панели проверки достоверности И последнее: если вы вместо этого хотите отображать сообщения об ошибках в окне сообщений клиентской стороны, установите свойство ShowMessageBox элемента управления ValidationSummary в true, а свойство ShowSummary — в false. Определение групп проверки достоверности Можно также определять группы, к которым относятся элементы управления проверкой достоверности. Это полезно, когда имеются области страницы, которые работают как единое целое. Например, может существовать одна группа элементов управ-
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1287 ления в объекте Panel, предназначенная для ввода пользователем адреса электронной почты, и другая группа в другом объекте Panel — для ввода информации о кредитной карте. С помощью групп можно конфигурировать каждый набор элементов управления для независимой проверки достоверности. Вставьте в текущий проект новую страницу по имени ValidationGroups.aspx, определяющую две Panel. Первый объект Panel будет содержать Text Box для некоторого пользовательского ввода (через RequiredFieldValidator), а второй объект Panel — для ввода номера карточки социального страхования США (через RegularExpressionValidator). На рис. 33.21 показан возможный вариант пользовательского интерфейса. Рис. 33.21. Эти объекты Panel независимо конфигурируют свои области ввода Чтобы обеспечить независимую проверку достоверности, просто включите элемент управления проверкой достоверности и проверяемый элемент управления в уникально именованную группу, используя свойство ValidationGroup. В следующем примере разметки обратите внимание, что применяемые здесь обработчики события Click остаются, по сути, пустыми в файле кода и служат только для обратной отправки на веб-сервер: <form id="forml" runat="server"> <asp:Panel ID=MPanell" runat="server" Height=M83pxM Width=96pxM> <asp:TextBox ID="txtRequiredData" runat="server" ValidationGroup="Firs tGroup"> </asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidatorl" runat="server" ErrorMessage="*Required field'" ControlToValidate="txtRequiredData" ValidationGroup="FirstGroup"> </asp:RequiredFieldValidator> <asp:Button ID="bntValidateRequired" runat="server" OnClick="bntValidateRequired_Click" Text="Validate" ValidationGroup="FirstGroup" /> </asp:Panel> <asp: Panel ID="Panel2M runat="server" Height=19px" Width=M295pxn> <asp:TextBox ID="txtSSN" runat="server" ValidationGroup="SecondGroup"> </asp:TextBox> <asp:RegularExpressionValidator ID="RegularExpressionValidatorl" runat="server" ControlToValidate="txtSSN" ErrorMessage="*Need SSN" ValidationExpression="\d{3}-\d{2}-\d{4}" ValidationGroup="SecondGroup"> </asp:RegularExpressionValidator>&nbsp;
1288 Часть VII. Построение веб-приложений с использованием ASP.NET <asp: Button ID=,,btnValidateSSN" runat=,,server" OnClick="btnValidateSSN_Click" Text="Validate" ValidationGroup="SecondGroup" /> </asp:Panel> </form> Теперь щелкните правой кнопкой мыши на поверхности визуального конструктора этой страницы и выберите в контекстном меню пункт View In Browser (Просмотреть в браузере), чтобы удостовериться, что проверка данных в элементах каждой панели происходит во взаимоисключающей манере. Исходный код. Веб-сайт ValidatorCtrls доступен в подкаталоге Chapter 33. Работа с темами До этих пор вы работали с многочисленными веб-элементами управления ASP.NET. Как было показано, каждый из них предоставляет набор свойств (многие из которых унаследованы от System.Web.Ul.Webcontrols.WebControl), позволяющих настраивать внешний вид и поведение этих элементов пользовательского интерфейса (цвет фона, размер шрифта, стиль рамки и т.п.). Конечно, на многостраничном веб-сайте принято определять общий внешний вид и поведение виджетов различных типов. Например, все TextBox должны поддерживать определенный шрифт, все Button — иметь общий вид, а все Calendar — ярко-синюю рамку. Очевидно, что было бы очень трудоемкой (и чреватой ошибками) задачей задавать одинаковые установки свойств для каждого виджета на каждой странице веб-сайта. Даже если вы в состоянии вручную обновить свойства каждого виджета пользовательского интерфейса на каждой странице, представьте, насколько мучительным может стать обновление цвета фона каждого TextBox, когда это понадобится вновь. Ясно, что должен существовать другой путь применения настроек пользовательского интерфейса на уровне всего сайта. Один из возможных подходов, позволяющий упростить установку общего внешнего вида пользовательского интерфейса, заключается в определении таблиц стилей. Если у вас есть опыт веб-разработки, то вы знаете, что таблицы стилей определяют общий набор настроек пользовательского интерфейса, применяемых в браузере. Как и можно было ожидать, веб-элементы управления ASP.NET могут принимать стиль за счет присваивания значения свойству CssStyle. Однако ASP.NET поставляется с дополняющей технологией для определения общего пользовательского интерфейса, которая называется темами. В отличие от таблиц стилей, темы применяются на веб-сервере (а не в браузере) и могут использоваться как программно, так и декларативно. Учитывая, что тема применяется на веб-сервере, она имеет доступ ко всем серверным ресурсам веб-сайта. Более того, темы определяются написанием той же разметки, которую можно найти в любом файле *.aspx (согласитесь, что синтаксис таблиц стилей чересчур лаконичный). Вспомните из главы 32, что веб-приложения ASP.NET могут определять любое количество "специальных" каталогов, один из которых — AppThemes. Этот единственный каталог может быть дальше разбит на подкаталоги, каждый из которых представляет одну из возможных тем веб-сайта. Например, взгляните на рис. 33.22, на котором показан один каталог AppThemes, содержащий три подкаталога, каждый из которых имеет набор файлов, образующих саму тему.
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1289 App_Therae Holiday_Theme Dramatic_Theme Basic_Green Holiday.skin Premadonna.skin SimpleCtrls.skin HolidayImages. xml TheScream.tif GridViewData.skin Snow.ti f CompanyLogo.tif Рис. 33.22. Один каталог App_Themes может определять многочисленные темы Файлы *.skin Каждый подкаталог темы обязательно содержит файл *.skin. Эти файлы определяют внешний вид и поведение различных веб-элементов управления. Для иллюстрации создайте новый веб-сайт по имени FunWithThemes. Добавьте новый файл *.skin (используя пункт меню Website^Add New Item) по имени BasicGreen.skin, как показано на рис. 33.23. Рис. 33.23. Вставка файла *.skin Среда Visual Studio 2010 предложит подтвердить добавление этого файла в Арр_ Themes (что как раз и требуется). Если теперь заглянуть в Solution Explorer, в каталоге AppThemes будет виден подкаталог BasicGreen, который содержит новый файл BasicGreen.skin. В файле *.skin задается внешность и поведение различных виджетов с использованием декларативного синтаксиса элементов управления ASP.NET. К сожалению, в IDE- среде не предусмотрена поддержка визуального конструктора для файлов *. skin. Один из способов сокращения объема ввода состоит в добавлении к программе временного файла *.aspx (например, temp.aspx), который может быть использован для построения пользовательского интерфейса виджетов с помощью визуального конструктора страниц Visual Studio 2010.
1290 Часть VII. Построение веб-приложений с использованием ASP.NET Полученную в результате разметку можно затем скопировать в файл *.skin. При этом, однако, вы обязаны удалить атрибут ID каждого веб-элемента управления. Это имеет смысл, поскольку мы не определяем вид и поведение конкретного элемента Button (например), а всех элементов Button. С учетом сказанного, вот как может выглядеть разметка для BasicGreen.skin, которая определяет внешний вид и поведение по умолчанию для типов Button, TextBox и Calendar: <asp:Button runat="server" BackColor="#80FF80"/> <asp:TextBox runat="server" BackColor="#80FF80"/> <asp:Calendar runat="server" BackColor="#80FF80"/> Обратите внимание, что каждый виджет по-прежнему имеет атрибут runat="server" (что обязательно), и ни одному из них не назначен атрибут ID. Теперь определим вторую тему по имени CrazyOrange. Используя Solution Explorer, щелкните правой кнопкой мыши на папке AppThemes и добавьте новую тему по имени CrazyOrange. Это приведет к созданию нового подкаталога внутри папки AppThemes. Затем щелкните правой кнопкой мыши на новой папке CrazyOrange в Solution Explorer и выберите в контекстном меню пункт Add New Item (Добавить новый элемент). В открывшемся диалоговом окне добавьте новый файл *.skin. Обновите файл CrazyOrange.skin, определив уникальный внешний вид и поведение для тех же веб- элементов управления. Например: <asp:Button runat="server" BackColor="#FF8000'7> <asp:TextBox runat="server" BackColor="#FF8000"/> <asp:Calendar BackColor="White" BorderColor="Black" BorderStyle="Solid" CellSpacing="l" Font-Names="Verdana" Font-Size="9pt" ForeColor="Black" Height=50px" NextPrevFormat="ShortMonth" Width=30px" runat="server"> <SelectedDayStyle BackColor="#333399" ForeColor="White" /> <OtherMonthDayStyle ForeColor="#999999" /> <TodayDayStyle BackColor="#999999" ForeColor="White" /> <DayStyle BackColor="#CCCCCC" /> <NextPrevStyle Font-Bold="True" Font-Size="8pt" ForeColor="White" /> <DayHeaderStyle Font-Bold="True" Font-Size="8pt" ForeColor="#333333" Height="8pt" /> <TitleStyle BackColor="#333399" BorderStyle="Solid" Font-Bold="True" Font-Size=2pt" ForeColor="White" Height=2pt" /> </asp:Calendar> После этого окно Solution Explorer должно выглядеть, как показано на рис. 33.24. Шхр1огег I I33 Solution 'FunW'rthThemes' (l project) I л J H:\_\FunWit hTnemes\ -j App_Data j jr App_Thernes л .^J BasicGreen J% BasicGreen.skin л Шг CrazyOrange jjjl' CrazyOrange.skin > (Щ Defaurt.aspx _* web.config Ц^ДЦЦДЦЦ£1^^ - j Solution Explor... Рис. 33.24. Веб-сайт с несколькими темами
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1291 Теперь, когда сайт имеет множество тем, возникает следующий логичный вопрос: как применить их к страницам? Как и можно было ожидать, существует несколько путей сделать это. На заметку! Дизайн рассматриваемых примеров тем довольно прост (с целью экономии места на печатной странице). Вы можете развить их по своему вкусу. Применение тем ко всему сайту Если вы хотите обеспечить оформление каждой страницы сайта в одной и той же теме, проще всего обновить файл Web. con fig. Откройте текущий файл Web. con fig и определите элемент <pages> внутри контекста корневого элемента <system.web>. Добавление атрибута theme к элементу <pages> гарантирует применение одной и той же темы ко всем страницам сайта (разумеется, значением этого атрибута должно быть имя одного из подкаталогов внутри AppThemes). Ниже показано основное изменение: <configuration> <system.web> <pages theme="BasicGreen"> </pages> </system.web> </configuration> Если теперь добавить различные элементы Button, Calendar и TextBox к файлу Default.aspx и запустить приложение, каждый виджет получит пользовательский интерфейс, определенный темой BasicGreen. Если изменить значение атрибута темы на CrazyOrange и снова запустить приложение, пользовательский интерфейс будет соответствовать заданному темой CrazyOrange. Применение тем на уровне страницы Темы также можно применять к отдельным страницам. Это полезно в различных ситуациях. Например, возможно, в файле Web. con fig определена тема для всего сайта (как описано в предыдущем разделе); однако определенной странице должна быть назначена другая тема. Для этого понадобится просто обновить директиву <%@Раде>. При использовании Visual Studio 2010 средство IntelliSense отобразит все доступные темы, определенные в папке Арр _ Themes. <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Theme ="CrazyOrange" %> Поскольку этой странице назначена тема CrazyOrange, а в файле Web.conf ig указана тема BasicGreen, то все страницы кроме этой будут визуализированы с темой BasicGreen. СВОЙСТВО SkinID Иногда может потребоваться определить набор возможных внешних видов для отдельного виджета. Например, предположим, что необходимо определить два возможных пользовательских интерфейса для типа Button внутри темы CrazyOrange. В этом случае для различения каждого варианта внешнего вида служит свойство SkinID элемента управления внутри файла *.skin: <asp:Button runat="server" BackColor="#FF8000"/> <asp:Button runat="server" SkinID = "BigFontButton" Font-Size=0pt" BackColor="#FF8000"/>
1292 Часть VII. Построение веб-приложений с использованием ASP.NET Теперь, если есть страница, которая использует тему CrazyOrange, каждому элементу Button по умолчанию будет назначена неименованная обложка Button. Если необходимо иметь несколько разных кнопок в одном файле *.aspx, используйте обложку BigFontButton, просто указывая ее в свойстве SkinID: <asp:Button ID="Button2" runat="server" SkinID= "BigFontButton" Text="Button" /xbr /> Программное назначение тем И последнее, что мы рассмотрим — назначение темы в коде. Это может пригодиться, когда необходимо предоставить конечным пользователям возможность выбора темы для текущего сеанса. Конечно, мы еще не рассматривали построение веб-приложений с поддержкой состояния, поэтому выбранная подобным образом тема будет утеряна между обратными отправками. В реальном рабочем сайте текущая тема пользователя сохраняется внутри переменной сеанса или же записывается в базу данных. Для иллюстрации программного назначения темы добавьте к пользовательскому интерфейсу в файле Default.aspx три элемента управления Button, как показано на рис. 33.25. Обработайте событие Click каждого из этих элементов Button. Defaultaspx X Fun with Themes i asp:Button»btnNo1heme| Green Theme | btnOrangeTheme No Theme fi> Here are the controls which will be themed HHHHi Гка Fri Sat | 4 5 6 5 < February 2010 Son ЛI on Tup Wed Thu 31 1 2 3 4 5 7 8 9 10 11 12 13 j 14 15 16 17 18 19 20 ; 21 22 23 24 25 26 27 j Q Design J □ Split j сЭ Source i |4|{<dtv>| <asp:Biitton»btnNoThcn>e> И Рис. 33.25. Обновленный пользовательский интерфейс примера применения тем Теперь имейте в виду, что назначать тему программно можно только на определенных фазах жизненного цикла страницы. Обычно это делается внутри обработчика события PagePrelnit. С учетом сказанного, модифицируйте файл кода следующим образом: partial class _Default : System.Web.UI.Page { protected void btnNoTheme_Click(object sender, System.EventArgs e) { // Пустая строка означает, что тема не применяется. Session["UserTheme"] = "" ; // Снова инициировать событие Prelnit. Server.Transfer(Request.FilePath); protected void btnGreenTheme_Click(object sender, System.EventArgs e) { Session["UserTheme"] = "BasicGreen";
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1293 // Снова инициировать событие Prelnit. Server.Transfer(Request.FilePath); } protected void btnOrangeTheme_Click(object sender, System.EventArgs e) { Session["UserTheme"] = "CrazyOrange"; // Снова инициировать событие Prelnit. Server.Transfer(Request.FilePath); } protected void Page_PreInit(object sender, System.EventArgs e) { try { Theme = Session [ "UserTheme" ]. ToStnng () ; I catch { Theme = " " ; } } } Обратите внимание, что выбранная тема сохраняется в переменной сеанса (детали ищите в главе 34) по имени UserTheme, значение которой формально присваивается внутри обработчика событий Page_PreInit(). Когда пользователь щелкает на заданной кнопке Button, мы вновь программно инициируем событие Prelnit, вызывая для этого метод Server.Transfer() и вновь запрашивая текущую страницу. Запустив эту страницу, вы обнаружите, что можно переключать темы щелчками на разных кнопках Button. Исходный код. Веб-сайт FunWithThemes доступен в подкаталоге Chapter 33. Резюме В этой главе рассматривалось использование разнообразных веб-элементов управления ASP.NET. Начали мы с изучения роли базовых классов Control и WebControl, после чего перешли к рассмотрению динамического взаимодействия с внутренней коллекцией элементов управления панели. По пути мы познакомились с новой моделью навигации сайта (файлами *.sitemap и компонентом SiteMapDataSource), новым механизмом привязки данных (через компонент SqlDataSource и элемент управления GridView), a также различными элементами управления проверкой достоверности. Вторая часть главы была посвящена роли мастер-страниц и темам. Вспомните, что мастер-страницы могут использоваться для определения общей разметки для набора страниц сайта. Также вспомните, что файл *.master определяет любое количество мест заполнения содержимым, куда страницы содержимого вставляют свои элементы пользовательского интерфейса. И, наконец, было показано, каким образом с помощью механизма тем ASP.NET декларативно или программно применять общий внешний вид и поведение пользовательского интерфейса к виджетам на веб-сервере.
ГЛАВА 34 Управление состоянием в ASP.NET В предыдущих двух главах внимание было сосредоточено на композиции и поведении страниц ASP.NET, а также веб-элементов управления, которые они содержат. Настоящая глава основывается на этой информации и посвящена роли файла Global. asaxH лежащего в его основе типа HttpApplication. Как будет показано, тип HttpApplication обеспечивает перехват многочисленных событий, которые позволяют трактовать веб-приложение как единую сущность, а не просто набор отдельных файлов * . aspx, управляемых мастер-страницей. В дополнение к исследованию типа HttpApplication, в главе также рассматриваются все важные темы, касающиеся управления состоянием. Вы узнаете о роли состояния представления, переменных сеанса и приложения (включая кэш приложения), cookie-данных и API-интерфейса ASP.NET Profile. i Проблема поддержки состояния В начале главы 32 упоминалось, что HTTP является протоколом, не поддерживающим состояние. Это делает веб-разработку совершенно отличающейся от привычного процесса построения исполняемой сборки. Например, при разработке приложения Windows Forms можно иметь уверенность, что переменные-члены, определенные в классе-наследнике Form, будут существовать в памяти до тех пор, пока пользователь явно не завершит исполняемую программу: public partial class MainWindow : Form { // Постоянные данные! private string userFavoriteCar = "Yugo"; } Однако в среде World Wide Web нельзя исходить из такого же удобного предположения. Чтобы удостовериться в этом, создайте проект Empty Web Site по имени SimpleStateExample и вставьте в него веб-форму. Внутри файла отделенного кода определите строковую переменную уровня страницы по имени userFavoriteCar: public partial class _Default : System.Web.UI. Page { // Постоянные данные? private string userFavoriteCar = "Yugo"; protected void Page_Load(object sender, EventArgs e) { } }
Глава 34. Управление состоянием в ASP.NET 1295 Затем сконструируйте пользовательский интерфейс, как показано на рис. 34.1. Defaul idivl (Simple State Example Set Favorite Car Get Favorite Car [bffavCar] •Л Design j □ Split '; 13 Source ! \i\phtmlTj[<body>|[<form#forml>] <div> 0 Рис. 34.1. Пользовательский интерфейс для простой страницы с состоянием Обработчик события Click серверной стороны для кнопки Set Favorite Car (Установить предпочитаемый автомобиль) по имени btnSetCar позволяет пользователю присвоить переменной-члену типа string значение, взятое из Text Box (по имени txtFavCar): protected void btnSetCar_Click(object sender, EventArgs e) { // Запомнить предпочитаемый автомобиль в переменной-члене. userFavoriteCar = txtFavCar.Text; } Обработчик события Click для кнопки Get Favorite Car (Получить предпочитаемый автомобиль) по имени btnGetCar отображает текущее значение переменной-члена внутри виджета Label (по имени IblFavCar) страницы: protected void btnGetCar_Click(object sender, EventArgs e) { // Отобразить значение переменной-члена. IblFavCar.Text = userFavoriteCar; Если бы строилось приложение Windows Forms, можно было бы предположить, что как только пользователь установил начальное значение, оно запоминается на все время жизни настольного приложения. К сожалению, запустив это веб-приложение, вы обнаружите, что всякий раз, когда выполняется обратная отправка на веб-сервер (в результате щелчка на любой кнопке), значение строковой переменной userFavoriteCar устанавливается обратно в свое начальное значение "Yugo", и потому текст в Label не изменяется. Учитывая то, что HTTP не имеет понятия о том, как запомнить данные после отправки ответа HTTP, получается так, что объект Раде уничтожается почти постоянно. В связи с этим, когда клиент отправляет обратно файл * . aspx, конструируется новый объект Раде, который сбрасывает значение всех переменных-членов уровня страницы. Понятно, что это — серьезная сложность. Представьте, насколько бесполезным была бы онлайновая торговля, если при каждой отправке данных на веб-сервер вся информация, которую вы ввели ранее (такая как наименования товаров, которые планируется приобрести), была отброшена. Когда требуется запомнить информацию о пользователях, которые зашли на сайт, приходится применять различные приемы управления состоянием. На заметку! Эта проблема никоим образом не ограничена ASP.NET. Веб-приложения Java, приложения CGI, приложения на классическом ASP и приложения РНР также сталкиваются с задачей сохранения состояния.
1296 Часть VII. Построение веб-приложений с использованием ASP.NET Чтобы сохранить значение строковой переменной userFavoriteCar между обратными отправками, один из подходов предполагает запись этого значения в переменную сеанса (session variable). Далее в этой главе вы ознакомитесь с деталями применения переменных сеанса. Однако для полноты примера внесем необходимые изменения в текущую страницу (обратите внимание, что приватная переменная-член типа string больше не используется, потому ее можно закомментировать либо вообще удалить из определения класса): public partial class _Default : System.Web.UI. Page { // Постоянные данные? // private string userFavoriteCar = "Yugo"; protected void Page_Load(object sender, EventArgs e) { } protected void btnSetCar_Click(object sender, EventArgs e) { // Запомнить значение в переменной сеанса. Session["UserFavCar"] = txtFavCar.Text; } protected void btnGetCar_Click(object sender, EventArgs e) { // Получить значение из переменной сеанса. lblFavCar.Text = (string)Session["UserFavCar"]; } } Если теперь запустить приложение, то запись о предпочитаемом автомобиле будет сохранена между обратными отправками, благодаря объекту HttpSessionState, которым опосредовано манипулирует унаследованное свойство Session. Исходный код. Веб-сайт SimpleStateExample доступен в подкаталоге Chapter 34. Приемы управления состоянием ASP.NET В ASP.NET предлагается несколько механизмов для поддержки информации о состоянии внутри веб-приложений. В частности, на выбор доступны следующие варианты: • использование состояния представления ASP.NET; • использование состояния элемента управления ASP.NET; • определение переменных уровня приложения; • использование объекта кэша; • определение переменных уровня сеанса; • определение cookie-данных. В дополнение к перечисленным выше подходам, для сохранения пользовательских данных на постоянной основе а ASP.NET предлагается готовый API-интерфейс Profile. Мы рассмотрим детали каждого из упомянутых подходов, начав с состояния представления ASP.NET. Роль состояния представления ASP.NET Понятие состояние представления (view state) уже несколько раз встречалось в предыдущих главах без формального определения, поэтому давайте проясним его сейчас.
Глава 34. Управление состоянием в ASP.NET 1297 В классическом ASP (на основе СОМ) веб-разработчики вынуждены были вручную перезаполнять значения, поступающие от виджетов, во время конструирования исходящего ответа HTTP. Например, если входящий запрос HTTP содержал пять текстовых полей с определенными значениями, то в файле * . asp необходим был сценарный код для извлечения текущих значений (через коллекцию QueryString или объект Request) и помещения их обратно в поток ответа HTTP (излишне говорить, что это был кошмар). Если разработчик забывал это сделать, то клиент сталкивался с пятью пустыми текстовыми полями немедленно после отправки. В ASP.NET мы более не обязаны вручную собирать и заново заполнять значениями виджеты HTML, потому что исполняющая среда ASP.NET автоматически встраивает скрытое поле формы (по имени VIEWS ТАТЕ), которое передается между браузером и определенной страницей. Данные, присвоенные этому полю, представляют собой закодированную по алгоритму Base64 строку, содержащую набор пар "имя/значение", которые соответствуют значениям виджетов пользовательского интерфейса на странице. Обработчик событий базового класса Init из пространства имен System.Web. UI. Page представляет собой сущность, ответственную за чтение входных значений из поля VIEWS ТАТЕ для наполнения соответствующих переменных-членов производного класса. (Вот почему обращаться к состоянию виджета в контексте обработчика события Init страницы, как минимум, рискованно.) Кроме того, непосредственно перед отправкой исходящего ответа запросившему браузеру данные VIEWSTATE используются для повторного наполнения виджетов формы. Лучшее в этом аспекте ASP.NET то, что все это происходит без какого-либо участия с вашей стороны. Конечно, при необходимости всегда можно взаимодействовать, изменять или отключать эту функциональность. Чтобы понять, как это делается, давайте рассмотрим конкретный пример состояния представления. Демонстрация работы с состоянием представления Создайте новый проект Empty Web Site по имени ViewStateApp и добавьте в него новую веб-форму. На страницу *.aspx поместите веб-элемент управления ASP.NET ListBox по имени myListBox и элемент управления Button по имени btnPostback. Обработайте событие Click элемента Button, чтобы при этом пользователь мог выполнить обратную отправку на веб-сервер: public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btnPostback_Click(object sender, EventArgs e) { // Никаких действий. Это нужно, чтобы разрешить обратную отправку. } } Теперь в окне Properties (Свойства) Visual Studio 2010 обратитесь к свойству Items и добавьте четыре элемента List Items к ListBox, используя ассоциированное диалоговое окно. В результате получается следующая разметка: «■'asp: ListBox ID="myListBox" runat="server"> <asp:Listltem>ltem 0ne</asp:Listltem> <asp:Listltem>ltem Two</asp:Listltem> <asp:Listltem>ltem Three</asp:Listltem> <asp:ListItem>Item Four</asp:Listltem> </asp:ListBox>
1298 Часть VII. Построение веб-приложений с использованием ASP.NET Обратите внимание, что элементы списка ListBox жестко закодированы внутри файла * . aspx. Как уже известно, все определения <asp: > в форме HTML будут автоматически визуализированы в свои HTML-представления перед отправкой финального ответа HTTP (благодаря наличию атрибута runat="server"). Директива <%@Page%> имеет необязательный атрибут по имени EnableViewState, который по умолчанию установлен в true. Чтобы отключить это поведение, измените директиву <%@Раде%> следующим образом: <ь@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" EnableViewState ="false" %> Итак, что вообще означает отключение состояния представления? Это зависит от обстоятельств. Исходя из предыдущего определения термина, могло бы показаться, что если отключить состояние представления для файла * . aspx, то значения в ListBox не будут запоминаться между обратными отправками на веб-сервер. Однако если запустить приложение в том виде, как оно есть, легко заметить, что информация в ListBox сохраняется, независимо от того, сколько" раз производится обратная отправка страницы. Фактически, просмотрев HTML-разметку, возвращенную в браузер (щелкнув правой кнопкой мыши на странице внутри браузера и выбрав в контекстном меню пункт View Source (Исходный код страницы)), вы удивитесь еще больше, обнаружив, что скрытое поле VIEWS ТАТЕ по-прежнему присутствует: <input type="hidden" name=" VIEWSTATE" id=" VIEWSTATE" value="/wEPDwUKLTM4MTM2MDM4NGRkqGC6g]EV25JnddkJiRmoIcl0SIA=" /> Однако предположим, что ListBox наполняется динамически внутри файла отделенного кода вместо того, чтобы определяться внутри HTML-дескриптора <form>. Удалите объявления <asp:ListItem> из текущего файла * .aspx: <asp:ListBox ID="myListBox" runat="server"> </asp:ListBox> Затем заполните список элементами внутри обработчика событий Load в файле отделенного кода: protected void Page_Load(object sender, EventArgs e) { if (HsPostBack) { // Заполним ListBox динамически! myListBox.Items.Add("Item One"); myListBox.Items.Add("Item Two"); myListBox.Items.Add("Item Three"); myListBox.Items.Add("Item Four"); } } Отправив эту обновленную страницу, вы увидите, что при первом запросе ее браузером значения ListBox присутствуют в том виде, как они были определены. Однако после обратной отправки ListBox внезапно становится пустым. Первое правило состояния представления ASP.NET состоит в том, что оно работает только тогда, когда есть виджеты, значения которых динамически генерируются кодом. Если жестко закодировать эти значения внутри дескрипторов <form> файла * .aspx, то состояние этих элементов будет всегда запоминаться между обратными отправками (даже если для страницы EnableViewState установлено в false). Если идея отключения состояния представления для всего файла * .aspx кажется чересчур экстремальной, имейте в виду, что каждый потомок базового класса System.
Глава 34. Управление состоянием в ASP.NET 1299 Web .UI .Control наследует свойство EnableViewState, которое существенно упрощает задачу отключения состояния представления отдельно для каждого элемента управления: <asp:GridView id="myHugeDynamicallyFilledGridOfData" runat="server" EnableViewState="false"> </asp:GridView> На заметку! В .NET 4.0 объемные данные состояния представления автоматически сжимаются, чтобы сократить размер этого скрытого поля формы. Добавление специальных данных в состояние представления В дополнение к свойству EnableViewState, класс System.Web.UI .Control предоставляет защищенное свойство по имени ViewState. "За кулисами" это свойство обеспечивает доступ к типу System. Web. UI. StateBag, представляющему все данные, которые хранятся в поле VIEWSTATE. Используя индексатор типа StateBag, можно встраивать специальную информацию в скрытое поле формы VIEWSTATE, применяя пары "имя/ значение". Ниже показан простой пример: protected void btnAddToVS_Click(object sender, EventArgs e) { ViewState["CustomViewStateltem"] = "Some user data"; lblVSValue.Text = (string)ViewState["CustomViewStateltem"]; } Поскольку тип System. Web.UI. StateBag спроектирован для операций с типами System.Object, когда вы обращаетесь к значению по заданному ключу, то должны явно приводить его к типу данных, лежащему в основе (в рассматриваемом случае — System. String). Однако имейте в виду, что значения, помещенные в поле VIEWSTATE, не могут быть объектом в буквальном смысле. В частности, допустимыми типами являются String, Integer, Boolean, ArrayList, Hashtable и массивы всех этих типов. Поэтому, исходя из того, что страницы * .aspx могут вставлять пользовательские фрагменты информации в строку VIEWSTATE, возникает следующий логичный вопрос: когда это может понадобиться? В большинстве случаев специальные данные состояния представления больше подходят для хранения специфичной для пользователя информации. Например, можно установить данные состояния представления, которые указывают, как пользователь желает видеть интерфейс элемента GridView (например, порядок сортировки). Данные состояния представления не слишком подходят для хранения полной информации пользователя, такой как элементы корзины для покупок или кэшированных DataSet. Когда нужно хранить сложную информацию подобного рода, лучше иметь дело с данными сеанса или данными приложения. Но прежде чем перейти к ним, следует прояснить роль файла Global.asax. Исходный код. Веб-сайт ViewstateApp доступен в подкаталоге Chapter 34. Роль файла Global. asax К этому моменту приложение ASP.NET может показаться чем-то таким, что лишь ненамного больше набора файлов * .aspx и их веб-элементов управления. Хотя можно строить веб-приложения, просто связывая вместе веб-страницы, скорее всего, рано или поздно потребуется способ взаимодействия с веб-приложением как с единым целым. В этом случае в веб-приложение ASP.NET можно включить дополнительный файл
1300 Часть VII. Построение веб-приложений с использованием ASP.NET Global. asax через пункт меню Web Site^Add New Item (Веб-сайт1^Добавить новый элемент), как показано на рис. 34.2 (обратите внимание, что нужно выбрать значок Global Application Class (Глобальный класс приложения)). ЁШ AJAX Web Form Visual C# Л IcIU. WCF Service Vrsual C* ^ j Global Application Class Vrsual C« A*l StvleSheet Vrsual C# jjjj Text File Vrsual C# Generic Hanti\er VKiutl Cf Per user extensions are currently not allowed to load, trwmig loading of pi г цдег «tens.ons Name Global.asax Placer ad i- » ■ ■ ■ ! ■ Add Cancel J Рис. 34.2. Добавление файла Global. asax Просто говоря, Global .asax — это почти то же самое, что запускаемый двойным щелчком файл * . ехе, который можно получить в мире ASP.NET, в том смысле, что этот тип представляет поведение времени выполнения самого веб-сайта. После добавления файла Global. asax в веб-проект вы сразу заметите, что это всего лишь блок <script>, содержащий обработчики событий: <%@ 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>
Глава 34. Управление состоянием в ASP.NET 1301 Тем не менее, внешность может быть обманчивой. Во время выполнения код внутри блока <script> собирается в класс, унаследованный от System.Web.HttpApplication (при наличии опыта работы с ASP.NET 1.x, можно вспомнить, что файл отделенного кода Global .asax буквально определял класс-наследник HttpApplication). Как уже упоминалось, члены, определенные внутри Global. asax, представляют собой обработчики событий, которые позволяют взаимодействовать с событиями уровня приложения (и уровня сеанса). В табл. 34.1 описаны роли всех обработчиков событий. Таблица 34.1. Основные обработчики событий пространства имен System.Web Обработчик событий Назначение Application_start () Этот обработчик событий вызывается в самом начале при запуске приложения. Поэтому данное событие инициируется только один раз за все время жизни веб-приложения. Это идеальное место для определения данных уровня приложения, используемых во всем веб-приложении Application_End () Этот обработчик событий вызывается при останове приложения. Это происходит, когда последний пользователь покидает приложение по тайм-ауту, или когда вы вручную останавливаете приложение в I IS Session_Start () Этот обработчик событий вызывается, когда новый пользователь входит в приложение. Здесь можно устанавливать все специфические для пользователя данные, которые нужно предохранить между обратными отправками Session_End () Этот обработчик событий вызывается по завершению пользовательского сеанса (обычно по истечении предопределенного тайм-аута) Application_Error () Это глобальный обработчик ошибок, который вызывается, когда веб- приложение генерирует необработанное исключение Глобальный обработчик исключений "последнего шанса" Сначала рассмотрим роль обработчика событий ApplicationError (). Вспомните, что определенная страница может обрабатывать событие Error, в результате перехватывая любое необработанное исключение, которое случается в контексте самой этой страницы. Аналогично обработчик событий ApplicationError () — это последнее место, где можно обработать исключение, которое не было обработано страницей. Как и в случае события уровня страницы Error, обращаться к определенному исключению System.Exception можно через унаследованное свойство Server: void Application_Error(object sender, EventArgs e) { // Получить необработанную ошибку. Exception ex = Server.GetLastError (); // Обработать ошибку... //По завершении очистить ошибку. Server.ClearError() ; } Поскольку обработчик событий ApplicationError () является обработчиком исключений "последнего шанса" для веб-приложения, довольно часто этот метод реализуется таким образом, что пользователь переносится на предопределенную страницу ошибки сервера. Другие распространенные действия могут включать отправку сообщения электронной почты веб-администратору или запись во внешний журнал ошибок.
1302 Часть VII. Построение веб-приложений с использованием ASP.NET Базовый класс HttpApplication Как упоминалось ранее, сценарий Global. as ax динамически генерируется как класс, производный от базового класса System.Web.HttpApplication, который предлагает некоторого рода функциональность, подобную функциональности класса System.Web. UI. Page (без видимого пользовательского интерфейса). В табл. 34.2 документированы ключевые свойства этого класса. Таблица 34.2. Ключевые свойства класса System.Web.HttpApplication Свойство Назначение Application Это свойство позволяет взаимодействовать с данными уровня приложения, используя тип HttpApplicationState Request Это свойство позволяет взаимодействовать с входящим запросом HTTP, используя лежащий в основе объект HttpRequest Response Это свойство позволяет взаимодействовать с исходящим ответом HTTP, используя лежащий в основе объект HttpResponse Server Это свойство получает встроенный серверный объект для текущего запроса, используя лежащий в основе объект HttpServerUtility Session Это свойство позволяет взаимодействовать с данными уровня сеанса, используя лежащий в основе объект HttpSessionState Поскольку файл Global. asax явно не документирует HttpApplication в качестве лежащего в основе базового класса, важно помнить, что здесь применимы все правила отношения "является" ("is-a"). Например, если вы примените операцию точки к ключевому слову base внутри любого члена Global.asax, то немедленно получите доступ ко всем членам цепочки наследования, как показано на рис. 34.3. Gbbal.asax* X Server Objects & Events (No Events) <%@ Application Language="C#" X> £3<script runat="server"> void Application_Start(object sender, EventArgs e) // Code that runs on application startup base. | £ jAcquireRequestState J "•♦ AddOnAcquireRequestStateAsync | ^ AddOnAuthenticateRequestAsync // C- ^ AddOnAuthorizeRequestAsync | ;# AddOnBeginRequestAsync I ♦ AddOnEndRequestAsync I ^ AddOnLogRequestAsync ^ AddOnMapRequestHandierAsync // Col ^ AddOnPostAcquireRequestStateAsync ; ^ AddOnPostAuthenticateRequestAsync | ■♦ AddOnPostAuthorizeRequestAsync | ^ AddOnPostLogRequestAsync d Sessi ■■■♦ AddOnPostMapRequestHandferAsync /У Code that runs when a new session is started void Applj loo %"T] , Рис. 34.3. Помните, что HttpApplication является родительским для типа, скрывающегося внутри Global .asax
Глава 34. Управление состоянием в ASP.NET 1303 Различие между свойствами Application и Session В ASP.NET состояние приложения поддерживается экземпляром типа HttpAppli cationState. Этот класс позволяет разделять глобальную информацию между всеми пользователями (и всеми страницами), работающими с приложением ASP.NET. С его помощью можно не только разделять все данные приложения между всеми пользователями сайта; в случае изменения этих данных уровня приложения, они также становятся немедленно видимыми всем пользователям после следующей обратной отправки. С другой стороны, состояние сеанса служит для запоминания информации для определенного пользователя (такой как содержимое корзины покупок). Физически состояние сеанса пользователя представлено типом класса HttpSessionState. Когда новый пользователь входит в веб-приложение ASP.NET, исполняющая среда автоматически назначает ему новый идентификатор сеанса, который устаревает по умолчанию после 20 минут отсутствия активности. Таким образом, если к сайту подключено 20 000 пользователей, существует 20 000 отдельных объектов HttpSessionState, каждому из которых автоматически назначен уникальный идентификатор сеанса. Отношение между веб-приложением и веб-сеансом показано на рис. 34.4. Имея опыт работы в классическом ASP, можно вспомнить, что данные состояния приложения и данные состояния сеанса были представлены отдельными СОМ- объектами (например, Application и Session). В ASP.NET типы-наследники Page, а также тип HttpApplication, используют идентично именованные свойства (т.е. Application и Session), которые представляют лежащие в основе объекты типов HttpApplicationState и HttpSessionState. Веб-приложение (HttpApplication) (HttpApplicationState: Глобальная информация, разделяемая всеми сеансами j ( Сеанс А Л I(HttpSessionState) ) i к Клиент А ( Сеанс л Л I (HttpSessionState) J f Сеанс В Л I (HttpSessionState) J i i Клиент В t i Клиент л Рис. 34.4. Отличие состояния приложения от состояния сеанса Поддержка данных состояния уровня приложения Тип HttpApplicationState позволяет разработчикам разделять глобальную информацию между множеством пользователей в приложении ASP.NET. В табл. 34.3 описаны некоторые основные члены этого типа.
1304 Часть VII. Построение веб-приложений с использованием ASP.NET Таблица 34.3. Члены типа HttpAppiicationState Член Назначение Add () Этот метод позволяет добавлять новую пару "имя/значение" к объекту HttpAppiicationState. Обратите внимание, что этот метод обычно не используется, а вместо него применяется индексатор класса HttpAppiicationState AllKeys Это свойство возвращает массив объектов string, которые представляют все имена в объекте HttpAppiicationState Clear () Этот метод очищает все элементы в объекте HttpAppiicationState. Его функциональность эквивалентна методу RemoveAll () Count Это свойство получает количество объектов-элементов, хранимых в объекте HttpAppiicationState Lock (), Unlock () Эти два метода используются, когда необходимо изменить набор переменных приложения в безопасной к потокам манере RemoveAll (), Эти методы удаляют определенный элемент (по строковому имени) из Remove (), объекта HttpAppiicationState. Метод RemoveAt () удаляет эле- RemoveAt () мент по числовому индексу Чтобы проиллюстрировать работу с состоянием приложения, создайте проект Empty Web Site по имени AppState (и добавьте к нему новую веб-форму). Затем вставьте в него новый файл Global.asax. После создания членов данных, которые могут быть разделены между всеми активными сеансами, нужно будет установить пары "имя/значение". В большинстве случаев самым естественным местом для этого является обработчик события Application_Start () в файле Global. asax. cs, например: void Application_Start(Object sender, EventArgs e) { // Установить некоторые переменные приложения. Application["SalesPersonOfTheMonth"] = "Списку"; Application["CurrentCarOnSale"] = "Colt"; Application["MostPopularColorOnLot"] = "Black"; } На протяжении жизненного цикла веб-приложения (те. пока приложение не будет остановлено вручную либо не истечет тайм-аут последнего пользователя), любой пользователь на любой странице при необходимости может обращаться к этим значениям. Предположим, что есть страница, которая отображает текущую скидку на автомобиль в элементе Label через обработчик события Click кнопки: protected void btnShowCarOnSale_Click(object sender, EventArgs arg) { lblCurrCarOnSale.Text = string.Format("Sale on {0}'s today1", (string)Application["CurrentCarOnSale"]); } Обратите внимание, как подобно свойству ViewState необходимо выполнять приведение значения, возвращенного объектом HttpAppiicationState, к корректному типу, лежащему в основе — поскольку свойство Application оперирует только общими типами System.Object. Теперь, учитывая, что свойство Application может хранить любой тип, становится возможным помещать в него объекты пользовательских типов (или любые объекты .NET) и хранить их в состоянии приложения. Предположим, что нужно поддерживать три переменных приложения внутри строго типизированного класса CarLotlnfo:
Глава 34. Управление состоянием в ASP.NET 1305 public class CarLotlnfo { public CarLotlnfo(string s, string c, string m) { salesPersonOfTheMonth = s; currentCarOnSale = c; mostPopularColorOnLot = m; } // public — для легкого доступа, но можно было бы // применить синтаксис автоматических свойств. public string salesPersonOfTheMonth; public string currentCarOnSale; public string mostPopularColorOnLot; } Имея этот вспомогательный класс, можно модифицировать обработчик события Application_Start () следующим образом: void Application_Start(Object sender, EventArgs e) { // Поместить специальный объект в раздел данных приложения. Application["CarSiteInfo"] = new CarLotlnfo("Списку", "Colt", "Black"); } и затем обращаться к информации, используя общедоступные поля данных внутри обработчика события Click серверной стороны для элемента управления Button по имени btnShowAppVariables: protected void btnShowAppVariables_Click (object sender, EventArgs e) { CarLotlnfo appVars = ((CarLotlnfo)Application["CarSitelnfo"]); string appState = string.Format("<li>Car on sale: {0}</li>", appVars.currentCarOnSale); appState += string.Format("<li>Most popular color: {0}</li>", appVars.mostPopularColorOnLot); appState += string.Format("<li>Big shot Salesperson: {0}</li>", appVars.salesPersonOfTheMonth); lblAppVariables.Text = appState; } Учитывая, что теперь данные о продаже машины представлены специальным типом класса, обработчик btnShowCarOnSale события Click должен быть изменен, как показано ниже: protected void btnShowCarOnSale_Clickl(object sender, EventArgs e) { lblCurrCarOnSale.Text = String.Format("Sale on {0}'s today!", ( (CarLotlnfo)Application["CarSitelnfo"]) .currentCarOnSale); } Модификация данных приложения Можно программно обновлять или удалять любые либо все элементы данных уровня приложения с использованием членов типа HttpApplicationState во время выполнения веб-приложения. Например, чтобы удалить определенный элемент данных, необходимо вызвать метод Remove ().
1306 Часть VII. Построение веб-приложений с использованием ASP.NET Чтобы удалить все данные уровня приложения, следует вызвать RemoveAll (). private void CleanAppData () { // Удалить один элемент по строковому имени. Application.Remove("SomeltemlDontNeed"); // Уничтожить все данные приложения! Application.RemoveAll(); } Если необходимо изменить значение существующего элемента данных уровня приложения, достаточно только выполнить новое присваивание соответствующему элементу данных. Предположим, что страница теперь имеет элемент Button, который позволяет пользователю изменять текущего рекордсмена продаж, прочитав его имя из элемента TextBox по имени txtNewSP. Обработчик события Click выглядит, как и следовало ожидать: protected void btnSetNewSP_Click(object sender, EventArgs e) { // Установить нового продавца. ((CarLotlnfo)Application["CarSitelnfо"]).salesPersonOfTheMonth = txtNewSP.Text; & http://loca ► Q- f>* Fun with Application State j Show App Variable» | • Car on sale: Cok • Most popular color. Black • Big shot SalesPeison. Md Запустив веб-приложение, после щелчка на новой кнопке вы увидите, что элемент данных уровня приложения изменился. Более того, поскольку переменные приложения доступны всем пользователям на любой странице веб-приложения, то после запуска трех или четырех экземпляров веб-браузера можно заметить, что при изменении в одном экземпляре текущего рекордсмена продаж все остальные отобразят его после обратной отправки. На рис. 34.5 показан возможный вывод. Имейте в виду, что если вы столкнетесь с ситуацией, в которой набор переменных уровня приложения должен изменяться как единое целое, то рискуете повредить данные (поскольку технически возможно, что данные уровня приложения будут изменяться как раз в тот момент, когда другой пользователь пытается их прочитать). Хотя можно было бы предпринять длительное путешествие и реализовать вручную логику блокировок с использованием потоковых примитивов из пространства имен System. Threading, тип HttpApplicatiobState уже имеет два метода — Lock() и Unlock (), — которые автоматически обеспечивают безопасность потоков: // Безопасный доступ к взаимосвязанным данным приложения. Application.Lock (); Application["SalesPersonOfTheMonth"] = "Maxine"; Application["CurrentBonusedEmployee"] = Application["SalesPersonOfTheMonth" ] ; Application.Unlock (); Рис. 34.5. Отображение данных приложения Обработка останова веб-приложения Тип HttpApplicationState спроектирован для поддержки значений элементов, которые он хранит до тех пор, пока не наступит одна из двух ситуаций: последний поль-
Глава 34. Управление состоянием в ASP.NET 1307 зователь сайта выйдет по тайм-ауту (или явно) либо же кто-то остановит веб-сайт через IIS. В любом случае будет автоматически вызван метод ApplicationEnd () унаследованного от HttpApplication типа. Внутри этого обработчика событий можно предусмотреть весь необходимый код очистки: void Application_End(Object sender, EventArgs e) { // Записать текущие переменные в базу // данных или куда-нибудь еще- • • } Исходный код. Веб-сайт AppState доступен в подкаталоге Chapter 34. Работа с кэшем приложения В ASP.NET предлагается второй, более гибкий способ для обработки данных уровня приложения. Как вы помните, значения внутри объекта HttpApplicationState остаются в памяти до тех пор, пока веб-приложение работает и используется. Однако иногда может понадобиться хранить некоторую часть данных приложения только на протяжении определенного периода времени. Например, может потребоваться получить объект ADO.NET Data Set, который действителен только в течение пяти минут. По истечении этого времени нужно будет получить свежий DataSet со всеми произошедшими изменениями данных. Хотя технически возможно построить такую инфраструктуру средствами HttpApplicationState и некоторого рода ручного мониторинга, решение этой задачи значительно упрощается с использованием кэша приложения ASP.NET. Как можно предположить из названия, объект ASP.NET System. Web. Caching. Cache (доступный через свойство Context .Cache) позволяет определить объекты, доступные всем пользователям из всех страниц в течение фиксированного периода времени. В простейшей форме взаимодействие с кэшем выглядит точно так же, как взаимодействие с типом HttpApplicationState: // Добавить элемент в кэш. // Этот элемент *не* устареет. Context.Cache["SomeStringltem"] = "This is the string item"; // Получить элемент из кэша. string s = (string)Context.Cache["SomeStringltem"] На заметку! Чтобы обратиться к кэшу из Global. asax, следует использовать свойство Context. Однако в контексте типа-наследника System.Web.UI. Page объект Cache можно использовать напрямую через свойство Cache страницы. В классе System.Web.Caching.Cache определено лишь небольшое количество членов помимо индексатора типа. Метод Add () можно использовать для вставки в кэш нового элемента, который еще не определен (если указанный элемент уже существует, метод Add () ничего не делает). Метод Insert () также помещает элемент в кэш. Если, однако, данный элемент определен, Insert () заменяет текущий элемент новым. Учитывая, что именно такое поведение чаще всего и требуется, сосредоточим внимание исключительно на методе Insert (). Работа с кэшированием данных Давайте рассмотрим пример. Создайте новый проект Empty Web Site по имени CacheState и добавьте в него веб-форму и файл Global. asax. Подобно элементу данных уровня приложения, поддерживаемому типом HttpApplicationState, кэш может
1308 Часть VII. Построение веб-приложений с использованием ASP.NET хранить любой унаследованный от System. Object тип и часто заполняется внутри обработчика событий Applications tart (). Целью текущего примера будет автоматическое обновление содержимого Data Set каждые 15 секунд. Интересующий нас Data Set будет содержать текущий набор записей из таблицы Inventory базы данных AutoLot, созданной во время обсуждения ADO.NET. С учетом сказанного выше, установите ссылку на AutoLotDAL.dll (см. главу 21) и обновите файл Global. asax следующим образом: <7.@ Application Language="C#" qu> <и@ Import Namespace = "AutoLotConnectedLayer" u> <"j@ Import Namespace = "System. Data" "> <script runat="server"> // Определить статическую переменную-член Cache. static Cache theCache; void Application_Start(Object sender, EventArgs e) { // Сначала присвоить значение статической переменной theCache. theCache = Context.Cache; // При запуске приложения прочитать текущие записи //из таблицы Inventory базы данных AutoLot. InventoryDAL dal = new InventoryDAL(); dal.OpenConnection (@"Data Source=(local)\SQLEXPRESS;" + "Initial Catalog=AutoLot;Integrated Security=True"); DataTable theCars = dal.GetAllInventory(); dal.CloseConnection ()/ // Сохранить DataTable в кэше. theCache.Insert("AppDataTable", theCars, null, DateTime.Now.AddSecondsA5), Cache.NoSlidingExpiration, CacheltemPriority.Default, new CacheltemRemovedCallback(UpdateCarlnventory)); } // Цель делегата CacheltemRemovedCallback. static void UpdateCarlnventory(string key, object item, CacheltemRemovedReason reason) { InventoryDAL dal = new InventoryDAL(); dal.OpenConnection(@"Data Source= (local)\SQLEXPRESS;" "Initial Catalog=AutoLot;Integrated Security=True")j DataTable theCars = dal.GetAllInventory(); dal.CloseConnection (); // Теперь сохранить в кэше. theCache.Insert("AppDataTable", theCars, null, DateTime.Now.AddSecondsA5), Cache.NoSlidingExpiration, CacheltemPriority.Default, new CacheltemRemovedCallback(UpdateCarlnventory)); </script> Для начала обратите внимание на определение статической переменной-члена Cache. Причина в том, что были определены два статических члена, которым нужен доступ к объекту Cache. Вспомните, что статические методы не имеют доступа к унаследованным членам, а потому использовать свойство Context нельзя!
Глава 34. Управление состоянием в ASP.NET 1309 Внутри обработчика событий ApplicationStart () осуществляется наполнение объекта DataTable и помещение его в кэш приложения. Как и можно было предположить, метод Context. Cache . Insert () имеет несколько перегрузок. В данном случае указывается значение для каждого параметра. Взгляните на следующий прокомментированный вызов Insert (): theCache. Insert ( "AppDataTable" , // Имя для идентификации элемента в кэше. theCars, // Объект, помещаемый в кэш. null, // Есть ли зависимости у этого объекта? DateTime.Now.AddSecondsA5), // Абсолютное время устаревания. Cache.NoSlidingExpiration, // He использовать скользящее устаревание. CacheltemPriority.Default, // Уровень приоритета элемента кэша. // Делегат для события CacheItemRemove. new CacheltemRemovedCallback(UpdateCarlnventory)); Первые два параметра просто создают пару "имя/значение" для элемента. Третий параметр позволяет определить объект CacheDependency (который в данном случае равен null, поскольку DataTable ни от чего не зависит). Параметр DateTime .Now. AddSeconds A5) задает абсолютное время устаревания. Это значит, что элемент будет неизбежно удален из кэша через 15 секунд. Абсолютное время устаревания удобно для элементов данных, которые постоянно должны обновляться (таких как биржевые показатели). Параметр Cache .NoSlidingExpiration указывает на то, что элемент кэша не использует скользящее устаревания (sliding expiration). Скользящее устаревание — это способ сохранения элемента в кэше в течение, как минимум, указанного времени. Например, если установить значение скользящего устаревания в 60 секунд для элемента кэша, он будет находиться там, как минимум, одну минуту. Если любая веб-страница обратится к элементу кэша в течение этого времени, таймер сбрасывается, и элемент кэша существует еще 60 секунд. Если в течение этого времени никакого обращения к нему не последует, элемент из кэша удаляется. Скользящее устаревание удобно для данных, которые может быть дорого (долго) генерировать, но которые не слишком часто используются веб-страницами. Обратите внимание, что для определенного элемента кэша указывать одновременно абсолютное и скользящее устаревание не допускается. Должно устанавливаться либо абсолютное (с помощью Cache .NoSlidingExpiration), либо скользящее (посредством Cache . NoAbsoluteExpiration) устаревание. Наконец, как видно из сигнатуры метода UpdateCarlnventory () , делегат CacheltemRemovedCallback может вызывать только методы, соответствующие следующей сигнатуре: static void UpdateCarlnventory(string key, object item, CacheltemRemovedReason reason) { } Итак, когда приложение стартует, DataTable заполняется и кэшируется. Каждые 15 секунд DataTable очищается, обновляется и заново вставляется в кэш. Чтобы увидеть эффект от этого, необходимо создать страницу, которая позволит некоторое взаимодействие с пользователем. Модификация файла *. aspx Обновите пользовательский интерфейс начального файла * . aspx, как показано на рис. 34.6.
1310 Часть VII. Построение веб-приложений с использованием ASP.NET Default-aop» X ЕЕ£ЕШШ22ШШ The Add New Car Page СагшГ МакеР СокяГ Pet Name» Add This Car Current Inventory [ asp:GridView#carsGridV»v| I abc abc abc '■ abc : abc abc abc abc abc abc abc abc «Design ! n Split j Й Source j Ujj<bod>'>Jj<form»forml>- |<;div>J <asp:GridView#carsGrkfView> Ы Рис. 34.6. Графический интерфейс приложения, использующего кэш В обработчике события Load страницы сконфигурируйте элемент управления GridView для отображения текущего содержимого кэшированного объекта DataTable при первом получении страницы пользователем (не забудьте импортировать в файле кода пространства имен System. Data и AutoLotConnectedLayer): protected void Page_Load(object sender, EventArgs e) { if ( ! IsPostBack) { carsGridView.DataSource = (DataTable)Cache["AppDataTable"]; carsGridView.DataBind(); } } В обработчике события Click кнопки Add This Car (Добавить этот автомобиль) вставьте новую запись в базу данных AutoLot, используя тип InventoryDAL. Как только запись будет вставлена, вызовите вспомогательную функцию по имени Ref reshGrid (), которая обновит пользовательский интерфейс: protected void btnAddCar_Click(object sender, EventArgs e) { // Обновить таблицу Inventory и вызвать RefreshGrid(). InventoryDAL dal = new InventoryDAL(); dal.OpenConnection(@"Data Source=(local)\SQLEXPRESS;" + "Initial Catalog=AutoLot;Integrated Security=True"); dal.InsertAuto(int.Parse(txtCarID.Text), txtCarColor.Text, txtCarMake.Text, txtCarPetName.Text); dal.CloseConnection (); RefreshGrid();
Глава 34. Управление состоянием в ASP.NET 1311 private void RefreshGrid () { InventoryDAL dal = new InventoryDAL(); dal.OpenConnection(@"Data Source=(local)\SQLEXPRESS;" + "Initial Catalog=AutoLot;Integrated Security=True"); DataTable theCars = dal.GetAllInventory(); dal.CloseConnection(); carsGridView.DataSource = theCars; carsGridView.DataBind(); } Теперь, чтобы проверить использование кэша, начните с запуска текущей программы (нажав <Ctrl+F5>) и скопируйте URL, появившийся в браузере, в системный буфер обмена. Затем запустите второй экземпляр браузера и вставьте скопированный URL в его строку адреса. После этого на экране должно быть два экземпляра браузера, оба с загруженной страницей Default.aspx, отображающей идентичные данные. В одном экземпляре браузера добавьте новую запись об автомобиле. Очевидно, что в результате получится обновленный GridView, видимый в браузере, который инициировал обратную отправку. Во втором экземпляре браузера щелкните на кнопке обновления (или нажмите <F5>). Вы не сразу увидите новый элемент, учитывая, что обработчик события PageLoad читает непосредственно из кэша. (Если вы его видите, значит, истекло 15 секунд. Либо действуйте быстрее, либо увеличьте период времени, в течение которого DataTable остается в кэше.) Подождите несколько секунд и еще раз щелкните на кнопке обновления во втором экземпляре браузера. Теперь вы должны увидеть новый элемент, учитывая, что DataTable в кэше устарел, и целевой метод делегата CacheltemRemoveCallback автоматически обновил кэшированный объект DataTable. Как видите, главное преимущество типа Cache заключается в том, что появляется шанс отреагировать, когда элемент удаляется из кэша. В рассмотренном примере определенно можно было бы обойтись без использования Cache, а просто заставить обработчик событий PageLoad () всегда читать непосредственно из базы данных AutoLot (правда, работа страницы в этом случае бы замедлилась). Тем не менее, идея должна быть ясна: кэш всегда позволяет автоматически обновлять данные, используя механизм кэширования. Исходный код. Веб-сайт CacheState доступен в подкаталоге Chapter 34. Поддержка данных сеанса На этом рассмотрение данных уровня приложения и кэшированных данных завершено. Далее давайте ознакомимся с ролью данных, специфичных для пользователя. Как упоминалось ранее, сеанс (session) — это не более, чем взаимодействие конкретного пользователя с веб-приложением, представленное уникальным объектом HttpSessionState. Чтобы поддерживать информацию о состоянии для конкретного пользователя, можно применять свойство Session в классе веб-страницы или в файле Global. a sax. Классический пример необходимости поддерживать данные пользователя — корзина покупок. Если 10 человек зашли в онлайновый электронный магазин, каждый из них будет иметь уникальный набор наименований товаров, который он намерен приобрести, и эти данные должны поддерживаться. Когда новый пользователь входит в веб-приложение, исполняющая среда .NET автоматически присваивает ему уникальный идентификатор сеанса, служащий для идентификации этого конкретного пользователя. Каждый идентификатор сеанса получает
1312 Часть VII. Построение веб-приложений с использованием ASP.NET свой экземпляр типа HttpSessionState для хранения специфичных для пользователя данных. Вставка или извлечение данных сеанса синтаксически идентична манипуляциям с данными приложения, например: // Добавить/извлечь данные сеанса для текущего пользователя. Session["DesiredCarColor"] = "Green"; string color = (string) Session["DesiredCarColor"]; В Global. as ax можно перехватывать момент начала и конца сеанса через обработчики событий Session_Start() и Session_End() . Внутри Session_Start() можно свободно создавать любые элементы данных, специфичные для пользователя, в то время как SessionEnd () позволяет выполнять любую работу, которая может понадобиться при закрытии сеанса пользователя: <".';@ Application Language="C#" 'h> void Session_Start(Object sender, EventArgs e) { // Новый сеанс! Провести подготовку, если требуется. } void Session_End(Object sender, EventArgs e) { // Пользователь вышел или отключен по тайм-ауту. // При необходимости выполнить очистку. } Подобно состоянию приложения, состояние сеанса может хранить объекты любых типов-наследников System.Object, включая специальные классы. Например, предположим, что создан новый проект Empty Web Site (по имени SessionState), в котором определен класс по имени UserShoppingCart: public class UserShoppingCart { public string desiredCar; public string desiredCarColor; public float downPayment; public bool isLeasing; public DateTime dateOfPickUp; public override string ToStringO { return string.Format ("Car: {0}<br>Color: {l}<br>$ Down: {2}<br>Lease: {3}<br>Pick-up Date: {4}", desiredCar, desiredCarColor, downPayment, isLeasing, dateOfPickUp.ToShortDateStringO ) ; } } Вставьте файл Global, asax. Внутри обработчика SessionStart () теперь можно присвоить каждому пользователю новый экземпляр класса UserShoppingCart: void Session_Start(Object sender, EventArgs e) { Session["UserShoppingCartInfo"] = new UserShoppingCart (); } Во время посещения пользователем веб-страниц можно брать экземпляр UserShoppingCart и заполнять его поля специфичными для пользователя данными. Например, предположим, что имеется простая страница * .aspx, определяющая набор элементов управления для ввода, соответствующих полям типа UserShoppingCart,
Глава 34. Управление состоянием в ASP.NET 1313 элемент Button, используемый для установки значений, и два элемента Label, которые будут отображать идентификатор сеанса пользователя и информацию о сеансе (рис. 34.7). DefauttKpx X | Ibofrl Fun with Session State i \\Ткпсо»ог?Г~ WhkhMake?T Down Payment? Г Lease? Delivery Date: 7 14 21 28 «j », e 15 22 1 Э I, 2 9 16 23 2 9 We 3 10 17 24 : rh 4 11 13 25 4 11 !^ 5 12 19 25 s -: [IblUserlD] | [folUserlnfo] 3 Design ] П Split | 3 Source [<][<html>| <body> E Рис. 34.7. Графический интерфейс приложения, использующего информацию сеанса Обработчик события Click серверной стороны для элемента управления Button достаточно прост (он извлекает значения из элементов TextBox и отображает данные корзины покупок в элементе управления Label): protected void btnSubmit_Click(object sender, EventArgs e) { // Установить предпочтения текущего пользователя. UserShoppingCart cart = (UserShoppingCart)Session["UserShoppingCartlnfo"]; cart.dateOfPickUp = myCalendar.SelectedDate; cart.desiredCar = txtCarMake.Text; cart.desiredCarColor = txtCarColor.Text; cart.downPayment = float.Parse(txtDownPayment.Text); cart.isLeasing = chklsLeasing.Checked; lblUserlnfo.Text = cart.ToString(); Session["UserShoppingCartlnfo"] = cart; Внутри SessionEnd () можно сохранить поля UserShoppingCart и все, что еще нужно, в базе данных (как будет показано в конце этой главы, API-интерфейс ASP.NET Profile делает это автоматически). К тому же, можно реализовать SessionError () для перехвата любого ошибочного ввода (либо применить различные элементы управления проверкой достоверности на странице Default, aspx для обработки ошибок пользователя).
1314 Часть VII. Построение веб-приложений с использованием ASP.NET В любом случае, запустив два или три экземпляра браузера с одним и тем же URL, обнаружится, что каждый пользователь может наполнять собственную корзину покупок, которая отображается на его уникальный экземпляр HttpSessionState. Дополнительные члены HttpSessionState Помимо индексатора типа, в классе HttpSessionState определен ряд других интересных членов. Свойство SessionID возвращает уникальный идентификатор пользователя. Если хотите увидеть автоматически присвоенный идентификатор сеанса в данном примере, обработайте событие Load страницы следующим образом: protected void Page_Load(object sender, EventArgs e) { lblUserlD.Text = string.Format("Here is your ID: {0}", Session.SessionID); } Методы Remove () и RemoveAll () могут использоваться для очистки элементов пользовательского экземпляра HttpSessionState: Session.Remove("SomeltemWeDontNeedAnymore"); В классе HttpSessionState также определен набор членов, которые управляют политикой устаревания текущего сеанса. По умолчанию каждый пользователь получает 20 минут отсутствия активности, прежде чем объект HttpSessionState будет уничтожен. Таким образом, если пользователь входит в веб-приложение (и, следовательно, получает уникальный идентификатор сеанса), но не возвращается на сайт в течение 20 минут, то исполняющая среда предполагает, что пользователь больше не заинтересован в сайте и уничтожает его данные сеанса. Период устаревания сеанса можно изменить, установив его для каждого пользователя с помощью свойства Timeout. Наиболее подходящим местом для этого является метод SessionStart (): void Session_Start (Object sender, EventArgs e) { // Каждый пользователь получает 5 минут отсутствия активности. Session.Timeout = 5; Session [ "UserShoppingCartlnfо"] = new UserShoppingCart(); } На заметку! Если настраивать значение Timeout для каждого пользователя не нужно, можете изменить 20-минутное значение по умолчанию для всех пользователей через атрибут Timeout элемента <sessionState> внутри файла Web.config (рассматривается в конце этой главы). Преимущество свойства Timeout состоит в возможности назначать специфический тайм-аут отдельно каждому пользователю. Например, предположим, что создано веб- приложение, которое позволяет пользователям платить дифференцированную плату за различные уровни членства. Вы можете сказать, что "золотые" пользователи должны иметь тайм-аут длительностью в один час, в то время как обычные — только 30 секунд. Такая возможность вызывает вопрос: как запомнить специфическую для пользователя информацию (вроде текущего уровня членства) между визитами на сайт? Один из вариантов — использовать тип HttpCookie. Исходный код. Веб-сайт SessionState доступен в подкаталоге Chapter 34.
Глава 34. Управление состоянием в ASP.NET 1315 Cookie-наборы Следующий прием управления состоянием состоит в хранении постоянных данных внутри cookie-наборов, которые часто реализуются в виде текстового файла (или набора файлов), расположенного на машине пользователя. Когда пользователь входит на определенный сайт, браузер проверяет наличие на пользовательской машине cookie-файла для заданного URL, и если он есть, добавляет его данные к запросу HTTP. Принимающая веб-страница серверной стороны может затем прочитать cookie- данные для создания графического интерфейса, основанного на предпочтениях конкретного пользователя. Наверняка вы замечали, что после посещения одного из любимых веб-сайтов он каким-то образом "помнит", какое содержимое вас интересует. Причина (отчасти) в том, что в cookie-наборах, хранимых на вашем компьютере, содержится информация, касающаяся данного веб-сайта. На заметку! Точное местоположение cookie-файлов зависит от используемого браузера и установленной операционной системы. Содержимое конкретного cookie-файла, очевидно, варьируется среди URL, но имейте в виду, что это обязательно текстовый файл. Поэтому cookie-набор — худший выбор для хранения чувствительной информации о текущем пользователе (такой как номер кредитной карты, пароль и т.п.). Даже если вы предпримите усилия, чтобы зашифровать данные, есть шанс, что настойчивый хакер расшифрует их и воспользуется в преступных целях. Но в любом случае, cookie-наборы играют определенную роль в разработке веб-приложений, поэтому давайте посмотрим, как ASP.NET работает с этой техникой управления состоянием. Создание cookie-наборов Прежде всего, знайте, что cookie-наборы ASP.NET могут быть сконфигурированы как постоянные или временные. Постоянные cookie-наборы обычно известны как классическое определение cookie-данных, устанавливающее пары "имя/значения", которые физически сохраняются на жестком диске пользователя. Временные cookie-наборы (также именуемые сеансовыми cookie-наборами) содержат те же данные, что и постоянные cookie-наборы, но пары "имя/значение" никогда не сохраняются на жестком диске пользователя, а вместо этого существуют только пока открыт браузер. Как только пользователь закрывает браузер, все данные, содержащиеся в сеансовом cookie-наборе, уничтожаются. System.Web.HttpCookie — это класс, представляющий серверную сторону cookie- данных (постоянных или временных). Для создания нового cookie-набора в коде вебстраницы необходимо обратиться к свойству Response.Cookies. Как только новый элемент HttpCookie вставлен во внутреннюю коллекцию, пары "имя/значения" отправляются браузеру внутри заголовка HTTP. Чтобы наглядно увидеть поведение cookie-наборов, создайте новый проект Empty Web Site по имени CookieStateApp и вставьте в него веб-форму с пользовательским интерфейсом, показанным на рис. 34.8. Внутри обработчика события Click кнопки Write This Cookie (Записать этот cookie- набор) постройте новый объект HttpCookie и вставьте его в коллекцию Cookie, представленную свойством HttpRequest. Cookies. Имейте в виду, что данные не будут сохраняться на жестком диске пользователя, если только вы явно не установите срок хранения в свойстве HttpCookie.Expires. Таким образом, следующая реализация создаст временный cookie-набор, который будет уничтожен, когда пользователь закроет браузер:
1316 Часть VII. Построение веб-приложений с использованием ASP.NET protected void btnCookie_Click(object sender, EventArgs e) { // Создать временный cookie-набор. HttpCookie theCookie = new HttpCookie(txtCookieName.Text, txtCookieValue.Text); Response.Cookies.Add(theCookie); } Приведенный ниже код сгенерирует постоянный cookie-набор, который будет действителен в течение трех месяцев, начиная с текущей даты: protected void btnCookie_Click (object sender, EventArgs e) { HttpCookie theCookie = new HttpCookie(txtCookieName.Text, txtCookieValue.Text); theCookie.Expires = DateTime.Now.AddMonthsC); Response.Cookies.Add(theCookie) ; Default.aspx X Q idiv I Fun with Cookies Cookie Name:' Cookie VakieJ Write This Cookie Show Cookie Data [MCookieDataj i Design 3 Spirt .•_>. Source ^^^^^^^^^^^^^" |<j|<htrnl>|[<body>i|<fom>*forml>| <div> ji Г1 В Рис. 34.8. Пользовательский интерфейс CookieStateApp Чтение входящих cookie-данных Вспомните, что браузер полностью отвечает за доступ к постоянным cookie-наборам при навигации на ранее посещенную страницу. Если браузер решает отправить cookie- набор серверу, доступ к входящим cookie-данным в коде страницы * .aspx можно получить через свойство HttpRequest. Cookies. Для целей иллюстрации реализуйте обработчик события Click для кнопки Show Cookie Data (Показать cookie-данные) следующим образом: protected void btnShowCookie_Click(object sender, EventArgs e) { string cookieData = "" ; foreach (string s in Request.Cookies) { cookieData += string. Format ("<lixb>Name</b> : {0}, <b>Value</b> : {1}</Ii>", s, Request.Cookies[s].Value); lblCookieData.Text = cookieData;
Глава 34. Управление состоянием в ASP.NET 1317 Теперь, запустив приложение и щелкнув на кнопке Show Cookie Data, вы увидите, что cookie-данные действительно были оправлены браузером и успешно доступны в коде * . aspx на сервере. Исходный код. Веб-сайт CookieStateApp доступен в подкаталоге Chapter 34. Роль элемента <sessionState> К этому моменту вы уже исследовали различные способы запоминания информации о пользователях. Как было показано, состояние представления и приложения, кэш, данные сеанса и cookie-наборы управляются программно более или менее сходным образом (через индексатор класса). Кроме того, Global, asax имеет методы, которые позволяют перехватывать и реагировать на события, возникающие во время жизни веб- приложения. По умолчанию ASP.NET сохраняет данные сеанса в процессе. Положительной стороной является предельно возможная скорость доступа к информации. Однако отрицательный момент такого решения заключается в том, что если домен приложения (AppDomain) терпит крах (по любой причине), то все пользовательские данные состояния разрушаются. Более того, когда данные состояния хранятся во внутрипроцессной сборке * . dll, нет возможности взаимодействовать с сетевой веб-фермой. Такой режим хранения по умолчанию работает достаточно хорошо, если веб-приложение развернуто на единственном веб-сервере. Однако, как и можно было предположить, подобная модель не идеальна для фермы веб-серверов, учитывая, что состояние сеанса "захватывается" определенным AppDomain. Хранение данных сеанса на сервере состояния сеансов ASP.NET В ASP.NET исполняющую среду можно инструктировать о необходимости развернуть сборку * .dll состояния сеанса в суррогатном процессе, который называется сервером состояния сеансов ASPNET (aspnetstate. ехе). При этом сборку * . dll можно перегрузить из aspnetwp. ехе в отдельный * . ехе, который может находиться на любой из машин веб-фермы. Даже если вы намерены запустить процесс aspnetwp. ехе на той же машине, что и веб-сервер, получается выигрыш от выделения данных состояния в отдельный процесс (поскольку это более надежно). Чтобы использовать сервер состояния сеансов, прежде всего, запустите Windows- службу aspnetstate .ехе на целевой машине посредством ввода следующей команды в окне командной строки Visual Studio 2010 (обратите внимание, что для этого понадобятся права администратора): net start aspnet_state В качестве альтернативы aspnetstate . ехе можно запустить через значок Services (Службы) в группе Administrative Tools (Администрирование) панели управления, как показано на рис. 34.9. Ключевое преимущество такого подхода связано с возможностью конфигурирования aspnet_state.exe в окне Properties на автоматический запуск при загрузке машины. В любом случае, как только сервер состояния сеансов запущен, добавьте следующий элемент <sessionState> в файл Web.config: <system.web> <sessionState mode="StateServer" stateConnectionString="tcpip=12 7.0.0.1:42626"
1318 Часть VII. Построение веб-приложений с использованием ASP.NET sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout=0" /> </system.web> Вот и все! Начиная с этого момента, CLR будет хранить данные о сеансе внутри процесса aspnet_state.exe. Таким образом, если AppDomain, в котором работает веб-приложение, потерпит крах, данные сеанса останутся неповрежденными. Обратите внимание, что элемент <sessionState> может также поддерживать атрибут stateConnectionString. Значение TCP/IP-адреса по умолчанию A27 .0.0.1) указывает на локальную машину. Если же вы хотите, чтобы исполняющая среда .NET использовала службу aspnet_state.exe, расположенную на другой машине (т.е. в случае веб- фермы), можете соответствующим образом изменить это значение. ?1 * ГаБРЛГТ State Service Stop the service Patne the service Restart the service Description; Provides support for out-of-process session states for ASP.NET. If this service is stopped, out-of-process 1 requests will not be processed. If this service is disabled any services that explicitly depend on it will fait to start. \ Extended /^Standard/ Name Щ Application Information k| Application Layer Gateway Service Application Management Щ, Background Intelligent Transfer Serv.. '4, Base Filtering Engine "r Bitlocker Drive Encryption Service ''»■ Block Level Backup Engine Service Bluetooth Support Service щ Bonjour Service Ц 8ranchCache Description Facilitates t... Provides su... Processes in... Transfers fit... The Base Fit... BDESVC hos... TheWBENG... The Bluetoo... Bonjour silo... This sen/ice ... Stat' * 1 Stan 1 Start 1 Start 1 Рис. 34.9. Запуск службы aspnet_state.exe через значок Services Хранение информации о сеансах в выделенной базе данных И, наконец, если требуется максимальная степень изоляции и надежности веб-приложения, можно заставить исполняющую среду хранить все данные о состоянии сеанса внутри Microsoft SQL Server. Необходимое для этого изменение файла Web.config выглядит очень просто: <sessionState mode="SQLServer" stateConnectionString="tcpip=127.0.0.1:42626" sqlConnectionString="data source=127.0.0.l;Trusted_Connection=yes" cookieless="false" timeout=0" /> Однако перед тем как попытаться запустить ассоциированное веб-приложение, следует удостовериться, что целевая машина (указанная в атрибуте sqlCofinectionString) правильно сконфигурирована. Во время установки .NET Framework 4.0 SDK (или Visual Studio 2010) предлагаются два файла InstallSqlState.sql и UninstallSqlState . sql, расположенные по умолчанию в каталоге C:\Windows\ Microsoft. NET\Frameworк\<версия>. На целевой машине потребуется запустить файл
Глава 34. Управление состоянием в ASP.NET 1319 InstallSqlState.sql, используя инструмент типа Microsoft SQL Server Management Studio (который поставляется вместе с Microsoft SQL Server). После выполнения SQL-сценария InstallsqlState, sql вы обнаружите новую базу данных SQL Server (ASPState), которая содержит набор хранимых процедур, вызываемых исполняющей средой ASP.NET, и набор таблиц, используемых для хранения данных сеансов. (Кроме того база tempdb будет обновлена набором таблиц, предназначенных для подкачки.) Как и можно было предположить, конфигурирование веб-приложения для хранения информации сеансов в SQL Server — самый медленный из всех возможных вариантов. Преимуществом является то, что пользовательские данные хранятся надежнее всего (даже если веб-сервер перезагружается). На заметку! Если для хранения данных о сеансах используется состояние сеанса ASP.NET или SQL Server, необходимо удостовериться, что все специальные типы, помещенные в объект HttpSessionState, помечены атрибутом [Serializable]. Интерфейс ASP.NET Profile API К настоящему моменту вы изучили различные приемы, позволяющие запоминать данные уровня пользователя и уровня приложения. Однако все они страдают от одного существенного ограничения: данные существуют до тех пор, пока пользователь подключен к сеансу и веб-приложение работает. Тем не менее, многие веб-сайты требуют хранения пользовательской информации и между сеансами. Например, необходимо предоставить пользователям возможность поддерживать свои учетные записи на сайте. Может быть, нужно хранить экземпляры ShoppingCart между сеансами (для сайта онлайнового магазина). Или, возможно, требуется сохранить базовые пользовательские предпочтения (темы и т.п.). Хотя вполне можно построить специальную базу данных (с несколькими хранимыми процедурами) для хранения такой информации, впоследствии придется создавать специальную библиотеку кода для взаимодействия с этими объектами базы данных. Это не обязательно будет сложной задачей, но минусом такого решения является то, что построение всей этой инфраструктуры возлагается на вас. Чтобы помочь справиться с такими ситуациями, ASP.NET поставляется с готовым API-интерфейсом управления профилями пользователей и соответствующей базой данных — ASP.NET Profile API. В дополнение к обеспечению необходимой инфраструктуры, Profile API также позволяет определять данные, которые должны храниться непосредственно в файле Web . con fig (с целью упрощения); однако можно хранить и любой тип, помеченный [Serializable]. Перед тем как погрузиться в эту тему, давайте посмотрим, где Profile API будет хранить указанные данные. База данных ASPNETDB. mdf Каждый веб-сайт ASP.NET, построенный средствами Visual Studio 2010, может поддерживать папку AppData. По умолчанию Profile API (как и другие службы, наподобие API-интерфейса ролей ASP.NET, который здесь не рассматривается) конфигурируется на использование локальной базы данных SQL Server по имени ASPNETDB.mdf, расположенной в папке AppData. Это поведение по умолчанию задается настройками в файле machine . conf ig для текущей установки .NET. Фактически, когда код использует службу ASP.NET, требующую папки AppData, файл ASPNETDB .mdf автоматически создается на лету, если он до этого не существовал. Если же необходимо, чтобы исполняющая среда ASP.NET взаимодействовала с файлом ASPNETDB .mdf, расположенным на другой машине в сети, или предпочтительнее
1320 Часть VII. Построение веб-приложений с использованием ASP.NET установить эту базу данных на экземпляре SQL Server 7.0 (или выше), потребуется вручную построить ASPNETDB.mdf с использованием утилиты командной строки aspnet_ regsql. ехе. Подобно любой хорошей утилите командной строки, aspnetregsql .exe поддерживает множество опций; однако если запустить ее без аргументов: aspnet_regsql то запустится мастер с графическим интерфейсом, который поможет пройти весь процесс создания и установки базы данных ASPNETDB.mdf на выбранной машине (и версии SQL Server). Теперь предположим, что сайт не использует локальную копию базы данных в папке AppData. Тогда последним шагом должно быть обновление файла Web.config, чтобы он указывал на уникальное местоположение ASPNETDB.mdf. Предположим, что файл ASPNETDB.mdf установлен на машине по имени ProductionServer. Чтобы указать Profile API, где по умолчанию расположены необходимые элементы базы данных, служит следующий фрагмент файла machine, config (для изменения этих настроек по умолчанию можно предусмотреть специальный файл Web .config): <configuration> <connectionStrings> <add name="LocalSqlServices11 connectionString ="Data Source=ProductionServer;Integrated Security=SSPI;Initial Catalog=aspnetdb;" providerName="System.Data.SqlClient"/> </connectionStrings> <system.web> <profile> <providers> <clear/> <add name=llAspNetSqlProfileProviderM connect ionStringName="LocalSqlServer11 applicationName="/" type="System.Web.Profile.SqlProflieProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7fHd50a3a" /> </providers> </profile> </system.web> </configuration> Подобно большинству файлов * . config, он выглядит намного сложнее, чем есть на самом деле. В основном, здесь определяется элемент <connectionString> с необходимыми данными, за которым следует именованный экземпляр SqlProfileProvider (это поставщик по умолчанию, используемый независимо от физического местоположения ASPNETDB.mdf). На заметку! Для простоты будем предполагать, что используется автоматически сгенерированная база данных ASPNETDB.mdf, расположенная в подкаталоге App_Data веб-приложения. Определение пользовательского профиля в Web. config Как уже упоминалось, пользовательский профиль определяется в файле Web .config. Изящество этого подхода состоит в том, что с этим профилем можно взаимодействовать в строго типизированной манере, используя унаследованное свойство Property в файлах кода. Чтобы проиллюстрировать это, создайте новый проект Empty Web Site no имени FunWithProfiles и откройте файл Web .config для редактирования.
Глава 34. Управление состоянием в ASP.NET 1321 Нашей целью будет создание профиля, который моделирует домашние адреса пользователей, находящихся в сеансе, а также общее количество их обращений к этому сайту. Не удивительно, что данные профиля определяются внутри элемента <profile> с использованием множества пар типа "имя/значение". Рассмотрим следующий профиль, созданный в контексте элемента <system.web>: <profile> <properties> <add name="StreetAddress" type="System.String" /> <add name="City" type="System.String" /> <add name="State" type="System.String" /> <add name="TotalPost" type="System.Int32" /> </properties> </profile> Здесь для каждого элемента профиля указано имя и тип данных CLR (разумеется, можно было бы добавить дополнительные элементы для почтового кода, имени и т.д., но идея и без того должна быть понятна). Строго говоря, атрибут типа не обязателен; однако по умолчанию принят System. String. Как и следовало ожидать, существует много других атрибутов, которые могут быть указаны в элементе профиля для дальнейшего уточнения способа хранения этой информации в ASPNETDB.mdf. В табл. 34.4 описаны некоторые основные атрибуты. Таблица 34.4. Избранные атрибуты данных профиля Атрибуты Примеры значений Назначение allowAnonymous true или false defaultValue Строка Name Provider readonly serializeAs type Строка Строка true | false String | XML | Binary Элементарный тип или тип, определяемый пользователем Ограничивает или разрешает анонимный доступ к данному значению. Если установлен в false, анонимные пользователи не имеют доступа к значению этого профиля Значение, которое должно быть возвращено, если свойство еще не было явно установлено Уникальный идентификатор для данного свойства Поставщик, используемый для управления этим значением. Переопределяет установку def aultProvider в Web . conf ig или machine.config Ограничивает или разрешает доступ для записи (значением по умолчанию является false, т.е. не только для чтения) Формат значения при записи в хранилище данных Элементарный тип NET или класс. Имена классов должны быть полностью квалифицированы (например, МуАрр. UserData. ColorPrefs) Мы увидим некоторые из этих атрибутов в действии, когда модифицируем текущий профиль. Пока давайте посмотрим, как программно обращаться к этим данным в коде страниц.
1322 Часть VII. Построение веб-приложений с использованием ASP.NET Программный доступ к данным профиля Вспомните, что общее предназначение интерфейса ASP.NET Profile API заключается в автоматизации процесса записи (и чтения) данных в выделенную базу данных. Чтобы попробовать это на практике, модифицируйте пользовательский интерфейс страницы Default, aspx, добавив набор элементов TextBox(n описательных Label) для ввода адреса пользователя: улицы, города и штата. Кроме того, добавьте элемент типа Button (по имени btnSubmit) и еще один элемент Label (по имени lblUserData), которые будут служить для отображения постоянно хранимых данных (рис. 34.10). Default.aspx xQ Fun with Profiles Street Address! Cky 1 St*e i [MUserData] [ajJeign J a Spfit j S Souite I H|<WmO||<body>||<fonnWorml> ^^-5 .........:...:....- -J <ajp:Button*btnSubmrt> Щ Рис. 34.10. Пользовательский интерфейс страницы Default.aspx веб-сайта FunWithProfiles Внутри обработчика события Click кнопки Submit Data (Отправить данные) воспользуйтесь унаследованным свойством Profile для сохранения каждой единицы данных профиля на основе информации, введенной пользователем в соответствующем поле TextBox. Сохранив каждый элемент данных в ASPNETDB.mdf, прочитайте их из базы данных и сформатируйте в string, отображаемую в элементе lblUserData типа Label. И, наконец, обработайте события страницы Load и отобразите ту же информацию в элементе Label. Таким образом, когда пользователь зайдет на страницу, он увидит текущие настройки. Ниже приведен полный код. public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { GetUserAddress (); } protected void btnSubmit_Click(object sender, EventArgs e) { // Здесь происходит запись в базу данных. Profile.StreetAddress = txtStreetAddress.Text; Profile.City = txtCity.Text; Profile.State = txtState.Text; // Получить установки из базы данных. GetUserAddress(); } private void GetUserAddress() { // Здесь происходит чтение из базы данных. lblUserData.Text = String.Format("You live here: {0}, {1}, {2}", Profile.StreetAddress, Profile.City, Profile.State); } }
Глава 34. Управление состоянием в ASP.NET 1323 Теперь, запустив страницу, вы заметите некоторую задержку при первом запросе Default .aspx. Причина в том, что при этом создается на лету файл ASPNETDB.mdf и помещается в папку AppData. В этом можно убедиться, заглянув в Solution Explorer (рис. 34.11). Solution Explorer ka-ia&iaii НЭ Solution FunWithProfiles' A project) d ф №V~\FunWrthProffes\ D- цз) App.Code 4 jjflp App_Data > (J3 ASPNETDB.MDF! j> Ш Default .aspx i^ Web.conftg Рис. 34.11. Появился файл ASPNETDB.mdf Кроме того, вы обнаружите, что при первом входе на эту страницу элемент Label lblUserData не отображает никаких данных, поскольку они еще не добавлены в соответствующую таблицу базы ASPNETDB.mdf. Как только вы введете значения в элементы управления TextBox и выполните обратную отправку на сервер, названный элемент Label будет заполнен постоянными данными. Теперь перейдем к самому интересному аспекту этой технологии. Если вы закроете браузер и перезапустите веб-сайт, то обнаружите, что ранее введенные данные профиля действительно сохранились, поскольку в Label отображается корректная информация. Это вызывает очевидный вопрос: как они были сохранены? В рассматриваемом примере интерфейс Profile API использует сетевую идентификацию Windows, которая получена из текущих регистрационных данных машины. Однако при построении публичных веб-сайтов (где пользователи не принадлежат к определенному домену) необходимо обеспечить интеграцию Profile API с моделью аутентификации ASP.NET на основе форм, а также поддержку понятия "анонимных профилей", которые позволяют хранить данные профиля для пользователей, не имеющих в данный момент активной идентичности на сайте. На заметку! Темы, связанные с безопасностью ASP.NET (такие как аутентификация с помощью форм и анонимные профили), в этой книге не рассматриваются. Подробности ищите в документации .NET Framework 4.0 SDK. Группирование данных профиля и сохранение специальных объектов В завершение этой главы необходимо дать дополнительные комментарии относительно того, как данные профиля могут быть определены в файле Web. con fig. Текущий профиль просто определяет четыре части данных, которые представлены непосредственно типом профиля. При построении более сложных профилей может быть полезно группировать вместе взаимосвязанные данные под одним уникальным именем. Рассмотрим следующее изменение:
1324 Часть VII. Построение веб-приложений с использованием ASP.NET <profile> <properties> <group name ="Address"> <add name="StreetAddress11 type="String11 /> <add name="City11 type="String11 /> <add name="State" type="String11 /> </group> <add name=,,TotalPost" type="Integer" /> </properties> </profile> На этот раз определена специальная группа по имени Address, включающая название улицы, город и штат пользователя. Чтобы обратиться к этим данным на страницах, потребуется модифицировать код, указывая Prof ile .Address для доступа к каждому подэлементу. Например, ниже приведен обновленный метод GetUserAddress () (обработчик события Click для кнопки Submit Data потребует аналогичных изменений): private void GetUserAddress () { // Здесь происходит чтение из базы данных. lblUserData.Text = String.Format("You live here: {0}, {1}, {2}", Profile.Address.StreetAddress, Profile.Address.City, Proflie.Address.State); } Прежде чем запустить этот пример, потребуется удалить ASPNETDB.mdf из папки AppData, чтобы гарантировать обновление схемы базы данных. После этого пример веб-сайта будет выполняться без ошибок. На заметку! Профиль может содержать столько групп, сколько вы считаете необходимым. Просто определите несколько элементов <group> внутри контекста <properties>. И, наконец, стоит упомянуть, что профиль может также хранить (и извлекать) пользовательские специальные объекты в ASPNETDB.mdf. Чтобы проиллюстрировать это, предположим, что требуется построить пользовательский класс (или структуру), которая будет представлять данные адреса пользователя. Единственное требование, предъявляемое интерфейсом Profile API к такому типу — он должен быть помечен атрибутом [Serializable], например: [Serializable] public class UserAddress { public string Street = string.Empty; public string City = string.Empty; public string State = string.Empty; } При наличии этого класса определение профиля может быть изменено следующим образом (обратите внимание, что специальная группа удалена, хотя это и не обязательно): <profile> <properties> <add name="AddressInfo11 type="UserAddress11 serializeAs ="Binary"/> <add name="TotalPost11 type="Integer11 /> </properties> </profile>
Глава 34. Управление состоянием в ASP.NET 1325 При добавлении к профилю типов [Serializable] атрибут type представляет собой полностью квалифицированное имя типа, который нужно хранить. Как вы увидите в окне IntelliSense в Visual Studio 2010, основными вариантами являются двоичные, XML и строковые данные. Получив информацию адреса в виде пользовательского класса, понадобится обновить базовый код: private void GetUserAddress () { // Здесь происходит чтение из базы данных. lblUserData.Text = String.Format("You live here: {0}, {1}, {2}", Profile.AddressInfo.Street, Proflie.AddressInfo.City, Profile.AddressInfo.State); } Тема, связанная с Profile API, намного шире того, что изложено здесь. Например, свойство Profile на самом деле инкапсулирует тип по имени Prof ileCommon. Используя этот тип, можно программно получать всю информацию о конкретном пользователе, удалять (или добавлять) профили в ASPNETDB .mdf, обновлять аспекты профиля и т.д. Кроме того, интерфейс Profile API имеет множество точек расширения, которые позволяют оптимизировать способ обращения диспетчера профилей к таблицам базы данных ASPNETDB .mdf. Как и можно было ожидать, существует немало способов сократить количество обращений к базе данных. Заинтересованные читатели могут обратиться за подробностями к документации .NET Framework 4.0 SDK. Исходный код. Веб-сайт FunWithProf iles доступен в подкаталоге Chapter 34. Резюме В этой главе вы дополнили знания ASP.NET сведениями об использовании типа HttpApplication. Как было показано, этот тип предлагает набор обработчиков событий по умолчанию, которые позволяют перехватывать различные события уровня приложения и уровня сеанса. Большая часть этой главы была посвящена рассмотрению различных технологий управления состоянием. Вспомните, что состояние представления используется для автоматического заполнения значениями виджетов HTML между обратными отправками определенной страницы. Кроме того, вы узнали о различии между данными уровня приложения и данными уровня сеанса, об управлении cookie- наборами и о кэше приложений ASP. NET. Наконец, вы ознакомились с интерфейсом ASP.NET Profile API. Как было показано, эта технология предлагает готовое решение задачи сохранения пользовательских данных между различными сеансами. Используя конфигурационный файл Web.config веб-сайта, можно определять любое количество элементов профиля (включая группы элементов и типы [Serializable]), которые будут автоматически сохраняться в базе данных ASPNETDB .mdf.
ЧАСТЬ VIII Приложения В этой части... Приложение А. Программирование с помощью Windows Forms Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono
ПРИЛОЖЕНИЕ А Программирование с помощью Windows Forms Со времени появления платформы .NET (примерно в 2001 г.) среди библиотек базовых классов появился API по имени Windows Forms, представленный в основном сборкой System.Windows.Forms.dll. Инструментальный набор Windows Forms предоставляет типы, необходимые для построения графических пользовательских интерфейсов для настольных компьютеров, создания специализированных элементов управления, управления ресурсами (например, строками и значками) и выполнения других задач, возникающих при программировании для пользовательских компьютеров. Имеется и дополнительный API по имени GDI+ (представленный сборкой System.Drawing.dll), который предоставляет дополнительные типы, позволяющие программисту генерировать двухмерную графику, взаимодействовать с сетевыми принтерами и обрабатывать графические данные. Windows Forms (и GDI+) применяются в платформе .NET 4.0 и, видимо, будут существовать еще некоторое время (возможно, длительное) в составе библиотеки базовых классов. Правда, после выхода .NET 3.0 компания Microsoft выпустила совершенно новый инструментальный API под названием Windows Presentation Foundation (WPF). Как было показано в главах 27-31, WPF — довольно мощный инструмент, который позволяет строить передовые пользовательские интерфейсы, и поэтому он является рекомендуемым API для создания современных графических пользовательских интерфейсов. Данное приложение задумано как ознакомительный тур по традиционному Windows Forms API. В частности, потому, что это полезно для понимания исходной модели программирования: сейчас существует много различных приложений Windows Forms, которые еще необходимо сопровождать. А, кроме того, во многих графических пользовательских интерфейсах просто не нужны все возможности, предлагаемые WPF. Если понадобится создать более традиционное бизнес-приложение, которому не нужны все эти примочки и навороты, то часто вполне достаточно Windows Forms API. В этом приложении вы познакомитесь с моделью программирования Windows Forms, поработаете с интегрированными конструкторами Visual Studio 2010, опробуете различные элементы управления Windows Forms и получите общее впечатление о графическом программировании с помощью GDI+. Кроме того, вы соберете все это в единое целое, упаковав в (не совсем совершенное) приложение рисования.
Приложение А. Программирование с помощью Windows Forms 1329 На заметку! Вот одно из доказательств того, что Windows Forms не собирается пропадать в ближайшем будущем: .NET 4.0 поставляется с совершенно новой сборкой Windows Forms System. Windows.Forms.DataVisualization.dll Эта библиотека позволяет добавлять в программы диаграммы с аннотациями, трехмерную графику и поддержку определения координат курсора. NET 4.0 Windows Forms API не описывается в данном приложении, но если вам понадобится информация о нем, просмотрите пространство имен System.Windows.Forms. DataVisualization.Charting. Пространства имен Windows Forms Windows Forms API состоит из сотен типов (классов, интерфейсов, структур, перечислений и делегатов), большинство из которых организованы в различные пространства имен сборки System.Windows.Forms.dll. На рис. АЛ показаны эти пространства имен, выведенные в браузере объектов Visual Studio 2010. Object Browser Browse .NET Framework 4 •<_3 S> stem. .veb.Services t> О System.Resources > {} System,Windows.Forms > {} System.Windcvvs.Fcrms.CompcnentModel.Com2Interop ■ <} System.VVindows.Fcims.Design i> {) System.Windows.Forms-Layout ■ {} System.Windcws.Forms.PropertyGridlnternal > {> System.Windows.Forms.VisualStyles 43 System.Windows.Forms.DataVisualization • .Q System.Windows.Forms.DataVisualization.Design 4J System.WindowsJnput.Manipuiations ■ >Q System,Windpws.Presentation I Assembly System.Windows.Forms Member of .NET Framework 4 C:\Program Files (x86)\Reference Assemblies\Microsofl Framework\.NETFramework\v4.0\System. Windows. Forms.dll Attributes: [System.Reflertion.AssemblyKeyFileAttributef "f:\dd\tools\devdiv Рис. А.1. Пространства имен сборки System.Windows.Forms.dll Несомненно, наиболее важным пространством имен Windows Forms является System.Windows.Forms. Типы из этого пространства имен можно разбить на следующие крупные категории: • Базовая инфраструктура. Это типы, представляющие базовые операции программ, которые используют Windows Forms (Form и Application), и различные типы, предназначенные для взаимодействия с устаревшими элементами ActiveX, a также для взаимодействия с новыми специальными элементами управления WPF • Элементы управления. Эти типы применяются для создания графических пользовательских интерфейсов (наподобие Button, MenuStrip, ProgressBar и DataGridView), все они являются производными от базового класса Control. Элементы управления допускают настройку на этапе проектирования и видимы (по умолчанию) во время выполнения. • Компоненты. Это типы, которые не порождены от базового класса Control, но все-таки могут предоставлять программам Windows Forms визуальные возможности (например, ToolTip и ErrorProvider). Многие компоненты (к примеру, Timer и System.ComponentModel.BackgroundWorker) не видимы во время выполнения, но все-таки допускают настройку на этапе проектирования. • Окна стандартных диалогов. В Windows Forms имеется несколько заготовленных диалоговых окон для распространенных операций (например, OpenFileDialog, PrintDialog и ColorDialog). Понятно, что если эти стандартные диалоги чем-то не удовлетворят вас, вы можете построить на их основе собственные диалоги.
1330 Часть VIII. Приложения Общее количество типов в пространстве имен System.Windows.Forms существенно превышает 100, и было бы излишней тратой времени (и бумаги) перечислять все члены семейства Windows Forms. Однако по мере чтения данного приложения вы получите четкое понимание, которое позволит продвигаться дальше. В любом случае дополнительную информацию можно прочитать в документации .NET Framework 4.0 SDK. Создание простого приложения на основе Windows Forms Конечно, современные IDE для .NET (Visual Studio 2010, С# 2010 Express и SharpDevelop) содержат многочисленные конструкторы форм, визуальные редакторы и интегрированные средства генерации кода (мастера), которые предназначены для облегчения создания приложений Windows Forms. Эти средства исключительно полезны, но они мешают процессу изучения Windows Forms, ведь они генерируют большой объем типового кода, который скрывает базовую объектную модель. Поэтому наш первый пример приложения Windows Forms будет проектом консольного приложения. Вначале создайте консольное приложение с именем SimpleWinFormsApp. Затем выберите пункт меню Projects Add Reference (Проект^Добавить ссылку) и на вкладке .NET открывшегося диалогового окна укажите сборки System.Windows.Forms.dll и System.Drawing.dll. Затем введите в файл Program.cs следующий код: using System; using System.Collections .Generic- using System.Linq; . using System.Text; // Пространства имен, минимально необходимые для Windows Forms. using System.Windows.Forms; namespace SimpleWinFormsApp { // Это объект приложения. class Program { static void Main(string[] args) { Application.Run(new MainWindow()); } } // Это главное окно. class MainWindow : Form { } } На заметку! Когда Visual Studio 2010 обнаруживает класс, расширяющий System.Windows. Forms.Form, открывается соответствующий конструктор графического пользовательского интерфейса (если это первый класс в файле с С#-кодом). Если дважды щелкнуть на файле Program.cs, конструктор откроется, но пока не делайте этого! Работа с конструктором Windows Forms будет рассмотрена в следующем примере; а пока щелкните правой кнопкой в Solution Explorer на файле с С#-кодом и в появившемся контекстном меню выберите пункт View Code (Просмотр кода). Этот код представляет собой самое простое из всех возможных приложений Windows Forms. В качестве абсолютного минимума необходим класс, расширяющий базовый класс Form, и метод Main() для вызова статического метода Application.RunO (классы Form и Application будут рассмотрены ниже в данной главе). Если запустить полу-
Приложение А. Программирование с помощью Windows Forms 1331 ченное приложение, то наверху стека окон откроется окно, которое можно свернуть, развернуть, закрыть, а также изменить его размеры (рис. А.2). На заметку! Запустив эту программу, вы увидите на фоне верхнего окна не очень заметную командную строку. Причиной ее появления является то, что по умолчанию флаг /target, передаваемый компилятору С#, равен /target :exe. Чтобы не выводить командную строку, можно изменить его на /target iwinexe. Для этого нужно дважды щелкнуть в Solution Explorer на значке Properties (Свойства) и на вкладке Application (Приложение) заменить значение Output Type (Тип выходного файла) на Windows Application (Приложение Windows). Рис. А.2. Простое приложение Windows Forms Ну да, полученное приложение не очень впечатляет, но зато оно показывает, каким простым может быть приложение Windows Forms. Чтобы немного его улучшить, можно добавить в класс MainWindow пользовательский конструктор, который позволяет вызывающей программе задать различные свойства отображаемого окна: // Это главное окно. class MainWindow : Form { public MainWindow() {} public MainWindow(string title, int height, int width) { // Задание различных свойств родительского класса. Text = title; Width = width; Height = height; // Унаследованный метод для вывода окна в центре экрана. CenterToScreen (); } } Теперь можно изменить вызов Application.Run(): static void Main(string[] args) { Application.Run(new MainWindow("My Window", 200, 300)); } Это шаг в правильном направлении, но любое чего-то стоящее окно содержит различные элементы пользовательского интерфейса (меню, строки состоянии, кнопки и т.д.), чтобы вводить какую-то информацию. Чтобы понять, как тип, производный от Form, может содержать такие элементы, необходимо разобраться в роли свойства Controls и соответствующей коллекции управляющих элементов. Заполнение коллекции управляющих элементов Базовый класс System.Windows .Forms .Control (потомок типа Form) определяет свойство Controls. Оно является оболочкой для специализированной коллекции ControlCollection, вложенной в класс Control. Эта коллекция содержит ссылки на все элементы графического интерфейса (User Interface — UI), поддерживаемые данным
1332 Часть VIII. Приложения производным типом. Как и другие контейнеры, этот тип поддерживает несколько методов для вставки, удаления и поиска конкретного графического элемента (см. табл. АЛ). Таблица А. 1. Члены коллекции ControlCollection Член Назначение Add(), AddRangeO Вставляют в коллекцию нового потомка типа Control (или массива типов) Clear () Удаляет из коллекции все члены Count Возвращает количество членов в коллекции Remove (), RemoveAt () Удаляют элемент из коллекции Когда нужно заполнить элементами управления пользовательский интерфейс типа, порожденного от класса Form, обычно выполняется одна и та же последовательность шагов: • В классе, порожденном от Form, определяется переменная-член нужного элемента UI. • Настраивается внешний вид и поведение элемента UI. • Полученный элемент UI добавляется в контейнер ControlCollection данной формы с помощью вызова Controls.Add(). Допустим, что в класс MainWindow нужно добавить поддержку меню File^Exit (Файл1^ Выход). Вот необходимые для этого добавления, а их анализ будет выполнен ниже: class MainWindow : Form { // Члены для простой системы меню. private MenuStrip mnuMainMenu = new MenuStripO ; private ToolStripMenuItem mnuFile = new ToolStripMenuItem(); private ToolStripMenuItem mnuFileExit = new ToolStripMenuItem(); public MainWindow(string title, int height, int width) { // Метод для создания системы меню. BuildMenuSystem(); } private void BuildMenuSystem() { // Добавление в главное меню пункта File. mnuFile.Text = "&File"; mnuMainMenu.Items.Add(mnuFile); // Добавление в меню File пункта Exit. mnuFileExit.Text = "E&xit"; mnuFile.DropDownltems.Add(mnuFileExit); mnuFileExit.Click += (o, s) => Application.Exit(); // Вставка меню в данную форму. Controls.Add(this.mnuMainMenu); MainMenuStrip = this.mnuMainMenu; } } Теперь тип MainWindow содержит три новых переменных-члена. Тип MenuStrip представляет всю систему меню, a ToolStripMenuItem — все пункты меню верхнего уровня (наподобие File) и элементы подменю (наподобие Exit), поддерживаемые содержащим их типом.
Приложение А. Программирование с помощью Windows Forms 1333 Система меню настраивается во вспомогательной функции BuildMenuSystem(). Текст каждого ToolStripMenuItem задается свойством Text; каждому пункту меню назначается строковый литерал, содержащий встроенный символ амперсанда. Как известно, этот синтаксис позволяет использовать клавиатурные сокращения с клавишей <Alt>. При этом нажатие <Alt+F> активирует меню File, a <Alt+X> активирует меню Exit. Обратите внимание, что объект ToolStripMenuItem (mnuFile) меню File добавляет подпункты с помощью свойства DropDownltems. Сам объект MenuStrip добавляет пункты меню верхнего уровня с помощью свойства Items. После создания системы меню ее можно добавить в коллекцию элементов управления (с помощью свойства Controls). Затем объект MenuStrip присваивается свойству MainMenuStrip формы. Этот шаг выглядит излишним, но специальное свойство наподобие MainMenuStrip позволяет динамически выбирать, какую систему меню предъявлять пользователю. Отображаемую систему меню можно менять на основе настроек пользователя или параметров безопасности. Еще одним интересным моментом является обработка события Click пункта меню File^Exit; она позволит перехватывать момент выбора пользователем этого пункта. Событие Click работает в сочетании со стандартным типом делегата System.EventHandler. Данное событие может вызывать лишь методы, которые принимают System.Object в качестве первого параметра и System.EventArgs — в качестве второго. Здесь используется лямбда-выражение для завершения всего приложения с помощью статического метода Application.Exit(). Перекомпилировав и запустив это приложение, вы увидите, что ваше простое окно уже содержит пользовательскую систему меню (рис. А.З). ^ My Window □ I В )Ц£Ы [ File |_ ■tJO Рис. А.З. Простое окно с простой системой меню Типы System.EventArgs и System.EventHandler System.EventHandler — один из многих типов-делегатов, применяемых в API Windows Forms (и ASP.NET) во время обработки событий. Как вы уже видели, этот делегат может указывать лишь на методы, где первый аргумент имеет тип System.Object, a второй является ссылкой на объект, сгенерировавший данное событие. Предположим, к примеру, что нужно изменить реализацию лямбда-выражения следующим образом: mnuFileExit.Click += (о, s) => { MessageBox.Show(string.Format ("{0} сгенерировал это событие", о.ToString())); Application.Exit (); }; Можно проверить, что данное событие сгенерировано типом mnuFileExit, т.к. строка в окне сообщения имеет вид "E&xit сгенерировал это событие"
1334 Часть VIII. Приложения А для чего нужен второй аргумент — System.EventArgs? Вообще-то мало для чего, т.к. он просто расширяет тип Object и практически не добавляет никакие возможности: public class EventArgs { public static readonly EventArgs Empty; static EventArgs (); public EventArgs (); } Однако этот тип может быть полезен в общей схеме обработки событий в .NET, т.к. он является предком многих (полезных) порожденных событий. К примеру, тип MouseEventArgs расширяет тип EventArgs, добавляя информацию о текущем состоянии мыши. Тип KeyEventArgs, также расширяющий EventArgs, сообщает информацию о состоянии клавиатуры (например, какая клавиша была нажата); тип PaintEventArgs расширяет EventArgs для сообщения графических данных и т.д. Многочисленные потомки EventArgs (и использующие их делегаты) встречаются не только при работе с Windows Forms, но и при работе с API WPF и ASP.NET. Вы можете и дальше добавлять различные возможности в класс MainWindow (например, строку состояния и диалоговые окна) с помощью простого текстового редактора, но так недолго переутомиться — ведь придется вручную писать всю муторную логику настройки управляющих элементов. К счастью, в Visual Studio 2010 встроены многочисленные конструкторы, которые могут позаботиться за вас обо всех таких мелочах. При использовании этих средств до конца данной главы не забывайте, что они просто генерируют обыденный С#-код, и ничего магического в них нет. Исходный код. Проект SimpleWinFormsApp находится в подкаталоге Appendix A. Шаблон проектов Windows Forms из Visual Studio Применение средств проектирования Windows Forms из Visual Studio 2010 обычно начинается с выбора шаблона для проекта приложения Windows Forms — с помощью пункта меню File^New Project (Файл1^Новый проект). Чтобы освоится с основными средствами проектирования Windows Forms, создайте новое приложение с именем SimpleVSWinFormsApp (рис. А.4). Поверхность визуального конструктора Прежде чем приступить к созданию более интересных Windows-приложений, вначале заново создадим предыдущий пример, но уже с помощью средств проектирования. После создания нового проекта Windows Forms в Visual Studio 2010 появляется поверхность конструктора, на которую можно перетаскивать любое количество управляющих элементов. Этот же конструктор позволяет настраивать начальный размер окна, просто изменяя размеры самой формы с помощью специальных "рукояток" (рис. А.5). Если вы захотите настроить внешний вид окна (а также любого элемента на нем), это можно сделать с помощью окна Properties (Свойства). Как и в Windows Presentation Foundation, это окно позволяет назначать значения свойствам, а также устанавливать обработчики событий для элемента, выделенного в данный момент на конструкторе (конфигурация выбирается с помощью выпадающего списка в верхней части окна Properties). Пока форма не содержит ничего, и поэтому список состоит только из первоначальной формы, которой по умолчанию присвоено имя Forml — это видно из свойства Name (только для чтения) на рис. А.6.
Приложение А. Программирование с помощью Windows Forms 1335 ,-NET Framework 4 •* Sort by*. Defat ^] Windows Forms Application Veu*IC# Jj Class Librae tf.sual C* ,-jf| WPF Application Visual C# _Но)г) WPF Browser Application Visual C* Щ| Console Application Visual C* #d* WPF Custom Control Library Visual C* СУ] Empty Project Visual C* Per user extensions are currently not aBowed to load. £ Щ Tjpc Visual C# A project for creating an application with a Windows Forms user interface Рис. А.4. Шаблон проекта Windows Forms в Visual Studio Рис. А.5. Визуальный конструктор форм Properties Forml System.Windows.Forms.Form p (ApplicationSettings) P (DataBindings) AcceptButton AccessibkDescription AccessibleName AccessibleRole AllowDrcp AutoScaleMode AutoScroll (Name) Indicates the name used in code to Щ Forml (none) Default False Font False identify the object, * Рис. А.6. Окно Properties для задания свойств и обработчиков событий
1336 Часть VIII. Приложения На заметку! В окне Properties можно указать вывод содержимого по категориям или по алфавиту — для этого предназначены первые две кнопки под выпадающим списком. Я рекомендую упорядочить их по алфавиту: так легче быстро найти нужное свойство или событие. Следующий элемент конструктора, о котором стоит знать — окно проводника решений (Solution Explorer). Все проекты Visual Studio 2010 поддерживают это окно, но оно особенно удобно при создании приложений Windows Forms для A) быстрого изменения имени файла и соответствующего класса для любого окна и B) просмотра файла, который содержит сопровождаемый конструктором код (об этом интересном моменте будет рассказано чуть ниже). А пока щелкните правой кнопкой на значке Forml.cs и выберите пункт Rename (Переименовать). Назовите первоначальное окно более осмысленно: MainWindow.es. IDE спросит, хотите ли вы изменить имя начального класса — согласитесь на это. Разбор первоначальной формы Прежде чем приступить к созданию системы меню, необходимо точно разобраться в том, что Visual Studio 2010 сгенерировала по умолчанию. Щелкните в окне Solution Explorer правой кнопкой на значке MainWindow.cs и выберите пункт View Code (Просмотр кода). Обратите внимание: форма определена как частичный тип, что позволяет определить один тип в нескольких кодовых файлах (подробнее об этом написано в главе 5). Кроме того, конструктор формы вызывает метод InitializeComponentO, a сгенерированный объект имеет тип Form: namespace SimpleVSWinFormsApp { public partial class MainWindow : Form { public MainWindow() { InitializeComponent(); } } } Как и следовало ожидать, метод InitializeComponentO определен в отдельном файле, который завершает определение частичного класса. По соглашению имя такого файла всегда заканчивается на .Designer.cs, а перед этим находится имя соответствующего С#-файла, содержащего тип-наследник Form. Откройте в окне Solution Explorer файл MainWindow. Designer, cs. Теперь посмотрите на следующий код (для простоты из него убраны комментарии; ваш код может слегка отличаться, в зависимости от настроек, выполненных в окне Properties): partial class MainWindow { private System.ComponentModel.IContainer components = null; protected override void Dispose(bool disposing) { if (disposing && (components '= null)) { components.Dispose(); } base.Dispose(disposing) ; }
Приложение А. Программирование с помощью Windows Forms 1337 private void InitializeComponent () { this.SuspendLayout (); this.AutoScaleDimensions = new System.Drawing.SizeFFF, 13F) ; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.SizeD22, 114); this.Name = "Forml"; this.Text = "Forml"; this.ResumeLayout(false); } } Переменная-член IContainer и метод Dispose () — это лишь инфраструктура, используемая средствами проектирования Visual Studio. А вот метод InitializeComponentO активно используется, причем не только конструктором формы во время выполнения: Visual Studio применяет этот метод во время проектирования для правильного отображения пользовательского интерфейса на поверхности конструктора Forms. Чтобы убедиться в этом, измените значение свойства Text текущей формы на "My Main Window" (Мое главное окно). После активации конструктора заголовок формы соответственно изменится. При использовании средств визуального проектирования (т.е. окна Properties или конструктора форм) IDE автоматически изменяет код метода InitializeComponentO. Для демонстрации этого аспекта конструктора Windows Forms сделайте активным в IDE окно конструктора Forms и найдите в окне Properties свойство Opacity (Прозрачность). Измените его значение на 0.8 (80%) — при следующей компиляции и выполнении программы окно станет немного прозрачным. А теперь еще раз посмотрите на реализацию метода InitializeComponentO: private void InitializeComponentO { this.Opacity = 0.8; } При практической работе не обращайте внимания на файлы *.Designer.cs: IDE автоматически сопровождает их при создании приложения Windows Forms с помощью Visual Studio. Если вы вставите в InitializeComponent () синтаксически (или логически) ошибочный код, то конструктор может оказаться неработоспособным. Кроме того, Visual Studio часто переформатирует этот метод на этапе проектирования. Поэтому IDE может удалить добавленный вами в метод InitializeComponentO пользовательский код! И всегда помните, что каждое окно приложения Windows Forms составлено с помощью частичных классов. Разбор класса Program Кроме кода реализации первоначального типа, производного от Form, типы из проекта Windows Application также содержат статический класс с именем Program, который определяет точку входа программы Main(): static class Program { [STAThread] static void Main 0 { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainWindow()); } }
1338 Часть VIII. Приложения Метод Main () вызывает Ар plication. Run() и еще пару других методов из типа Application, чтобы установить некоторые основные параметры прорисовки. Обратите внимание на атрибут [STAThread]: он сообщает среде выполнения, что если данный поток создаст за время своей жизни какие-либо классические объекты СОМ (в том числе и устаревшие элементы ActiveX), то их следует поместить в специальную область — однопоточный апартамент (single-threaded apartment). Если коротко, это гарантирует многопоточную поддержку объектов СОМ, даже если автор конкретного СОМ-объекта не включил в код специальный код. Визуальное построение системы меню Чтобы завершить обзор средств визуального проектирования Windows Forms и перейти к более иллюстративным примерам, активируйте окно конструктора Forms, найдите окно Toolbox (Набор инструментов) Visual Studio 2010 и под узлом Menus & Toolbars (Меню и Инструментальные панели) найдите элемент MenuStrip (рис. А.7). Перетащите элемент MenuStrip в верхнюю часть конструктора Forms. После этого Visual Studio откроет редактор меню. Если внимательно присмотреться, можно увидеть в верхнем правом углу элемента маленький треугольник. Щелчок на нем открывает контекстно-чувствительный встроенный редактор, который позволяет задать значения сразу нескольких свойств (кстати, аналогичные редакторы имеются у многих элементов управления Windows Forms). К примеру, щелкните на параметре Insert Standard Items (Вставка стандартных элементов), как показано на рис. А.8. В этом примере Visual Studio любезно создала за вас всю систему меню. А теперь откройте сопровождаемый конструктором файл (MainWindow.Designer.cs) — вы увидите, что в методе InitializeComponentO появилось несколько новых переменных-членов, которые представляют созданную систему меню (замечательная вещь эти средства проектирования!). И, наконец, вернитесь в конструктор и отмените последнюю операцию, нажав на клавиатуре комбинацию Ctrl+Z. После этого вы опять будете находиться в редакторе меню, а сгенерированный код исчезнет. С помощью конструктора меню введите самый верхний пункт меню File (Файл) и его подпункт Exit (Выход) (рис. А.9). 1 Toolbox 1 ;> All Windows Forms 1 & Common Controls 1 t> Containers 1 л Menus & Toolbars Ifc Pointer §0 ContextMenuStrip $Ш MenuStrip U~ StatusStrip Ш ToolStrip Q ToolStripContainer 1 t> Data 1 •> Components 1 & Printing 1 !> Dialogs ' 1 lГ.W llliriirniiril-liflliiit * ~ ] *| 1 5 1 Рис. А.7. Элементы управления Windows Forms, которые можно добавить на поверхность конструктора M»inWindow.cs {Design]1* X | ■ь* menuStripl Dock: | Top GripStyte ; Hidden ;nt ToolStripi Рис. А.8. Встроенный редактор меню
Приложение А. Программирование с помощью Windows Forms 1339 Ma.nw.ndo».cs[D«.gnj j. \ВВЯШШШВШШВШШШШШШШВ lain Windo-л щ menuStripl Рис. А.9. Ручное создание системы меню А теперь взгляните на код InitializeComponentO — вы увидите примерно такой же код, который вы ввели вручную в первом примере данного приложения. В заключение упражнения вернитесь в конструктор Forms и щелкните на кнопке со значком молнии в окне Properties. Будут показаны все события, на которые можно реагировать для выбранного элемента. Выберите меню Exit (по умолчанию называется exitToolStripMenuItem), а затем найдите событие Click (рис. АЛО). Теперь можно ввести имя метода, который будет вызываться при щелчке на элементе, а если лень, то можно просто дважды щелкнуть на событии из списка в окне Properties. В последнем случае IDE выберет за вас имя обработчика события (по образцу ИмяЭлемента_ИмяСобытия()). При любом варианте IDE создает код заглушки, который можно заполнить конкретной реализацией: Рис- А. 10. События, доступные для обработки с помощью IDE public partial class MainWindow : Form { public MainWindow() { InitializeComponent(); CenterToScreen (); } private void exitToolStripMenuItem_Click (object sender, EventArgs e) { Application.Exit (); } } Загляните в код InitializeComponentO — в нем также учтены все последние изменения: this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click); Теперь вы, видимо, уже можете более свободно перемещаться по IDE при создании приложений Windows Forms. Конечно, существует множество дополнительных клавиатурных сочетаний, редакторов и интегрированных мастеров генерации кода, но изложенной информации вполне достаточно для продолжения работы. L.J.i;i.::^...j j з (а S3 : j Properties * G X exitToolStripMenuItem System.Windows.Forms.ToolSt * Disp.layStyleChanged DoubleClick DropDownClosed DrcpDownltemClicked Click Occurs vvhen the item is clicked.
1340 Часть VIII. Приложения Анатомия формы Итак, вы уже умеете создавать простые приложения Windows Forms с помощью Visual Studio (или без нее). А теперь пора хорошенько разобраться в типе Form. В мире Windows Forms тип Form представляет любое окно в приложении, включая главное окно самого верхнего уровня, дочерние окна приложений с многодокументным интерфейсом (multiple document interface — MDI), а также модальные и немодальные диалоговые окна. Как показано на рис. А. 11, тип Form содержит множество возможностей, унаследованных от классов-предков, а также из реализуемых им многочисленных интерфейсов. л & Баи Types л ■*>% ContainerControl л '*$ ScroltableControl л % Control л **i$ Component л *$ MsrshelByRefObject - 3 Object i> "О ICcmponent t> *ч) Disposable .• *"° IBmdableCcmponent t> ** IComponent r> *"° Disposable p. <-o IDropTarget > *"° JSynchfonirelnvoke 6> -° IWin32Window '„■, *o IComponent ;. -o iDisposable :» ""° IContainerControl > Q| Derived Types ■ •*$ Form.ControlCollectiori > d Extension Members % Activated ЦЬ ActivateMdiChild(S>rtem.Aindo«s.Forms.Form) ♦ AddOwnedFormi System. V.'indcws.Fcrms.Formj *}• AdjustFormScrollbars(bool) $♦ CenterToParentf) |* CenterToScreen(J % Closed !?♦ CreateContrclsInstanceO I* OeateHandle/J — public class Form : S^»m..WiP<Joj»?^pnn»CAritjiJn!trgpntrBl Member of Syt*CT..Win4ow»,Fom'» Summary: Represents a window or dialog box that makes up an application's user interface. Attributes: [System.ComponentModel.DesignerAttribute rSystem.WindovrS.Forms.Design.FormDocumentDesigner System.Design. Version =4.0.0.0. Cutture=neutral PublicKeyToken=b03f5f7flld50a3a-\ Sj£^pji£nmnnrigrjlMQrlelJlesififiJRQOJDfisu3Qfi Лу"й£гл v>j.sjon= Рис. А.11. Цепочка наследования для типа System.Windows.Forms.Form В табл. А.2 приведен высокоуровневый обзор каждого из классов в цепочке наследования класса Form. Таблица А.2. Базовые классы в цепочке наследования класса Form Класс-предок Назначение System.Object System.MarshalByRefObject System. ComponentModel. Component System. Windows. Forms. Control System.Windows. Forms.ScrollableControl System.Windows. Forms.ContainerControl System.Windows.Forms.Form Как и любой класс в .NET, класс Form является объектом К типам, порожденным от этого класса, можно дистанционно обратиться с помощью ссылки на удаленный тип (а не локальной копии) Предоставляет стандартную реализацию интерфейса IComponent. В мире .NET компонент — это тип, поддерживающий редактирование на этапе проектирования, хотя он не обязательно видим во время выполнения Определяет общие члены UI для всех элементов управления Windows Forms, включая и сам тип Form Определяет поддержку горизонтальных и вертикальных полос прокрутки, а также членов, управляющих окном просмотра в прокручиваемой области Содержит функции управления фокусом ввода для элементов, которые могут служить в качестве контейнеров для других элементов управления Представляет любую пользовательскую форму, MDI- форму, или диалоговое окно
Приложение А. Программирование с помощью Windows Forms 1341 Для полноценного порождения типа Form нужны и многие другие базовые классы и интерфейсы, но даже профессиональному разработчику Windows Forms совсем не обязательно знать роли всех членов всех классов или реализованных интерфейсов. Вообще- то большинство членов (а именно, свойств и событий) можно спокойно настраивать с помощью окна Properties (Свойства) Visual Studio 2010. Однако все-таки важно знать возможности, предоставляемые родительскими классами Control и Form. Возможности класса Control Класс System.Windows.Forms.Control задает общее поведение, необходимое для любого типа графического пользовательского интерфейса. Основные члены класса Control позволяют настраивать размер и позицию элемента, захватывать данные клавиатуры и мыши, получать или задавать фокус/видимость члена и т.д. В табл. А.З приведены наиболее интересные свойства, которые сгруппированы по схожести функций. Таблица А.З. Основные свойства типа Control Свойство Назначение BackColor ForeColor Backgroundlmage Font Cursor Anchor Dock AutoSize Top Left Bottom Right Bounds ClientRectangle Height Width Enabled Focused Visible ModifierKeys MouseButtons Tablndex TabStop Opacity Text Controls Определяют основные качества элемента (цвет, шрифт текста и вид курсора мыши, когда он находится над элементом) Управляют положением элемента внутри контейнера Задают текущие размеры элемента Содержат логические значения, которые задают состояние элемента Статическое свойство, проверяет текущее состояние клавиш-модификаторов (Shift, Ctrl и Alt) и возвращает это состояние в типе Keys Статическое свойство, проверяет текущее состояние кнопок мыши (левая, правая и средняя) и возвращает это состояние в типе MouseButtons Позволяют настроить очередность переходов при нажатии клавиши <ТаЬ> Определяет прозрачность элемента @.0 — полностью прозрачный, 1.0 — полностью непрозрачный) Содержит строковые данные, связанные с данным элементом Предоставляет доступ к строго типизированной коллекции (например, ControlsCollection), которая содержит все дочерние элементы данного управляющего элемента
1342 Часть VIII. Приложения Конечно, класс Control определяет ряд событий, которые позволяют перехватывать действия, связанные, к примеру, с мышью, клавиатурой, отображением и перетаскиванием. Некоторые полезные события, сгруппированные по схожести функций, приведены в табл. А.4. Таблица А.4. События типа Control Событие Назначение Click Позволяют взаимодействовать с мышью Doubleclick MouseEnter MouseLeave MouseDown MouseUp MouseMove MouseHover MouseWheel KeyPress Позволяют взаимодействовать с клавиатурой KeyUp KeyDown DragDrop Позволяют отслеживать действия при перетаскивании DragEnter DragLeave DragOver Paint Позволяет взаимодействовать со службой графического отображения из GDI+ И, наконец, базовый класс Control определяет несколько методов, которые позволяют взаимодействовать с любым типом, порожденным от Control. Просматривая методы типа Control, обратите внимание, что имена многих из них содержат префикс On с последующим именем конкретного события (например, OnMouseMove, OnKeyUp и On Paint). Каждый из таких виртуальных On-методов представляет собой обработчик соответствующего события по умолчанию. Если вы переопределите любой из этих виртуальных методов, вы получите возможность выполнять всю необходимую пред- и постобработку события перед и после вызова реализации по умолчанию в родительском классе: public partial class MainWindow : Form { protected override void OnMouseDown(MouseEventArgs e) { // Сюда можно добавить пользовательский код обработки события MouseDown. // После этого - вызов родительской реализации. base.OnMouseDown(e); } } В некоторых случаях это может быть удобно (особенно при создании пользовательского элемента, производного от стандартного управляющего элемента), но события часто обрабатываются с помощью стандартного синтаксиса работы с событиями в С# — вообще-то это и есть стандартное поведение конструкторов в Visual Studio. При такой обработке событий среда вызывает пользовательский обработчик события после завершения работы родительской реализации. Например, следующий код позволяет вручную обрабатывать событие MouseDown:
Приложение А. Программирование с помощью Windows Forms 1343 public partial class MainWindow : Form { public MainWindow() { MouseDown += new MouseEventHandler(MainWindow_MouseDown); } private void MainWindow_MouseDown(object sender, MouseEventArgs e) { // Добавьте код обработки события MouseDown. } } Кроме описанных методов ОпХХХ(), следует знать еще о нескольких других: • Hide() — скрывает элемент и заносит в свойство Visible значение false. • Show() — делает элемент видимым и заносит в свойство Visible значение true. • Invalidate() — заставляет элемент вывести себя снова, послав событие Paint (мы еще поговорим о графическом отображении с помощью GDI+ ниже в данном приложении). Возможности класса Form Обычно (хотя и не обязательно) класс Form является непосредственным базовым классом для пользовательских типов Form. Кроме обширного набора членов, унаследованных от классов Control, ScrollableControl и ContainerControl, тип Form добавляет дополнительные возможности — в особенности для главных окон, дочерних MDI-окон и диалоговых окон. Сначала ознакомимся с основными свойствами, приведенными в табл. А.5. Таблица А.5. Свойства типа Form Свойство Назначение AcceptButton Получает или назначает кнопку на форме, для которой имитируется щелчок при нажатии клавиши <Enter> ActiveMdiChild Используются в контексте MDI-приложений IsMdiChild IsMdiContainer CancelButton Получает или назначает кнопку на форме, для которой имитируется щелчок при нажатии клавиши <Esc> ControlBox Получает или устанавливает значение, которое указывает, есть ли на форме блок управления (обычно значки, позволяющие свернуть, развернуть и закрыть форму в правом верхнем углу окна) FormBorderStyle Получает или задает стиль формы. Используется в сочетании с перечислением FormBorderStyle Menu Получает или устанавливает меню, расположенное на форме MaximizeBox Определяют, активны ли на форме значки развертывания и свертывания MinimizeBox ShowlnTaskbar Определяет, будет ли отображаться форма на панели задач Windows StartPosition Получает или определяет первоначальную позицию формы во время выполнения, заданную с помощью перечисления FormStartPosition WindowState Задает отображение формы на этапе запуска. Используется в сочетании с перечислением FormWindowState
1344 Часть VIII. Приложения Кроме многочисленных стандартных On-обработчиков событий, тип Form определяет ряд основных методов, перечисленных в табл. А.6. Таблица А.6. Основные методы типа Form Метод Назначение Activate () Делает данную форму активной и передает ей фокус ввода Close () Закрывает текущую форму CenterToScreenO Помещает форму в центр экрана LayoutMdi () Размещает все дочерние формы (в соответствии с перечислением MdiLayout) в родительской форме Show () Выводит форму как немодальное окно ShowDialogO Выводит форму как модальное диалоговое окно И, наконец, класс Form определяет ряд событий, большинство из которых генерируются во время жизни формы (см. табл. А. 7). Таблица А.7. События типа Form Событие Назначение Activated Возникает при активации формы, т.е. при получении фокуса на рабочем столе FormClosed Возникают непосредственно перед закрытием формы и сразу после FormClosing закрытия Deactivate Возникает при деактивации формы, т.е. при потере фокуса на рабочем столе Load Возникает после размещения формы в памяти, но еще до отображения на экране MdiChildActive Возникает при активации дочернего окна Жизненный цикл типа Form Если вам доводилось программировать пользовательские интерфейсы с помощью инструментальных наборов для построения графических пользовательских интерфейсов наподобие Java Swing, Mac OS X Cocoa или WPF, то вы знаете, что оконные типы имеют много событий, генерируемых во время их жизни. Это верно и для Windows Forms. Как вы уже знаете, жизнь формы начинается тогда, когда вызывается конструктор класса перед его передачей методу Application.Run(). Когда объект помещен в управляемую кучу, среда генерирует событие Load (Загружен). В обработчике события Load можно настроить внешний вид объекта Form, подготовить содержащиеся в нем дочерние элементы (наподобие ListBox и TreeView) или выделить ресурсы, необходимые для работы формы (например, подключение к базе данных и модули доступа к удаленным объектам). После события Load генерируется событие Activated (Активирован), но только тогда, когда форма получает фокус как активное окно на рабочем столе. Логической противоположностью событию Activated является (конечно же) событие Deactivate (Деактивирован), которое возникает, когда форма перестает быть активным окном.
Приложение А. Программирование с помощью Windows Forms 1345 Понятно, что события Activated и Deactivate могут возникать неоднократно за время жизни конкретного объекта Form, когда пользователь переключается между активными окнами и приложениями. Если пользователь решает закрыть данную форму, возникают два события: FormClosing (Форма закрывается) и FormClosed (Форма закрыта). Событие FormClosing генерируется первым и удобно для вывода конечному пользователю надоедливого (но полезного) сообщения: "Вы уверены, что хотите закрыть данное приложение?" Этот шаг позволяет пользователю сохранить все нужные данные, прежде чем закрыть приложение. Событие FormClosing работает в сочетании с делегатом FormClosingEventHandler. Если установить свойство FormClosingEventArgs.Cancel равным true, то окно не будет уничтожено и просто возвратится к нормальной работе. А если FormClosingEventArgs.Cancel равно false, то возникает событие FormClosed, и приложение Windows Forms завершает работу, выгружает AppDomain и завершает процесс. Приведенный ниже фрагмент изменяет конструктор формы и обрабатывает события Load, Activated, Deactivate, FormClosing и FormClosed (вспомните: в главе 11 было написано, что IDE автоматически генерирует нужный делегат и обработчик события при двукратном нажатии клавиши <ТаЬ> после ввода символов +=): public MainWindow() { InitializeComponent (); // Обработка различных событий времени жизни формы. FormClosing += new FormClosingEventHandler(MainWindow_Closing); Load += new EventHandler(MainWindow_Load); FormClosed += new FormClosedEventHandler(MainWindow_Closed); Activated += new EventHandler(MainWindow_Activated); Deactivate += new EventHandler(MainWindow_Deactivate); } В обработчиках событий Load, FormClosed, Activated и Deactivate нужно изменить значение новой строковой переменной-члена уровня Form (с именем lifeTimelnfo) — это просто сообщение, которое выводит имя перехваченного события. Сначала добавьте этот член в свой класс, производный от Form: public partial class MainWindow : Form [ private string lifeTimelnfo = ""; } После этого необходимо реализовать обработчики событий. Значение строки lifeTimelnfo выводится в диалоговом окне в обработчике события FormClosed: private void MainWindow_Load(object sender, System.EventArgs e) { lifeTimelnfo += "Load event\n"; // Событие Load } private void MainWindow_Activated(object sender, System.EventArgs e) { lifeTimelnfo += "Activate event\n"; // Событие Activate } private void MainWindow_Deactivate(object sender, System.EventArgs e) [ lifeTimelnfo += "Deactivate event\n"; // Событие Deactivate }
1346 Часть VIII. Приложения private void MainWindow_Closed(object sender, FormClosedEventArgs e) { lifeTimelnfo += "FormClosed event\n"; // Событие FormClosed MessageBox.Show(lifeTimelnfo); } В обработчике события FormClosing выводится запрос, действительно ли пользователь хочет завершить работу приложения с входными аргументами FormClosingEventArgs. В приведенном ниже коде метод MessageBox.Show() возвращает объект DialogResult, содержащий значение, которое представляет кнопку, выбранную конечным пользователем. В нашем случае диалоговое окно содержит кнопки Yes и No; поэтому значение, возвращаемое методом Show(), проверяется на равенство DialogResult.No: private void MainWindow_Closing(object sender, FormClosingEventArgs e) { lifeTimelnfo += "FormClosing event\n"; // Вывод диалогового окна с кнопками Yes и No. DialogResult dr = MessageBox.Show ("Вы ДЕЙСТВИТЕЛЬНО хотите закрыть это приложение?", "Closing event!", MessageBoxButtons.YesNo); //На какой кнопке щелкнул пользователь? if (dr == DialogResult.No) e.Cancel = true; else e.Cancel = false; } И последний штрих. В настоящий момент пункт меню File^Exit (Файл<=>Выход) сразу удаляет все приложение, что нежелательно. Обычно обработчик File^Exit окна верхнего уровня вызывает наследуемый метод Close(), который генерирует события, связанные с закрытием, и лишь затем уничтожает приложение: private void exitToolStripMenuItem_Click (object sender, EventArgs e) { // Application.Exit(); Close () ; } Теперь запустите полученное приложение и несколько раз переместите фокус на форму и вне ее (для запуска событий Activated и Deactivate). При закрытии приложения появится диалоговое окно, которое выглядит примерно так, как показано на рис. А. 12. Исходный код. Проект SimpleVSWinFormsApp находится в подкаталоге Appendix A. Реагирование на действия мыши и клавиатуры Вы, видимо, помните, что родительский класс Control определяет набор событий, которые позволяют различными способами отслеживать действия мыши и клавиатуры. Чтобы удостовериться в этом, создайте новый проект приложения Windows Forms с име- * нем MouseAndKeyboardEventsApp, измените (используя Solution Explorer) первоначальное имя формы на MainWindow.cs и предусмотрите обработку события MouseMove с помощью окна Properties (Свойства). Эти шаги генерируют следующий обработчик события: Load event Activate event Deactivate event Activate event FormClosing event Deactivate event Activate event FormClosing event Deactivate event Activate event FormClosed event OK Рис. А.12. Жизнь и отдельные моменты типа-наследника Form
Приложение А. Программирование с помощью Windows Forms 1347 public partial class MainWindow : Form { public MainWindow() { InitializeComponent (); } // Сгенерировано с помощью окна Properties. private void MainWindow_MouseMove(object sender, MouseEventArgs e) { } } Событие MouseMove работает в сочетании с делегатом System.Windows.Forms. MouseEvent Handle г. Этот делегат может вызывать только методы, у которых первый параметр имеет тип System.Object, а второй — тип MouseEventArgs. Данный тип содержит различные члены, предоставляющие подробную информацию о состоянии события, связанного с мышью: public class MouseEventArgs : EventArgs { public MouseEventArgs(MouseButtons button, int clicks, int x, int y, int delta); public MouseButtons Button { get; } public int Clicks { get; } public int Delta { get; } public Point Location { get; } public int X { get; } public int Y { get; } } Большинство общедоступных свойств понятны сами по себе, но в табл. А.8 приведены более конкретные детали. Таблица А.8. Свойства типа MouseEventArgs Свойство Назначение Button Выдает кнопку мыши, которой был выполнен щелчок (см. перечисление MouseButtons) Clicks Выдает количество нажатий и отпусканий кнопки мыши Delta Выдает количество делений (которые соответствуют одному сдвигу колесика мыши) со знаком для текущего поворота колесика мыши Location Возвращает объект Point, содержащий координаты х и у мыши X Выдает координату х щелчка мышью Y Выдает координату у щелчка мышью Теперь можно реализовать обработчик события MouseMove, который будет отображать в заголовке Form текущие Х- и Y-координаты мыши. Для этого применяется свойство Location: private void MainWindow_MouseMove (object sender, MouseEventArgs e) { Text = string.Format("Mouse Position: {0}", e.Location); // "Позиция мыши" }
1348 Часть VIII. Приложения Если запустить это приложение и перемещать курсор мыши по окну, то позиция курсора будет отображаться в поле заголовка типа MainWindow (рис. А. 13). ».J Mouse Position: {X=59r¥=33} 3 IftEfrl Г Г Рис. А. 13. Перехват движений мыши Определение кнопки мыши, которой был выполнен щелчок Еще одно действие, которое обычно требуется выполнять при работе с мышью — определение, какой кнопкой был выполнен щелчок при возникновении события MouseUp, MouseDown, MouseClick или MouseDoubleClick. Когда нужно точно знать кнопку мыши (левую, правую или среднюю), нужно проверить значение свойства Button класса MouseEventArgs. Значения свойства Button берутся из соответствующего перечисления MouseButtons: public enum MouseButtons { Left, Middle, None, Right, XButtonl, XButton2 } На заметку! Значения XButtonl и XButton2 позволяют перехватывать кнопки движения вперед и назад, которые имеются у многих устройств, совместимых с контроллером мыши. Все это можно проверить в действии: для этого достаточно обрабатывать событие MouseDown типа MainWindow с помощью окна Properties (Свойства). Приведенный ниже обработчик события MouseDown выводит кнопку мыши, щелчок которой был выполнен в пределах окна с сообщением: private void MainWindow_MouseDown (object sender, MouseEventArgs e) { // Какой кнопкой был выполнен щелчок? if(e.Button == MouseButtons.Left) MessageBox.Show("Left click!"); // Щелчок левой кнопкой if(e.Button == MouseButtons.Right) MessageBox.Show("Right click!"); // Щелчок правой кнопкой if (e.Button == MouseButtons.Middle) MessageBox.Show("Middle click!"); // Щелчок средней кнопкой } Определение нажатой клавиши В Windows-приложениях обычно имеется множество элементов ввода данных (например, TextBox), в которые пользователь может ввести информацию с помощью
Приложение А. Программирование с помощью Windows Forms 1349 клавиатуры. При таком перехвате клавиатурного ввода нет необходимости в явной обработке событий клавиатуры, т.к. текстовую информацию можно извлечь из управляющего элемента с помощью различных свойств (например, свойства Text в случае типа TextBox). Но если вам понадобится отслеживать ввод с клавиатуры для более экзотических целей (например, для фильтрации символов, поступающих в управляющий элемент или захвата нажатий клавиш на самой форме), то для таких случаев в библиотеках базовых классов имеются события KeyUp и KeyDown. Эти события работают в сочетании с делегатом KeyEventHandler, который может указывать на любой метод, принимающий объект в качестве первого параметра и тип KeyEventArgs в качестве второго. Этот тип определяется так: public class KeyEventArgs : EventArgs { public KeyEventArgs (Keys keyData); public virtual bool Alt { get; } public bool Control { get; } public bool Handled { get; set; } public Keys KeyCode { get; } public Keys KeyData { get; } public int KeyValue { get; } public Keys Modifiers { get; } public virtual bool Shift { get; } public bool SuppressKeyPress { get; set; } } В табл. А.9 приведены некоторые наиболее интересные свойства, поддерживаемые типом KeyEventArgs. Таблица А.9. Свойства типа KeyEventArgs Свойство Назначение Alt Определяет, была ли нажата клавиша Alt Control Определяет, была ли нажата клавиша Ctrl Handled Выдает или задает значение, определяющее, было ли событие полностью обработано его обработчиком KeyCode Выдает код клавиши для события KeyDown или KeyUp Modifiers Определяет, какие клавиши-модификаторы (Ctrl, Shift, и/или Alt) были нажаты Shift Определяет, была ли нажата клавиша Shift Все это можно увидеть в действии, обрабатывая событие KeyDown: private void MainWindow_KeyDown(object sender, KeyEventArgs e) { Text = string.Format("Key Pressed: {0} Modifiers: {1}", // Нажата клавиша: Модификаторы: e.KeyCode.ToString (), e.Modifiers.ToString ()); } А теперь откомпилируйте и запустите полученную программу. Вы можете узнать не только, какой кнопкой мыши был выполнен щелчок, но и какая клавиша была нажата. Например, на рис. А. 14 показан результат одновременного нажатия клавиш <Ctrl> и <Shift>.
1350 Часть VIII. Приложения Рис. А. 14. Перехват действий клавиатуры Исходный код. Проект MouseAndKeyboardEventsApp находится в подкаталоге Appendix A. Создание диалоговых окон В программах с графическим пользовательским интерфейсом диалоговые окна являются одним из основных способов ввода пользовательской информации для использования в самом приложении. В отличие от других API-интерфейсов, с которыми вы, возможно, имели дело, в Windows Forms нет базового класса Dialog. Все диалоговые окна являются простыми типами, порожденными от класса Form. Как правило, диалоговые окна не должны менять свой размер, поэтому свойству FormBorderStyle присваивается значение FormBorderStyle.FixedDialog. Кроме того, свойства MinimizeBox и MaximizeBox обычно устанавливаются равными false. В этом случае диалоговое окно имеет постоянный размер. А если установить равным false свойство ShowInTaskbar, то форма не будет отображаться на панели задач Windows. Сейчас мы научимся создавать диалоговые окна и работать с ними. Создайте новый проект Windows Forms Application с именем CarOrderApp и измените с помощью Solution Explorer первоначальное имя файла For ml. с s на MainWindow.cs. Теперь воспользуйтесь конструктором Forms, чтобы создать простое меню File^Exit (Файл^Выход), а также пункт меню Tool1^Order Automobile... (СервисеЗаказ автомобиля...). Напомним, что для создания меню нужно перетащить элемент MenuStrip с инструментальной панели, а затем в окне конструктора настроить пункты меню. После этого нужно написать обработчики события Click для пунктов меню Exit и Order Automobile с помощью окна Properties (Свойства). Обработчик пункта меню File^Exit завершает работу приложения вызовом Close(): private void exitToolStripMenuItem_Click(object sender, EventArgs e) { Close () ; } Теперь в меню Project (Проект) Visual Studio выберите пункт Add Windows Forms (Добавить форму Windows Forms) и назовите новую форму OrderAutoDialog.es (рис. А. 15). В нашем примере создайте диалоговое окно с традиционными кнопками О К и Cancel (которые называются соответственно btnOK и btnCancel), а также тремя полями TextBox с именами txtMake, txtColor и txtPrice. Теперь воспользуйтесь окном Properties, чтобы завершить создание диалога: • Свойство FormBorderStyle задайте равным FixedDialog. • Свойства MinimizeBox и MaximizeBox задайте равными false. • Свойство StartPosition задайте равным CenterParent. • Свойство ShowInTaskbar задайте равным false.
Приложение А. Программирование с помощью Windows Forms 1351 Рис. А. 15. Вставка новых диалоговых окон с помощью Visual Studio Свойство DialogResult И, наконец, выберите кнопку ОК и с помощью окна Properties установите свойство DialogResult равным ОК. Аналогично установите свойство DialogResult кнопки Cancel равным (конечно) Cancel. Ниже вы убедитесь, что свойство DialogResult может быть довольно полезным, т.к. оно позволяет выбрать нужное действие. Вообще-то свойству DialogResult можно присвоить любое значение из перечисления DialogResult: public enum DialogResult { Abort, Cancel, Ignore, No, None, OK, Retry, Yes } На рис. А. 16 показан пример построения диалогового окна — для наглядности в нем даже добавлено несколько меток. 1 GrderAutoDwlogxs [Design) X | OrderAutoOialcg Make Color Price ".HVi'iT'ra r> 3 №■■ n OK и n Pi и Т&ШИ&. Cancel ] Рис. А.16. Тип OrderAutoDialog
1352 Часть VIII. Приложения Настройка порядка переходов по клавише <ТаЬ> Ну вот, наше диалоговое окно уже выглядит довольно привлекательно. Следующий шаг — формализация порядка переходов при нажатии клавиши <ТаЬ>. Вы уже знаете, что многие пользователи привыкли перемещать фокус ввода с помощью клавиши <ТаЬ>, если форма содержит несколько графических элементов. Настройка последовательности переходов требует знакомства с двумя свойствами: TabStop и Tablndex. Свойство TabStop может принимать значения true или false, в зависимости от того, хотите ли вы, чтобы данный элемент получал фокус с помощью клавиши <ТаЬ>. Если свойство TabStop для какого-то элемента равно true, то для этого элемента можно установить свойство TabOrder, чтобы задать порядок активации в последовательности переходов (нумерация с нуля), например: // Настройка свойств перехода при нажатии <ТаЬ>. txtMake.Tablndex = 0; txtMake.TabStop = true; Мастер порядка переходов Свойства TabStop и Tablndex можно задавать вручную с помощью окна Properties, однако в IDE Visual Studio 2010 имеется мастер порядка переходов (lab Order Wizard), который вызывается пунктом меню View^Tab Order (Вид^Порядок переходов). Учтите, что этот пункт доступен только если активен конструктор Forms. После активации для каждого графического элемента формы выводится текущее значение Tablndex. Чтобы изменить эти значения, щелкните на каждом элементе в той очередности, в которой вы хотите выполнять переходы при нажатии клавиши <ТаЬ> (рис. А. 17). Рис. А. 17. Мастер порядка переходов Для выхода из мастера нажмите клавишу <Esc>. Задание кнопки по умолчанию для формы Во многих формах, требующих от пользователя ввода каких-то данных (особенно в диалоговых окнах) имеется специальная кнопка, которая срабатывает при нажатии клавиши <Enter>. Допустим, что при нажатии пользователем клавиши <Enter> нужно вызывать обработчик события Click для кнопки btnOK. Для этого достаточно установить свойство AcceptButton следующим образом (это же можно сделать и с помощью окна Properties):
Приложение А. Программирование с помощью Windows Forms 1353 public partial class OrderAutoDialog : Form { public OrderAutoDialog () { InitializeComponent(); // Нажатие клавиши <Enter> - как бы щелчок на кнопке btnOK. this.AcceptButton = btnOK; } } На заметку! В некоторых формах бывает нужно имитировать щелчок на кнопке Cancel при нажатии пользователем клавиши <Esc>. Это можно сделать, присвоив свойству CancelButton формы объект Button, который соответствует щелчку на кнопке Cancel. Отображение диалоговых окон При работе с диалоговыми окнами первым делом надо решить, открывать ли их в модальном или немодалъном режиме. Вы, видимо, знаете, что модальные диалоговые окна должны быть закрыты пользователем, прежде чем он сможет вернуться в окно, из которого первоначально было открыто данное диалоговое окно. Примером модального окна может служить большинство окон "О программе". Чтобы открыть новое диалоговое окно, нужно вызвать метод ShowDialogO из объекта этого диалогового окна. А для вывода немодального диалогового окна предназначен метод Show(), который позволяет пользователю переключаться между диалогом и главным окном (как диалог "Найти/ заменить"). Сейчас мы изменим в нашем примере обработчик пункта меню Tools'^ Order Automobile... для типа MainWindow, чтобы объект OrderAutoDialog выводился в модальном режиме. Вот первоначальный код: private void orderAutomobileToolStripMenuItem_Click(object sender, EventArgs e) { // Создание объекта диалога. OrderAutoDialog dig = new OrderAutoDialog(); // Вывод в виде модального диалогового окна и определение, на какой кнопке // был выполнен щелчок, с помощью возвращаемого значения DialogResult. if (dig.ShowDialogO == DialogResult.OK) { // Пользователь щелкнул на OK, и надо срочно что-то делать... } } На заметку! Методы ShowDialogO и Show() можно вызывать, указывая объект, представляющий владельца диалогового окна (для формы, загружающей диалоговое окно, это this). Указание владельца диалогового окна устанавливает z-упорядочение форм, а заодно гарантирует (в случае немодального диалога), что при закрытии главного окна будут закрыты и все принадлежащие ему окна. Не забывайте, что при создании экземпляра типа, порожденного от Form (в данном случае OrderAutoDialog), окно не отображается на экране, а лишь размещается в памяти. А видимым оно становится лишь после вызова Show() или ShowDialogO. Кроме того, учтите, что метод ShowDialogO возвращает значение DialogResult, назначенное одной из кнопок (метод Show() возвращает просто void).
1354 Часть VIII. Приложения После возврата из метода ShowDialogO форма не отображается на экране, но по- прежнему находится в памяти. Это означает, что можно извлечь значения из каждого элемента Text В ох. Однако при компиляции следующего кода будут выданы ошибки компиляции: private void orderAutomobileToolStripMenuItem_Click(object sender, EventArgs e) { // Создание объекта диалога. OrderAutoDialog dig = new OrderAutoDialog(); // Вывод в виде модального диалогового окна и определение, на какой кнопке // был выполнен щелчок, с помощью возвращаемого значения DialogResult. if (dig.ShowDialogO == DialogResult.OK) { // Хочется получить значения из текстовых полей? Ошибка компиляции! string orderlnfo = string.Format("Марка: {0}, Цвет: {1}, Цена: {2}", dig.txtMake.Text, dig.txtColor.Text, dig.txtPrice.Text); MessageBox.Show(orderlnfo, "Информация о вашем заказе"); } } Ошибка компиляции возникает из-за того, что Visual Studio 2010 объявляет элементы, добавляемые на конструктор Forms, как приватные переменные-члены класса. Можете убедиться в этом сами, открыв файл OrderAutoDialog.Designer.cs. Если действовать по всем правилам, то диалоговое окно должно сохранить инкапсуляцию с помощью добавления общедоступных свойств для установки и получения значений в текстовых полях, но можно сделать проще: переопределить их с ключевым словом public. Для этого выберите в конструкторе каждый элемент TextBoxn установите его свойство Modifiers равным Public (с помощью окна Properties). После этого соответствующий код конструктора будет выглядеть так: partial class OrderAutoDialog { // Переменные-члены формы определены в файле, который сопровождает конструктор, public System.Windows.Forms.TextBox txtMake; public System.Windows.Forms.TextBox txtColor; public System.Windows.Forms.TextBox txtPrice; } Теперь можно откомпилировать и запустить приложение. После вывода диалогового окна и щелчка на кнопке О К вы увидите входные данные, которые отображались в окне сообщения. Наследование форм До сих пор все пользовательские окна, обычные и диалоговые, рассмотренные в данной главе, были порождены непосредственно от класса System.Windows.Forms.Form. Однако одним из интересных аспектов Windows Forms является то, что типы Form могут служить базовыми классами для порождения других Form. Предположим, к примеру, что вы создали кодовую библиотеку .NET, которая содержит все основные диалоговые окна компании. Но потом вы решили, что окно "О программе" постновато и в него неплохо добавить трехмерный логотип компании. В этом случае можно не пересоздавать заново все окно, а расширить базовое окно "О программеи, унаследовав прежний внешний вид: // ThreeDAboutBox "является" AboutBox public partial class ThreeDAboutBox : AboutBox { // Код отображения логотипа компании... }
Приложение А. Программирование с помощью Windows Forms 1355 Чтобы наглядно увидеть наследование форм, вставьте в свой проект новую форму — с помощью пункта меню Project^Add (ПроектеДобавить) Windows Forms. Но на этот раз выберите значок Inherited Form (Наследуемая форма) и назовите новую форму ImageOrderAutoDialog.cs (рис. А. 18). i - CarOrderApp Installed Templates л Visual С* hems Code Data General Web WPF Report Workflow E Sort by Default H inherited Form N £ Inherited User Control J About Box j Windows Form T\ MDI Parent Form jpj Custom Control I User Control Visual C» hems Visual C* hems Visual C* hems Visual С «hem 5 Visual C* hems VrsuaiCOems Visual C# hems Type Visual C* hems w form based on an existing Windows Рис. А. 18. Добавление порожденной формы в проект При выборе этого варианта открывается диалоговое окно Inheritance Picker (Выбор наследования), в котором выводятся все формы из текущего проекта. Кнопка Browse (Обзор) позволяет выбрать форму из внешней сборки .NET. В нашем случае просто выберите класс OrderAutoDialog. На заметку! Чтобы формы проекта отображались в диалоговом окне, необходимо хотя бы раз откомпилировать проект, т.к. для отображения вариантов это средство использует метаданные сборки. После щелчка на кнопке О К средство визуального проектирования выводит все базовые элементы управления на их родительских элементах, и у каждого элемента слева вверху имеется значок — стрелочка, означающая наследование. Чтобы завершить построение порожденного диалогового окна, найдите элемент PictureBox в разделе Common Controls (Общие элементы) окна Toolbox (Инструментарий) и добавьте его на порожденную форму. Затем с помощью свойства Image выберите нужный файл с изображением. На рис. А. 19 показан один из возможных UI — стилизованное изображение руки, ведущей антикварный автомобиль (полная развалюха!). ImageOrder AutoOialogcs [Design] X| 1 OrderAutoDialog ®ake Щ \ Ш* Ш" $hce Щ е_ок_ !■£■) ■~г --J-> Г»^» 1 JpCanMl | , Рис. А.19. Интерфейс с использованием класса ImageOrderAutoDialog
1356 Часть VIII. Приложения Теперь можно изменить обработчик события Click для пункта меню Tools^Order Automobile... (СредствамЗаказ автомобиля...), чтобы создать экземпляр порожденного типа, а не базового класса OrderAutoDialog: private void orderAutomobileToolStripMenuItem_Click(object sender, EventArgs e) { // Создание объекта порожденного диалога. ImageOrderAutoDialog dig = new ImageOrderAutoDialog(); Исходный код. Проект CarOrderApp находится в подкаталоге Appendix A. Вывод графических данных с помощью GDI+ Многие приложения с графическим пользовательским интерфейсом могут динамически генерировать графические данные, которые затем отображаются на поверхности окна. Допустим, например, что из реляционной базы данных выбран набор записей, и на их основе нужно вывести круговую (или столбчатую) диаграмму, которая наглядно показывает наличие товаров на складе. Или, может быть, вы захотите создать старую видеоигру, но уже на платформе .NET. Независимо от цели, API под названием GDI+ позволяет отображать графические данные в приложениях Windows Forms. Эта технология сконцентрирована в сборке System.Drawing.dll, которая определяет несколько пространств имен (рис. А.20). Browse NET Framework 4 3 System.DirectcryServices } System,DirectoryServices.AccountManagement 1 System.DirectoryServices.ProtoccIs {) System.Drawing О System.Drawing.Design {} System.Drawing.Drawing2D {} System.Drawing.lmaging {> System,Drawing.Printing {} System.Drawing.Text » System.Drawing.Design Assembly System.Drawing Member of .NET Framework 4 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework \.NETFrameworfry4.0\System.Drawing.dll Л Attributes: |[System.Runtime.CompilerSer\'ices.Dependenc)'Attnbute("System.", 1) Рис. А.20. Пространства имен сборки System.Drawing.dll На заметку! Дружеское напоминание: в WPF имеется своя собственная подсистема и API графического отображения; GDI+ требуется только в приложениях Windows Forms. В табл. АЛО приведены высокоуровневые пояснения ролей основных пространств имен GDI+. Пространство имен System.Drawing Большинство типов, необходимых для программирования GDlH-приложений, находятся в пространстве имен System.Drawing. В нем можно найти классы, которые представляют изображения, кисти, перья и шрифты. Кроме того, System.Drawing определяет несколько вспомогательных типов, таких как Color, Point и Rectangle. В табл. А. 11 приведены некоторые (но не все) основные типы.
Приложение А. Программирование с помощью Windows Forms 1357 Таблица А. 10. Основные пространства имен GDI+ Пространство имен Назначение System.Drawing System.Drawing. Drawing2D System.Drawing.Imaging System.Drawing. Printing System.Drawing.Text Основное пространство имен GDI+, которое определяет многочисленные типы для обычного отображения (шрифты, перья и простые кисти), а также всемогущий тип Graphics Содержит типы для более сложных двумерных и векторных графических операций (градиентные кисти, кончики пера и геометрические преобразования) Определяет типы для работы с графическими изображениями (изменение палитры, извлечение метаданных об изображении, обработка метафайлов и т.д.) Определяет типы для вывода изображений на печатные страницы, взаимодействия с принтерами и форматирования общего вида для задания печати Позволяет работать с коллекциями шрифтов Таблица А.11. Основные типы пространства имен System.Drawing Тип Назначение Bitmap Brush Brushes SolidBrush SystemBrushes TextureBrush BufferedGraphics Color SystemColors Font FontFamily Graphics Icon Systemlcons Image Image Animator Pen Pens SystemPens Point PointF Инкапсулирует данные изображения (*.bmp или другие) Объекты кистей используются для заполнения внутренностей графических фигур, таких как прямоугольники, эллипсы и многоугольники Предоставляет графический буфер для двойной буферизации, которая нужна для снижения или устранения мерцания вследствие перерисовки поверхности элемента Определяют ряд статических свойств только для чтения, которые позволяют получить конкретные цвета для создания различных перьев и/или кистей Тип Font инкапсулирует характеристики данного шрифта (имя, плотность, наклон и размер в пунктах). FontFamily предоставляет абстракцию для группы шрифтов схожего вида, но с некоторыми расхождениями в стиле Основной класс, представляющий поверхность для рисования, а также несколько методов для прорисовки текста, изображений и геометрических фигур Представляют пользовательские значки и набор стандартных значков, поддерживаемых системой Image — абстрактный базовый класс, содержащий функции для типов Bitmap, Icon и Cursor. ImageAnimator предоставляет возможность перебора нескольких типов-наследников Image через заданный промежуток времени Объекты, используемые для рисования прямых и кривых линий. Тип Реп определяет несколько статических свойств, которые возвращают новый объект Реп заданного цвета Структуры, представляющие отображение координат (х, у) на соответствующие целые и дробные значения
1358 Часть VIII. Приложения Окончание табл. А. 11 Тип Назначение Rectangle RectangleF Size SizeF StringFormat Region Структуры, представляющие размеры прямоугольника (также отображаются на соответствующие целые и дробные значения) Структуры, представляющие высоту и ширину (также отображаются на соответствующие целые и дробные значения) Инкапсулирует различные характеристики текстовой компоновки (например, выравнивание и промежутки между строками) Описывает внутренность геометрической фигуры, составленной из прямоугольников и ломаных линий Назначение типа Graphics Класс System.Drawing.Graphics служит шлюзом для функциональности прорисовки в GDI+. Он не только представляет поверхность для рисования (поверхность формы, управляющего элемента или области памяти), но и определяет десятки членов, позволяющих выводить текст, изображения (например, значки и битовые изображения), а также различные геометрические фигуры. Частичный список членов класса приведен в табл. А. 12. Таблица А. 12. Некоторые члены класса Graphics Метод Назначение FromHdcO FromHwndO FromImage() Clear () DrawArcO DrawBeziersO DrawCurveO DrawEllipseO DrawIcon() DrawLineO DrawLines() DrawPieO DrawPathO DrawRectangle () DrawRectangles () DrawStringO FillEllipseO FillPieO FillPolygon() FillRectangle() FillPathO Статические методы, определяющие способ получения объекта Graphics из данного изображения (к примеру, значка или битового изображения) или графического элемента Заполняет объект Graphics указанным цветом, стерев при этом текущую поверхность рисования Методы для прорисовки заданного изображения или геометрической фигуры. Все методы DrawXXXO используют объекты Реп из GDI+ Методы для заполнения внутренности заданной геометрической фигуры. Все методы FillXXXO используют объекты Brush из GDI+ Экземпляры класса Graphics невозможно создать напрямую с помощью ключевого слова new, т.к. у этого класса нет общедоступных определенных конструкторов. А как же получить объект Graphics? Хороший вопрос!
Приложение А. Программирование с помощью Windows Forms 1359 Получение объекта Graphics с помощью события Paint Чаще всего объект Graphics получают с помощью окна Properties (Свойства) Visual Studio 2010: в нем можно задать обработчик события Paint для окна, на котором нужно выполнить прорисовку. Это событие определено через делегат Paint Event Handle г, который может указывать на любой метод, принимающий в качестве первого параметра System.Object, а в качестве второго — PaintEventArgs. Параметр PaintEventArgs содержит объект Graphics, необходимый для прорисовки на поверхности формы. Для примера создайте новый проект Windows Application с именем PaintEventApp. Потом с помощью Solution Explorer смените имя первоначального файла Form.cs на MainWindow.cs, а затем с помощью окна Properties создайте обработчик события Paint. При этом будет сгенерирована следующая заглушка: public partial class MainWindow : Form { public MainWindow () { InitializeComponent (); } private void MainWindow_Paint(object sender, PaintEventArgs e) { // Здесь мог бы быть ваш код прорисовки. } } Ну хорошо, обработчик события Paint есть, а когда возникает это событие? Оно возникает, когда окно становится гр5\зным (dirty) — т.е. когда изменяется его размер, когда его перестает загораживать (частично или полностью) другое окно или когда оно было свернуто, а затем развернуто. Во всех этих случаях — т.е. когда форму необходимо перерисовать — платформа .NET автоматически вызывает обработчик события Paint. Рассмотрим следующую реализацию обработчика MainWindow _ Paint (): private void MainWindow_Paint(object sender, PaintEventArgs e) { // Получение объекта Graphics для данной Form. Graphics g = e.Graphics; // Прорисовка окружности. g.FillEllipse(Brushes.Blue, 10, 20, 150, 80); // Вывод строки заданным шрифтом. g. Drawstring("Hello GDI + ", new Font("Times New Roman", 30), Brushes.Red, 200, 200); // Вывод линии заданным пером. using (Pen p = new Pen(Color.YellowGreen, 10)) { g.DrawLine(p, 80, 4, 200, 200); } } После получения объекта Graphics из входного параметра PaintEventArgs вызывается метод FillEllipse(). Этот метод (как и любой другой метод с именем, начинающимся на Fill) требует в качестве первого параметра тип, порожденный от Brush. В принципе, можно самостоятельно создать любое количество интересных объектов кистей из пространства имен System.Drawing.Drawing2D (например, HatchBrush и LinearGradientBrush), но, кроме того, имеется вспомогательный класс Brushes, который предоставляет удобный доступ к множеству типов кистей со сплошным цветом.
1360 Часть VIII. Приложения Затем вызывается метод DrawStringO, которому в первом параметре передается выводимая строка. Для этого в GDI+ имеется тип Font, который представляет не только имя шрифта для отображения текстовых данных, но и характеристики этого шрифта, например, размер в пунктах (в данном случае 30). Методу DrawStringO также нужен тип Brush — ведь с точки зрения GDI+ строка "Hello GDI+" представляет собой просто набор'геометрических фигур, которые нужно вывести и залить на экране. В конце вызывается метод DrawLine(), который выводит прямую линию с помощью специального типа Реп шириной 10 пикселей. Результат работы этого кода показан на рис. А.21. Рис. А.21. Простые операции отображения с помощью GDI+ На заметку! В приведенном выше коде объект Реп освобождается явным образом. Как правило, при непосредственном создании типа GDI+, который реализует интерфейс IDisposable, сразу после окончания работы с данным объектом необходимо вызвать метод Dispose (). Это позволяет как можно раньше освободить занятые ресурсы. В противном случае ресурсы в конце концов будут освобождены с помощью сборщика мусора — когда он сочтет нужным. Актуализация клиентской области формы Во время работы приложения Windows Forms может понадобиться явно сгенерировать в коде событие Paint, а не ждать, пока окно станет естественно грязнымв результате действий пользователя. К примеру, программа может предоставлять пользователю на выбор несколько заготовленных изображений с помощью специального диалогового окна. После закрытия диалога выбранное изображение необходимо вывести в клиентской области формы. Понятно, что если ожидать естественного загрязнения формы, то пользователь не увидит никаких изменений, пока не изменит размер окна или не прикроет-откроет его другим окном. Чтобы программным образом выполнить перерисовку окна, необходимо вызвать унаследованный метод Invalidate(): public partial class MainForm: Form { private void MainForm_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; // Здесь выводится первоначальное изображение. }
Приложение А. Программирование с помощью Windows Forms 1361 private void GetlmageFromDialog () { // Вывод диалогового окна и получение нового изображения. // Перерисовка всей клиентской области. Invalidate() ; Метод Invalidate() имеет ряд перегруженных вариантов. Это позволяет указать конкретную прямоугольную часть формы, которую нужно перерисовать, чтобы не выполнять перерисовку всей клиентской области (по умолчанию). Если нужно обновить только небольшой прямоугольник в верхнем левом углу клиентской области, то можно написать примерно такой код: // Перерисовка заданной прямоугольной области формы. private void UpdateUpperArea() { Rectangle myRect = new Rectangle@, 0, 75, 150); Invalidate(myRect) ; Исходный код. Проект PaintEventApp находится в подкаталоге Appendix A. Создание полного приложения Windows Forms В заключение данного вводного обзора Windows Forms API и GDI+ API мы создадим полное приложение с графическим интерфейсом, в котором будут продемонстрированы некоторые описанные ранее приемы. Это будет примитивная программа для рисования, которая позволяет пользователям выбрать одну из двух фигур (круг или прямоугольник) и цвет для ее отображения на форме. Конечные пользователи смогут также сохранять свои творения в локальном файле на жестком диске с помощью службы сериализации объектов. Создание главной системы меню Вначале создайте новое приложение Windows Forms с именем MyPaintProgram и смените первоначальное имя файла For ml. с s на MainWindow.cs. Потом создайте в пустом окне систему меню с пунктом File (Файл), в котором имеются подпункты Save... (Сохранить), Load... (Загрузить) и Exit (Выход) (рис. А.22). 1 £.le III I Save... Load... Ex.t Г~ТУР у ' и ut sJHwe^J Щ menuStripl Рис. А.22. Система меню File
1362 Часть VIII. Приложения На заметку! Указание в качестве пункта меню одного дефиса (-) позволяет вставлять разделители в систему меню. После этого создайте еще один пункт меню верхнего уровня Tools (Сервис), который содержит подпункты для выбора выводимой фигуры и ее цвета, а также для очистки формы от всех графических данных (рис. А.23). Рис. А.23. Система меню Tools Теперь нужны обработчики события Click для каждого из этих подпунктов меню. Все эти обработчики будут создаваться по мере проработки примера, а пока можно заполнить обработчик для пункта File^Exit с помощью вызова метода Close(): private void exitToolStripMenuItem_Click(object sender, EventArgs e) { Close () ; } Определение типа ShapeData Данное приложение должно позволять конечным пользователям выбирать одну из двух предопределенных фигур и ее цвет. Кроме того, конечный пользователь должен иметь возможность сохранять свои графические данные в файле, поэтому нужно определить специальный класс, который инкапсулирует все необходимые детали. Для простоты мы сделаем это с помощью автоматических свойств С# (см. главу 5). Вначале добавьте в проект ShapeData.cs новый класс и реализуйте нужный тип следующим образом: [Serializable] class ShapeData { // Верхняя левая координата фигуры. public Point UpperLeftPoint { get; set; } // Текущий цвет фигуры. public Color Color { get; set; } // Вид фигуры. public SelectedShape ShapeType { get; set; } } Здесь класс ShapeData использует автоматические свойства, которые инкапсулируют различные типы данных, два из которых (Point и Color) определены в пространстве имен System. Drawing, поэтому не забудьте импортировать это пространство имен в ко-
Приложение А. Программирование с помощью Windows Forms 1363 довый файл. Обратите внимание, что у данного типа имеется атрибут [Serializabie]. Ниже тип MainWindow будет настроен для использования списка типов ShapeData, который будет храниться с помощью службы сериализации объектов (см. главу 20). Определение типа ShapePickerDialog Чтобы пользователь мог выбрать круг или прямоугольник, можно создать простое диалоговое окно с именем ShapePickerDialog (вставьте этот новый тип Form). Кроме традиционных кнопок ОК и Cancel (с присвоенными соответствующими значениями DialogResult), в диалоге будет находиться один элемент Group Box, содержащий два объекта RadioButton: radioButtonCircle и radioButtonRect. На рис. А.24 приведен один из вариантов построения. Рис. А.24. Тип ShapePickerDialog Теперь откройте окно с кодом для данного диалога. Для этого щелкните правой кнопкой на конструкторе Forms и выберите пункт меню View Code (Просмотр кода). В пространстве имен MyPaintProgram объявите перечисление с именем SelectedShape, определяющее имена возможных фигур: public enum SelectedShape { Circle, Rectangle } Теперь измените текущий класс ShapePickerDialog: • Добавьте автоматическое свойство типа SelectedShape. Вызывающий метод может использовать это свойство для определения, какую фигуру следует выводить. • Создайте обработчик события Click для кнопки ОК с помощью окна Properties (Свойства). • Реализуйте обработчик события, который определяет, что было выбрано переключателем (с помощью свойства Checked): круг или прямоугольник. Если круг, в свойство SelectedShape заносится значение SelectedShape.Circle, а иначе — значение SelectedShape.Rectangle. Вот полный код: public partial class ShapePickerDialog : Form { public SelectedShape SelectedShape { get; set; } public ShapePickerDialog () { InitializeComponent ();
1364 Часть VIII. Приложения private void btnOK_Click(object sender, EventArgs e) { if (radioButtonCircle.Checked) SelectedShape = SelectedShape.Circle; else SelectedShape = SelectedShape.Pectangle; } } На этом инфраструктура программы завершена. Осталось реализовать обработчики событий Click для остальных пунктов меню главного окна. Добавление инфраструктуры в тип MainWindow Вернемся к построению главного окна и добавим в нашу форму три новых пере- менных-члена. Эти переменные позволят отслеживать выбранную фигуру (с помощью перечисления SelectedShape), выбранный цвет (представленный переменной-членом System.Drawing.Color) и все выведенные изображения, которые хранятся в обобщенном списке List<T> (где Т — тип ShapeData): public partial class MainWindow : Form { // Текущие фигура и цвет для прорисовки. private SelectedShape currentShape; private Color currentColor = Color.DarkBlue; // Здесь хранятся все ShapeData. private List<ShapeData> shapes = new List<ShapeData> (); } Теперь создадим обработчики событий Mouse Down и Paint для данного типа-наследника Form с помощью окна Properties. Мы реализуем их на следующем шаге, а пока IDE просто сгенерирует следующие заглушки: private void MainWindow_Paint(object sender, PaintEventArgs e) { } private void MainWindow_MouseDown (object sender, MouseEventArgs e) { } Реализация функций меню Tools Чтобы пользователь мог установить значение переменной-члена currentShape, необходимо реализовать обработчик события Click для пункта меню Tools^Pick Shape.... Он должен открывать специальное диалоговое окно и на основании выбора пользователя устанавливать значение соответствующей переменной-члена: private void pickShapeToolStripMenuItem_Click(object sender, EventArgs e) { // Загрузка диалогового окна и установка нужного вида фигуры. ShapePickerDialog dig = new ShapePickerDialog(); if (DialogResult.OK == dig.ShowDialog() ) { currentShape = dig.SelectedShape; } }
Приложение А. Программирование с помощью Windows Forms 1365 А чтобы пользователь мог установить значение переменной-члена currentColor, необходимо реализовать обработчик события Click для пункта меню Tools^Pick Color..., в котором используется стандартный тип System.Windows.Forms.ColorDialog: private void pickCQlorToolStripMenuItem_Click(object sender, EventArgs e) { ColorDialog dig = new ColorDialog (); if (dlg.ShowDialog () == DialogResult.OK) { currentColor = dig.Color; } } Если теперь запустить полученную программу и выбрать пункт меню Tools^Pick Color..., то откроется диалоговое окно, показанное на рис. А.25. Рис. А.25. Стандартный тип ColorDialog И в завершение нужно реализовать обработчик пункта меню Tools^Clear Surface, который очищает содержимое переменной-члена List<T> и программно генерирует событие Paint с помощью вызова метода Invalidate(): private void clearSurfaceToolStripMenuItem_Click(object sender, EventArgs e) { shapes.Clear ()/ // Генерация события Paint. Invalidate(); } Захват и вывод графических данных Поскольку вызов Invalidate () генерирует событие Paint, необходимо добавить код в обработчик события Paint. В нем нужно реализовать перебор (пока пустого) списка List<T> и вывод в текущем положении курсора мыши окружности или прямоугольника. Для этого сначала нужно реализовать обработчик события MouseDown и вставить новый объект ShapeData в обобщенный список List<T>, учитывая выбранные пользователем цвет, вид и текущее положение курсора мыши: private void MainWindow_MouseDown(object sender, MouseEventArgs e) { // Создание объекта ShapeData с учетом выбора пользователя. ShapeData sd = new ShapeData ();
1366 Часть VIII. Приложения sd.ShapeType = currentShape; sd.Color = currentColor; sd.UpperLeftPoint = new Point(e.X, e.Y); // Добавление в List<T> и перерисовка формы. shapes.Add(sd); Invalidate(); } И теперь можно реализовать обработчик события Paint: private void MainWindow_Paint(object sender, PaintEventArgs e) { // Получение объекта Graphics для текущего окна. Graphics g = e.Graphics; // Прорисовка каждой фигуры заданным цветом. foreach (ShapeData s in shapes) { // Вывод квадрата или круга размером 20x20 пикселей, // используя нужный цвет. if (s.ShapeType == SelectedShape.Rectangle) g.FillReсtangle(new SolidBrush(s.Color), s.UpperLeftPoint.X, s.UpperLeftPoint.Y, 20, else g.FillEllipse(new SolidBrush (s.Color), s.UpperLeftPoint.X, s.UpperLeftPoint.Y, 20, 20) ; 20) ; } } Если теперь снова запустить приложение, вы сможете нарисовать любое количество фигур произвольных цветов (см. рис. А.26). РННПИ11 suiting Program File Tccls В @] Рис. А.26. Работа приложения MyPaintProgram Разработка кода сериализации И последнее, что нужно сделать в нашем проекте — реализовать обработчики событий для пунктов меню File^Save... и File^Load.... Поскольку класс ShapeData снабжен атрибутом [Serializable] (а класс List<T> сериализуем сам по себе), можно сохранить текущие графические данные с помощью имеющегося в Windows Forms типа SaveFileDialog. Но перед этим нужно указать в директивах using, что будут использоваться пространства имен System.Runtime.Serialization.Formatters.Binary и System.10:
Приложение А. Программирование с помощью Windows Forms 1367 // Для двоичного форматирования. using System.Runtime.Serialization.Formatters.Binary; using System.10; Теперь измените обработчик пункта меню File^Save... следующим образом: private void saveToolStripMenuItem_Click (object sender, EventArgs e) { using (SaveFileDialog saveDlg = new SaveFileDialog()) { // Настройка внешнего вида для диалога сохранения файла. saveDlg.InitialDirectory = " . " ; saveDlg.Filter = "Shape files (*.shapes)|*.shapes"; saveDlg.RestoreDirectory = true; saveDlg.FileName = "MyShapes"; // Если пользователь щелкнул на кнопке OK, // открываем новый файл и сериализуем List<T>. if (saveDlg.ShowDialog() == DialogResult.OK) { Stream myStream = saveDlg.OpenFile() ; if ((myStream != null)) { // Сохранение фигур. BinaryFormatter myBinaryFormat = new BinaryFormatter (); myBinaryFormat.Serialize(myStream, shapes); myStream.Close(); } } } } Обработчик события File^Load открывает указанный файл и десериализует данные в переменную-член List<T>. Для этого используется имеющийся в Windows Forms тип OpenFileDialog: private void loadToolStripMenuItem_Click(object sender, EventArgs e) { using (OpenFileDialog openDlg = new OpenFileDialog ()) { openDlg.InitialDirectory = " . " ; openDlg.Filter = "Shape files (*.shapes)|*.shapes"; openDlg.RestoreDirectory = true; openDlg.FileName = "MyShapes"; if (openDlg.ShowDialog() == DialogResult.OK) { Stream myStream = openDlg.OpenFile() ; if ((myStream != null)) { // Получение фигур. BinaryFormatter myBinaryFormat = new BinaryFormatter(); shapes = (List<ShapeData>)myBinaryFormat.Deserialize(myStream); myStream.Close (); Invalidate (); } } } } Если вы внимательно прочли главу 20, то общая логика сериализации должна быть вам знакома. Оба диалога SaveFileDialog и OpenFileDialog имеют свойство Filter
1368 Часть VIII. Приложения (Фильтр), которому можно присвоить не очень понятное строковое значение. Этот фильтр управляет рядом параметров для диалоговых окон сохранения и открытия файлов — в частности, расширением имени файла (*. shapes). Свойство FileName позволяет задать имя сохраняемого файла по умолчанию — в нашем случае это MyShapes. Вот и все, наше приложение рисования фигур завершено. Теперь вы можете сохранять свои графические данные в любом количестве файлов *. shapes и загружать их снова. Можно усовершенствовать данную программу: добавить дополнительные фигуры или позволить пользователю управлять размером и формой фигур, а также выбирать формат сохраняемых данных (например, двоичный, XML или SOAP — см. главу 20). Резюме В данной главе рассмотрен процесс построения традиционных пользовательских приложений с помощью API Windows Forms и GDI+, которые входят в состав .NET Framework, начиная с версии 1.0. Минимальное приложение Windows Forms состоит из расширения типа Form и метода Main(), который взаимодействует с типом Application. Если на формах нужно разместить элементы пользовательского интерфейса (например, системы меню и графические элементы ввода данных), это можно сделать, добавляя новые объекты в унаследованную коллекцию Controls. В данной главе также показано, как реагировать на события мыши, клавиатуры и прорисовки. Заодно вы познакомились с типом Graphics и множеством способов генерации графических данных во время выполнения приложения. Как уже было сказано, Windows Forms API (в какой-то мере) вытеснен WPF API, появившемся с выходом .NET 3.0 (о котором было рассказано в части 6 данной книги). Конечно, WPF удобна для создания навороченных пользовательских интерфейсов, однако Windows Forms API остается простейшим (и зачастую наиболее прямым) способом создания стандартных бизнес-приложений, приложений для местного использования и простых утилит для настройки различных параметров. По этим причинам в ближайшие годы Windows Forms будет входить в состав библиотек базовых классов .NET.
ПРИЛОЖЕНИЕ Б Независимая от платформы разработка . NET-приложений с помощью Mono В данном приложении вы познакомитесь с межплатформенной разработкой приложений на С# и .NET с помощью реализации .NET с открытым исходным кодом, которая называется Mono. Если вам интересно, что это значит — по-испански mono означает "обезьяна", и это указание на различные символы-талисманы платформы Mono, которые использовались первоначальными разработчиками — корпорацией Ximian. Вы познакомитесь с ролью общеязыковой инфраструктуры, общей областью применения Mono и различными средствами разработки Mono. Когда вы прочтете это приложение (и если вы разобрались в остальном материале книги), вы сможете, если сочтете нужным, самостоятельно углублять свои навыки в разработке Мопо-приложений. На заметку! В качестве подробного описания межплатформенной разработки в .NET я рекомендую книгу Марка Истона и Джейсона Кинга (Mark Easton and Jason King) Cross-Platform .NET Development: Using Mono, Portable .NET, and Microsoft .NET (Apress, 2004). Платформенная независимость .NET i Когда-то программистам, которые использовали язык разработки Microsoft (например, VB6) или среду программирования Microsoft (например, MFC, COM или ATL), приходилось ограничиваться только созданием ПО, которое (обычно) работало лишь под семейством операционных систем Windows. Многие разработчики .NET, привыкшие к такой привязке к Microsoft, бывают удивлены, когда узнают, что система .NET не зависит от платформы. Однако это так. Создавать, компилировать и выполнять сборки .NET можно под операционными системами, отличными от Microsoft. С помощью реализаций .NET с открытым исходным кодом наподобие Mono .NET- приложения могут спокойно работать под многими другими операционными системами, в число которых входят Mac OS X, Solaris, AIX и многочисленные разновидности Unix/ Linux. Mono даже содержит установочный пакет для (кто бы мог подумать?) Microsoft Windows. Таким образом, можно создавать и выполнять сборки .NET под операционной системой Windows, даже не устанавливая Microsoft .NET Framework 4.0 SDK или Visual Studio 2010 IDE.
1370 Часть VIII. Приложения На заметку! Все же учтите, что лучшими средствами создания программ .NET для семейства операционных систем Windows являются Microsoft .NET Framework 4.0 SDK и Visual Studio 2010. Даже когда разработчики узнают о межплатформенных возможностях кода .NET, они часто считают, что область платформенно-независимой разработки .NET-приложений ограничена консольными приложениями на уровне "Hello world". Однако на самом деле они могут создавать работоспособные сборки, в которых используются ADO.NET, Windows Fbrms (в дополнение к альтернативным пакетам создания GUI наподобие GTK# и Сосоа#), ASP.NET, LINQ и веб-службы на базе XML, задействуя многие основные пространства имен и языковые возможности, которые были продемонстрированы в данной книге. Роль CLI Реализация межплатформенных возможностей .NET отличается от подхода Sun Microsystems с платформой программирования на Java. В отличие от Java, Microsoft не разрабатывает программы установки .NET для Mac, Linux и т.д. Вместо этого Microsoft выпустила набор формализованных спецификаций, которые могут использоваться другими субъектами в качестве дорожной карты для создания дистрибутивов .NET для их платформ. Все вместе эти спецификации называются общеязыковой инфраструктурой (Common Language Infrastructure — CLI). Как было кратко сказано в главе 1, при выпуске С# и платформы .NET компания Microsoft выпустила две формальные спецификации для ЕСМА (European Computer Manufacturers Association — Ассоциация европейских производителей компьютеров), предназначенные для всеобщего пользования. После утверждения Microsoft отправила эти спецификации в Международную организацию по стандартизации (International Organization for Standardization — ISO), где они были вскоре утверждены. Какая польза от этого нам? Эти две спецификации предоставляют дорожную карту для компаний, разработчиков, университетов и других организаций, которые хотят создавать самостоятельно разработанные дистрибутивы языка программирования С# и платформы .NET. Упомянутые спецификации — это: • ЕСМА-334 — определяет синтаксис и семантику языка программирования С#, • ЕСМА-335 — определяет многие детали платформы .NET, которые все вместе называются общеязыковой инфраструктурой. Спецификация ЕСМА-334 определяет лексику и грамматику С# очень формализованным научным способом (понятно, что такой уровень детализации весьма важен для тех, кто хочет реализовать компилятор для С#). ЕСМА-335 имеет гораздо больший объем и поэтому поделена на шесть разделов (см. табл. Б.1). Таблица Б.1. Разделы спецификации ЕСМА-335 Раздел ЕСМА-335 Назначение I. Концепции и архитектура Описание общей архитектуры CLI: правила системы общих типов, спецификации общего языка и принципы работы механизма времени выполнения .NET II. Определение метаданных Подробное описание формата метаданных .NET и семантики III. Набор инструкций CIL Описание синтаксиса и семантики общего промежуточного языка программирования (CIL)
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1371 Окончание табл. Б. 1 Раздел ЕСМА-335 Назначение IV. Профили и библиотеки Высокоуровневый обзор минимального и полного набора библиотек классов, которые должны поддерживаться дистрибутивом .NET, совместимым с CLI V. Форматы обмена Описание переносимого формата обмена отладочной информа- отладочной информацией цией (CILDB). Переносимые файлы CILDB обеспечивают стандартный способ обмена отладочной информацией между производителями и потребителями CLI VI. Приложения Разнообразные статьи, которые проясняют темы наподобие рекомендаций по созданию библиотек классов и подробностей реализации компилятора CIL В данном приложении мы не будем вдаваться в подробности спецификаций ЕСМА-334 и ЕСМА-335 — да вы и не должны разбираться во всех тонкостях этих документов, чтобы уметь создавать платформенно-независимые сборки .NET. Но если вам интересна данная тема, вы можете свободно загрузить обе спецификации с веб-сайта ЕСМА (http://www.ecma-international.org/publications/standards). Главные дистрибутивы CLI На данный момент помимо CLR, разработанной Microsoft, существуют две главные реализации CLI— это Microsoft Silverlight и Microsoft NET Compact Framework (см. табл. Б.2). Таблица Б.2. Главные дистрибутивы .NET CLI Дистрибутив CLI Веб-сайт поддержки Назначение Mono www.mono-project.com Дистрибутив .NET с открытым исходным кодом и коммерческой поддержкой, спонсируемый корпорацией Novell. Предназначен для работы под множеством популярных разновидностей Unix/Linux, Mac OS X, Solaris и Windows Portable .NET www.dotgnu.org Распространяется по универсальной общественной лицензии GNU. Как понятно из названия, Portable .NET предназначен для работы на множестве разнообразных операционных систем и архитектур, в том числе и таких экзотических, как BeOS, Microsoft Xbox и Sony PlayStation (последние две — это не шутка!) Каждая из перечисленных в табл. Б.2 реализаций СП содержит полнофункциональный компилятор С#, многочисленные средства разработки уровня командной строки, реализацию глобального кэша сборок (GAC), примеры кода, полезную документацию и десятки сборок, которые составляют библиотеки базовых классов. Кроме реализаций основных библиотек, определенных в разделе IV спецификации ЕСМА-335, в Mono и Portable .NET имеются совместимые с Microsoft реализации библиотек mscorlib.dll, System.Core.dll, System.Data.dll, System.Web.dll, System.Drawing.dll и System.Windows.Forms.dll (а также многих других). В состав дистрибутивов Mono и Portable .NET входит также ряд сборок, предназначенных конкретно для работы с операционными системами Unix/ Linux и Mac OS X.
1372 Часть VIII. Приложения Например, Сосоа# — оболочка .NET для инструментального набора Cocoa, широко применяющегося для разработки Mac OX GUI. В данном приложении будут рассматриваться только стеки программирования, не зависящие от конкретной ОС. На заметку! Здесь не будет рассмотрен дистрибутив Portable .NET, однако важно знать, что Mono не единственный платформенно-независимый дистрибутив платформы .NET, который доступен в настоящее время. Я рекомендую, кроме знакомства с платформой Mono, самостоятельно поэкспериментировать и с Portable .NET Область действия Mono Понятно, что если Mono является API-интерфейсом, построенным на существующих спецификациях ЕСМА, которые были созданы в Microsoft, то Mono постоянно обновляется с выходом новых версий платформы Microsoft .NET. На момент написания этих строк Mono совместим с С# 2008 (и ведется работа над новыми языковыми возможностями С# 2010) и .NET 2.O. Это означает, что эта технология позволяет создавать веб-сайты ASP.NET, приложения Windows Forms, приложения работы с базами данных с помощью ADO.NET и (конечно) простые консольные приложения. В настоящее время Mono не полностью совместим с новыми возможностями С# 2010 (например, с необязательными аргументами и ключевым словом dynamic), а также со всеми аспектами .NET 3.0-4.0. Это означает, что приложения Mono пока не могут (опять- таки, на момент написания этих строк) использовать следующие API-интерфейсы из Microsoft .NET: • Windows Presentation Foundation (WPF) • Windows Communication Foundation (WCF) • Windows Workflow Foundation (WF) • LINQ to Entities (хотя поддерживаются LINQ to Objects и LINQ to XML) • Все особенности языка С# 2010 Некоторые из этих API-интерфейсов могут в конечном счете войти в стеки дистрибутивов Mono. Например, на веб-сайте Mono утверждается, что последующие версии B.8-3.0) будут поддерживать особенности языка С# 2010 и платформы .NET 3.5-4.0. Кроме постоянной гонки за основными возможностями различных API-интерфейсов .NET и языка С#, Mono также предоставляет дистрибутив с открытым исходным кодом Silverlight API под названием Moonlight Он позволяет браузерам, которые работают под операционными системами на базе Linux, выполнять и использовать веб-приложения Silverlight / Moonlight. Возможно, вы знаете, что Microsoft Silverlight уже включает поддержку Mac OS X, и при наличии Moonlight API эта технология действительно стала межплатформенной. Mono также поддерживает некоторые технологии на основе .NET, у которых нет прямых аналогов от Microsoft. К примеру, Mono поставляется с GTK# — оболочкой .NET для популярной среды разработки GUI в Linux под названием GTK. Привлекательным API-интерфейсом на основе Mono является MonoTbuch, позволяющий создавать приложения для устройств Apple iPhone и ilbuch на языке программирования С#. На заметку! На веб-сайте Mono имеется страница с описанием общей функциональности Mono и планами на разработку последующих выпусков (www.mono-project.com/plans). И еще один интересный момент, касающийся возможностей Mono: подобно .NET Framework 4.0 SDK от Microsoft, Mono SDK также поддерживает несколько языков про-
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1373 граммирования для .NET. В данном приложении используется С#, однако в Mono имеется поддержка компилятора Visual Basic, а также многих других языков программирования, ориентированных на работу с .NET. Получение и установка Mono Теперь, когда вы имеете лучшее представление о возможностях платформы Mono, рассмотрим вопрос получения и установки самой Mono. Зайдите на веб-сайт Mono (www.mono-project.com) и перейдите на вкладку Download (Загрузка). Здесь доступны для загрузки различные программы установки (рис. Б.1). Рис. Б.1. Страница загрузки Mono Здесь будет описан процесс установки Windows-дистрибутива Mono (эта установка никак не повлияет на любую существующую установку Microsoft .NET IDE или Visual Studio IDE). Загрузите последний стабильный установочный пакет для Microsoft Windows и сохраните установочную программу на локальный жесткий диск. При выполнении установочной программы вы сможете установить различные средства разработки Mono, кроме стандартных библиотек базовых классов и средств программирования на С#. А именно, программа установки спросит, хотите ли вы установить GTK# (.NET GUI API с открытым исходным кодом на основе предназначенного для Linux инструментального набора GTK) и XSP (обособленный веб-сервер, подобный вебсерверу разработки ASP.NET от Microsoft — webdev.webserver.exe). Здесь мы будем считать, что вы выбрали вариант, который отмечает все пункты в сценарии установки (рис. Б.2). Структура каталога Mono По умолчанию Mono устанавливается в каталог C:\Program П1ез\Мопо-<версия> (на момент написания этих строк последней версией Mono была 2.4.2.3, но ваш номер версии почти наверняка будет другим). В этом каталоге находятся несколько подкаталогов (рис. Б.З).
1374 Часть VIII. Приложения Select the components you want to ratal: dear the components you do not want to ratal. Ctck Next when you are ready to continue. I~il«idnlrfiiiii мдляамюп piMonoRtes РЙ GTK+2.10 and Gnome 2.16 fltes - В Gfc# 2.12.9 Hea hUMonodoc \-Щ Gecko» Rtes L® Samples SiXSPftes ; BE XSP 2.0 She! Heojation N L> * 227 7 MB 121.7 MB 15.3 MB 22 MB 02 MB 2.8 MB 1.2 MB Cunent selection requires at least 351.1 MB of drak space. [ <Back ][ Next>  [~ Cancel" Рис. Б.2. Выбор варианта Full Installation (Полная установка) при установке Mono ЪШЬЛ.1" ** Pr°9ram FUes №*) И Organue ▼ Include in library ▼ . Microsoft.NET * ]| Mono-2.4.2.3 MSBuild 1 ^ NOS ■ 1 Paradox Interactive t J* PowerKO ! £) QuickTime fr i|| Reference Assemblies Г>£ Safari > it Sandcastle !> J| SharpDeveJop #. SystemRecfuirementsLab! > ||| TechSmith > Jit The Wrtcher Enhanced E<| 1 > lit THQ 0 £ Valve t> Ji Windows Defender > £ Windows Live j, Windows Live SkyDrive , V 22 items » Mono-2.42.3 ► Share with ▼ Burn \\ I bin etc It К make samples - New folder fl include H share *f Щ ieafdh Mono-.?*!,... /> ж *.• a- • J >, i! lib libexec It l> src xulrunner 1 d в в е>\ gtk-bundl GtkPbs e_214.7-20 090119 win 32.READ... а в MonoDoc MonoRelea Web seNotes |R В Xspfoul XsplocaB GtkSharp El RereaseNot es.txt mono.ico Mono ! H uninsOOO.d uninsOOO.ex 1 at e Рис. Б.З. Структура каталога Mono В данном приложении нас будут интересовать только следующие подкаталоги: • bin — содержит большинство средств разработки Mono, в том числе и компиляторы С# уровня командной строки. • lib\mono\gac — указывает на местоположение глобального кэша сборок Mono. Поскольку большинство средств разработки Mono запускаются из командной строки, вам понадобится командная строка Mono, которая автоматически распознает все такие средства. Открыть командную строку (функционально эквивалентную командной строке Visual Studio 2010) можно с помощью меню Start^AII Programs1^Mono <версия>
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1375 For Windows (Пуск^Программы^Мопо <версия> для Windows). Для проверки установки введите следующую команду и нажмите клавишу <Enter>: mono —version Если все в порядке, вы должны увидеть разнообразную информацию о среде времени выполнения Mono. Вот данные, полученные на моем компьютере с установленной платформой Mono: Mono JIT compiler version 2.4.2.3 (tarball) Copyright (C) 2002-2008 Novell, Inc and Contributors. www.mono-project.com TLS: normal GC: Included Boehm (with typed GC) SIGSEGV: normal Notification: Thread + polling Architecture: x86 Disabled: none Языки разработки Mono Как и в дистрибутиве CLR от Microsoft, Mono поставляется с несколькими компиляторами: • mcs: Компилятор С# для Mono • vbnc: Компилятор Visual Basic .NET для Mono • booc: Компилятор языка Boo для Mono • ilasm: Компиляторы CIL для Mono В данном приложении рассматриваются только компиляторы для С#, однако помните, что проект Mono содержит и компилятор для Visual Basic. Это средство пока еще в стадии разработки, и его цель — внести больше понятных (англоязычным) людям ключевых слов (наподобие Inherits, MustOverride и Implements) в мир Unix/Linux и Мае OS X (подробнее см. на странице www.mono-project.com/Visual_Basic). Boo — это объектно-ориентированный язык программирования со статическими типами для CLI, поддерживающий синтаксис на основе языка Python. Дополнительные сведения о языке программирования Boo можно найти на сайте http://boo.codehaus .org. A ilasm — компилятор CIL для платформы Mono. Работа с компилятором С# Компилятор С# для проекта Mono называется mcs, и он полностью совместим с С# 2008. Как и компилятор С# от Microsoft (csc.exe), mcs поддерживает файлы ответов, флаг /target: (для указания типа сборки), флаг /out: (для указания имени откомпилированной сборки) и флаг /reference: (для обновления манифеста текущей сборки с внешними зависимостями). Список всех опций mcs можно получить с помощью следующей команды: mcs -? Создание приложений Mono с помощью MonoDevelop После установки Mono не содержит графическую IDE. Однако это совсем не означает, что вам придется создавать все приложения Mono с помощью командной строки! Кроме базовой среды, можно дополнительно загрузить бесплатную среду MonoDevelop IDE. Как понятно из названия, MonoDevelop создана с помощью кодовой базы SharpDevelop (см. главу 2).
1376 Часть VIII. Приложения MonoDevelop IDE можно загрузить с веб-сайта Mono, и она поддерживает установку пакетов для Mac OS X, различных дистрибутивов Linux, а также (какая неожиданность!) Microsoft Windows! После установки вы обнаружите в своем распоряжении интегрированный отладчик, возможности IntelliSense и множество шаблонов проектов (наподобие ASP.NET и Moonlight). Некоторое понятие о содержимом MonoDevelop IDE можно получить на рис. Б.4. Ы» MyGtkApp - Mawcj- ■ ftle £dit ^iew Search £roject guild Sun ДМ1 loots fflindow Help Mwbn ' in Solution MyGtkApp ГД entry) (?1 В MyGtkApp If* Reference; j~« «tk-srurp ""# gdk-iheip  glade-sharp >« glib-sharp =й§ gtk-sharp «$ Mono.Posiic •0 pango-ihsrp «5» System ■■:' Ш User Interface О StocHcons У MainWrndow E) AssemWyJrfo SO M»ir>Window.« Debug|xB6 a *J MainClass class KsinClsss public static void Main («tniwjrf,] args) Application.Inlt ()_; ♦ Main(stnngll) 11 Build successful. -JCiasies SJSiCurrefrtEvent ^:Я ♦Equals ♦ Event sPending Loeded ModuV 'GVWii Ltwdcd Moduli ' j losiiedMi-c,',:- Iwied Modurt- 'C:Wi/Kia№*\*s« ..-!.-, • ■■;,- ♦ initCheckI* ♦ invoke ♦ Quit ♦ ReferenceEquals ♦ Run ♦ Runtteration lor: .! V ' "■ I ia Syst*m.Di**ing.tra' AteeKiJiilrtv.dll LlifcM* Рис. Б.4. MonoDevelop IDE Средства разработки в Mono, совместимые с Microsoft Кроме управляемых компиляторов, в состав Mono входят различные средства разработки, функционально эквивалентные свойствам, которые имеются в Microsoft .NET SDK (некоторые из них имеют те же имена). В табл. Б.З приведены соответствия между некоторыми часто используемыми утилитами Mono и Microsoft .NET. Таблица Б.З. Средства командной строки в Mono и их аналоги из Microsoft .NET Утилита Mono Утилита Microsoft .NET Назначение al gacutil Mono при запуске с опцией -aot при работающей сборке wsdl disco al.exe gacutil.exe ngen.exe wsdl.exe disco.exe Обработка манифестов сборок и создание сборок из нескольких файлов (и кое-что еще) Взаимодействие с GAC Предкомпиляция CIL-кода сборки Генерация кода для модуля доступа на стороне клиента для веб-служб на основе XML Обнаружение URL-адресов веб-служб на основе XML, которые находятся на веб-сервере xsd xsd.exe Генерация определений типов из XSD-схемы файла
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1377 Окончание табл. Б.З Утилита Mono Утилита Microsoft .NET Назначение sn monodis ilasm xsp2 sn.exe ildasm.exe ilasm.exe webdev. webserver, exe Генерация ключевых данных для строго именованной сборки Дизассемблер CIL Ассемблер CIL Веб-сервер для тестирования и разработки ACRNET-приложений Свойства разработки, имеющиеся только в Mono Кроме этого, в состав Mono входят средства разработки, для которых нет прямых эквивалентов в Microsoft .NET Framework 3.5 SDK (см. табл. Б.4). Таблица Б.4. Средства Mono, для которых нет прямых эквивалентов в Microsoft .NET SDK Средство разработки из Mono Назначение топор SQL# Glade 3 Утилита топор (mono print) выводит определение заданного типа с помощью синтаксиса С# (краткий пример приведен в следующем разделе) Mono Project поставляется с графическим интерфейсом (SQL#), который позволяет взаимодействовать с реляционными базами данных с помощью различных поставщиков данных ADO.NET IDE визуальной разработки для создания графических приложений GTK# На заметку! SQL# и Glade можно загрузить с помощью кнопки Windows Пуск из папки Applications установки Mono. Попробуйте — это наглядно демонстрирует богатство платформы Mono. Использование топор Утилита топор (от mono print — печать Mono) применяется для вывода С#-опре- деления для данного типа в указанной сборке. Понятно, что это средство может быть весьма полезным, если нужно быстро просмотреть сигнатуру какого-либо метода, вместо того чтобы искать ее в формальной документации. В качестве примера попробуйте ввести в командной строке Mono следующую команду: топор System.Object Вы увидите определение для хорошо известного вам типа System.Object: [Serializable] public class Object { public Object () ; public static bool Equals (object objA, object objB); public static bool ReferenceEquals (object objA, object objB); public virtual bool Equals (object obj); protected override void Finalize (); public virtual int GetHashCode (); public Type GetType (); protected object MemberwiseClone (); public virtual string ToString ();
1378 Часть VIII. Приложения Создание приложений .NET с помощью Mono А сейчас опробуем Mono в действии. Вначале создайте кодовую библиотеку с названием CoreLibDumper.dll. Эта сборка содержит единственный тип класса с именем CoreLibDumper, который поддерживает статический метод DumpTypeToFile(). Данный метод принимает строковый параметр — полностью определенное имя любого типа из mscorlib.dll — и получает соответствующую информацию о типе с помощью API-интерфейс рефлексии (см. главу 15), а затем выводит сигнатуры классов-членов в локальный файл на жестком диске. Создание кодовой библиотеки Mono Создайте на диске С: новую папку с именем MonoCode. В этой папке создайте подпап- ку с именем CorLibDumper, а в ней следующий С#-файл с именем CorLibDumper.cs: // CorLibDumper.cs using System; using System.Reflection; using System.10; // Определение версии сборки, [assembly:AssemblyVersion (.0.0.0")] namespace CorLibDumper { public class TypeDumper { public static bool DumpTypeToFile (string typeToDisplay) { // Попытка загрузки типа в память. Type theType = null; try { // Второй параметр GetTypeO указывает, нужно ли // генерировать исключение, если тип не найден. theType = Type.GetType(typeToDisplay, true); } catch { return false; } // Создание локального файла *.txt. using(StreamWriter sw = File.CreateText(string.Format("{0}.txt", theType.FullName))) { // Выгрузка типа в файл. sw.WriteLine("Type Name: {0}", theType.FullName); sw.WriteLine("Members:"); foreach(Memberlnfо mi in theType.GetMembers()) sw.WriteLine ("\t-> {0}", mi.ToString ()); } return true; } } } Как и компилятор С# от Microsoft, компиляторы из Mono поддерживают использование файлов ответов (см. главу 2). Можно откомпилировать этот файл, указав вручную все нужные аргументы в командной строке, но проще создать новый файл с именем LibraryBuild.rsp (в том же каталоге, что и CorLibDumper.cs) со следующим набором команд:
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1379 /target:library /out:CorLibDumper.dll CorLibDumper.cs Теперь для компиляции библиотеки из командной строки нужна следующая команда: mcs @LibraryBuild.rsp Данный подход функционально эквивалентен следующему (более объемному) набору команд: mcs /target:library /out:CorLibDumper.dll CorLibDumper.es Назначение библиотеке CoreLibDumper. dll строгого имени В Mono поддерживается развертывание строго именованных сборок (см. глав 15) в Mono GAC. Для генерации необходимых общедоступных и приватных ключевых данных в Mono имеется утилита командной строки sn, которая работает примерно так же, как средство от Microsoft с тем же именем. Например, следующая команда генерирует новый файл *.snk (все возможные команды можно вывести с помощью опции -?): sn -k myTestKeyPair.snk Можно указать компилятору С#, чтобы он использовал эти ключевые данные для назначения строгого имени библиотеке CorLibDumper.dll, добавив в файл LibraryBuild.rsp следующие команды: /target:library /out:CorLibDumper.dll /keyf lie -.myTestKeyPair . snk CorLibDumper.cs Теперь перекомпилируйте сборку: mcs @LibraryBuild.rsp Просмотр измененного манифеста с помощью monodls Перед развертыванием сборки в Mono GAC следует ознакомиться с утилитой командной строки monodis, которая функционально эквивалентна утилите ildasm.exe от Microsoft (без графического интерфейса), monodis позволяет просматривать CIL- код, манифест и метаданные типа для указанной сборки. В нашем случае нужно просмотреть основную информацию о (уже строго именованной) сборке с помощью флага —assembly. Результат выполнения команды monodis —assembly CorLibDumper.dll показан на рис. Б.5. Манифест сборки теперь содержит общедоступный ключ, определенный в myTestKeyPair.snk. Установка сборок в Mono GAC Итак, вы уже снабдили сборку CorLibDumper.dll строгим именем, и теперь ее можно установить в Mono GAC. Как и одноименное средство от Microsoft, утилита gacutil из состава Mono может устанавливать и удалять сборки, а также выводить список сборок, установленных в кэше C:\Program Files\Mono-<BepcMH>\lib\mono\gac. Следующая команда развертывает сборку CorLibDumper.dll в GAC и регистрирует ее в компьютере как общедоступную сборку: gacutil -i CorLibDumper.dll
1380 Часть VIII. Приложения К Admtnrstra d Prompt :\CoreLibDumper>monodis —assembly sembly Table iName: CorLibDumper Hash Algoritm: 0x00008004 1.0.0.0 ■Flags: 0x00000001 Dump: 10x00000000: 00 24 00 00 04 ■0x00000010: 00 24 00 00 52 ■0x00000020: El ED 29 DC B4 10x00000030: 52 BO A8 6B ЕЕ ■0x00000040: DA El ED F7 04 ■0x00000050: A8 A3 FC D4 F9 10x00000060: 7F F9 AC 3E D6 ■0x00000070: 03 7E E5 Bl C2 ■0x00000080: 04 El F8 E8 F2 10x00000090: 19 22 32 53 21 ■Culture: |C: \Corel_ i bDumpe r>_ ■■iiiii^EftHH^^ Ю000036) 80 00 00 94 00 00 00 06 53 41 31 00 04 00 00 11 AC 44 00 ЕЕ 2В 59 64 79 98 ЗА 2D 81 F5 4B AC 11 30 5A 25 B9 C7 F2 79 Al BB 86 B5 D7 22 53 AB ЗА 4D DB ЕЕ 48 8E CB CO 45 E6 9A 26 6B 86 FD 07 D5 11 B6 23 4A A9 26 B8 8F BA 9B 03 E8 21 27 80 99 -.штштшттШшшт 02 00 00 00 00 00 IE 9F BO IE D6 2B 33 E8 OE 58 F5 OF 73 81 4A ED F9 79 07 Bl A7 EF B6 88 J Рис. Б.5. Просмотр CIL-кода, метаданных и манифеста сборки с помощью утилиты monodis На заметку! Для установки этого двоичного файла в Mono GAC обязательно используйте командную строку Mono! Если воспользоваться разработанную Microsoft программу gacutil.exe, то сборка CorLibDumper.dll будет установлена в Microsoft GAC! Если после выполнения этой команды открыть каталог \дас, то вы увидите там новую папку с именем CorLibDumper (рис. Б.6). Эта папка определяет подкаталог, который следует тем же соглашениям по именованию, что и Microsoft GAC (versionOf As sembly publicKeyToken). l> jL Boo.Lang.PatternMatching t> |t, Boolang.Useful t> i ByteFX.Data | Commons,Xml.Relaxng * I CorLibDumper 1.0.0.0_61eadc6932beccad *> ;. cscompmgd ConibDumper.dll Date modified: 10/28/200911:34 AM Щт Application extension Size: 4.00 KB Ddte created: 10/28/200911:37 AM Рис. Б.6. Развертывание кодовой библиотеки в Mono GAC На заметку! Опция -1 утилиты gacutil выводит список всех сборок в Mono GAC. Создание консольного приложения в Mono Вашим первым клиентом Mono будет простое консольное приложение с именем ConsoleClientApp.exe. Создайте в папке C:\MonoCode\CorLibDumper новый файл ConsoleClientApp.cs, который содержит следующее определение класса Program:
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1381 // Это клиентское приложение использует CorLibDumper.dll // для вывода информации о типе в файл, using System; using CorLibDumper; namespace ConsoleClientApp { public class Program { public static void Main(string[] args) { Console.WriteLine("***** Приложение для вывода типа *****\п"); // Запрос имени типа у пользователя, string typeName = " "; Console.Write("Введите имя типа: "); typeName = Console.ReadLine (); // Передача его во вспомогательную библиотеку, if(TypeDumper.DumpTypeToFile(typeName)) Console.WriteLine("Данные сохранены в {0}.txt", typeName); else Console.WriteLine("Ошибка! Этот тип не найден..."); } } } Обратите внимание, что метод Main () запрашивает у пользователя полностью определенное имя типа. Метод TypeDumper.DumpTypeToFile () использует введенное пользователем имя для вывода членов типа в локальный файл. Затем нужно создать для этого приложения файл ClientBuild.rsp и указать ссылку на CorLibDumper.dll: /target:exe /out:ConsoleClientApp.exe /reference:CorLibDumper.dll ConsoleClientApp.cs Теперь используйте командную строку Mono для компиляции с помощью mcs: mcs @ClientBuild.rsp Загрузка клиентского приложения в среду выполнения Mono После этого приложение ConsoleClientApp.exe можно загрузить в механизм времени выполнения Mono, указав в качестве аргумента имя исполняемого файла (с расширением *.ехе): mono ConsoleClientApp.exe Для проверки введите в окне командной строки System.Threading.Thread и нажмите клавишу <Enter>. Вы увидите новый файл с именем System.Threading.Thread.txt, который содержит определение метаданных типа (рис. Б.7). Прежде чем перейти к созданию клиента Windows Forms, попробуйте выполнить следующий эксперимент. С помощью Проводника Windows переименуйте сборку CorLibDumper.dll из папки, содержащей клиентское приложение, в DontUseCorLibDumper.dll. Клиентское приложение все равно должно успешно запуститься, т.к. доступ к данной сборке при создании клиента необходим лишь для изменения манифеста клиента. Во время выполнения среда выполнения Mono загрузит версию CorLibDumper.dll, развернутую в Mono GAC. Но если вы попытаетесь выполнить ConsoleClientApp.exe, дважды щелкнув на нем в Проводнике Windows, вы увидите, что сгенерировано исключение
1382 Часть VIII. Приложения FileNotFoundException. На первый взгляд может показаться, что оно возникло из-за переименования сборки CorLibDumper.dll в папке клиентского приложения. Однако настоящей причиной является то, что вы только загрузили ConsoleClientApp.exe в Microsoft CLR! Чтобы выполнить приложение в среде Mono, необходимо передать его механизму выполнения Mono с помощью команды Mono. Иначе сборка будет загружена в Microsoft CLR в предположении, что все общедоступные сборки, установленные в Microsoft GAC, находятся в каталоге <%windir%>\Assembly. На заметку! Двойной щелчок на исполняемом файле в Проводнике Windows загружает эту сборку в Microsoft CLR, а не в механизм выполнения Mono! -> void .стог(тГ -> system.Runtime.Remoting.contexts.context get_Currentcontext() -> iPrincipal get_currentPrincipal() -> void set_currentPrincipa1(lPrincipa"n -> System.Threading.Thread get_currentThread() -> System.LocalDatastoreslot AllocateNamedDataslot(system.string) -> void FreeNamedDatas1ot(system.string) -> System.LocalDatastoreslot AllocateDataslotQ -> system.object GetData(system.LocalDatastoreslot) -> void setData(system.LocalDatastoreslot, system.object) -> system.AppDomain GetDomain() -> lnt32 GetDomainiDO -> system.LocalDatastoreslot GetNamedDataslot(system, string) -> void ResetAbortQ -> void Sleep(int32) • Рис. Б.7. Результат выполнения клиентского приложения Создание клиентской программы Windows Forms Теперь переименуйте файл DontUseCorLibDumper.dll снова в CorLibDumper.dll, иначе следующий пример не сможет скомпилироваться. Затем создайте новый С#-файл с именем WinFormsClientApp.cs и сохраните его в ту же папку, что и текущие файлы проекта. В этом файле определяются два типа классов: using System; using System.Windows.Forms; using CorLibDumper; using System.Drawing; namespace WinFormsClientApp { // Объект приложения, public static class Program { public static void Main(string [ ] args) { Application.Run(new MainWindow ()); } } // Простое окно, public class MainWindow : Form private Button btnDumpToFile = new Button (); private TextBox txtTypeName = new TextBox()j
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1383 public MainWindow () { // Настройка UI. ConfigControls(); } private void ConfigControls () { // Настройка формы. Text = "My Mono Win Forms App' " ; ClientSize = new System.Drawing.SizeC66, 90); StartPosition = FormStartPosition.CenterScreen; AcceptButton = btnDumpToFile; // Настройка кнопки. btnDumpToFile.Text = "Dump"; btnDumpToFile.Location = new System.Drawing.PointA3, 40), // Анонимная обработка события щелчка. btnDumpToFile.Click += delegate { if(TypeDumper.DumpTypeToFile(txtTypeName.Text)) MessageBox.Show(string.Format( "Data saved into {0}.txt", txtTypeName.Text)); else MessageBox.Show("Error! Can't find that type..."); }; Controls.Add(btnDumpToFile); // Настройка текстового поля. txtTypeName.Location = new System.Drawing.PointA3, 13); txtTypeName.Size = new System.Drawing.SizeC41, 20); Controls.Add(txtTypeName); } Для компиляции этого приложения Windows Forms с помощью файла ответов создайте файл с именем WinFormsClientApp.rsp и запишите в него следующие команды: /target:winexe /out:WinFormsClientApp.exe /г:CorLibDumper.dll /г:System.Windows.Forms.dll /r:System.Drawing.dll WinFormsClientApp.cs Теперь сохраните этот файл (обязательно в ту же папку, что и код примера) и передайте его в качестве аргумента компилятору mcs (с помощью элемента @): msc @WinFormsClientApp.rsp И, наконец, запустите полученное приложение Windows Forms с помощью Mono: mono WinFormsClientApp.exe Возможный результат тестового запуска показан на рис. Б.8. E) My Mono Win Forms A 1 | System Math Dump Г" "~£S3¥ ■■■■■■■■■■и j DatasavedintoSystemMath.txt LI OK | l_u^mBJW\ Рис. Б.8. Приложение Windows Forms, созданное с помощью Mono
1384 Часть VIII. Приложения Выполнение приложения Windows Forms под Linux До сих пор в данном приложении демонстрировалось создание сборок, которые можно было бы скомпилировать и с помощью Microsoft .NET Framework 4.0 SDK. Однако важность Mono станет понятнее, если вы взглянете на рис. Б.9, где показано то же самое приложение Windows Forms, работающее под SuSe Linux. Приложение Windows Forms имеет примерно тот же внешний вид (с учетом текущей темы рабочего стола). Рис. Б.9. Выполнение приложения Windows Forms под SuSe Linux Исходный код. Проект CorLibDumper находится в подкаталоге Appendix В. Оказывается, в точности тот же самый С#-код, продемонстрированный в данном приложении, можно откомпилировать и выполнить под Linux (а также под любой другой ОС, поддерживаемой Mono) с помощью тех же средств разработки Mono. Любую сборку, созданную в данном тексте без использования программных конструкций .NET 4.0, можно развернуть или перекомпилировать для другой ОС, совместимой с Mono, а затем выполнить с помощью утилиты времени выполнения Mono. А поскольку все сборки содержат код CIL, не зависящий от платформы, приложения вообще не нужно перекомпилировать. Кто использует Mono? В Данном кратком приложении я попытался обрисовать платформу Mono с помощью нескольких простых примеров. Если вы собираетесь создавать программы .NET только для семейства операционных систем Windows, вы можете и не повстречаться с компаниями или отдельными лицами, которые активно используют Mono. Но все-таки Mono живет и процветает в сообществах программистов для Mac OS X, Linux и Windows. Например, зайдите в раздел /Software веб-сайта Mono: http://mono-project.com/Software Там находится длинный список коммерческих продуктов, созданных с помощью Mono, включая средства разработки, серверные продукты, видеоигры (в том числе игры для Nintendo Wii и iPhone) и медицинские прикладные системы. Если вы не пожалеете времени и пройдете по ссылкам, вы увидите, что Mono отлично оснащена для создания действительно межплатформенного программного обеспечения .NET, которое реально работает вплоть до уровня предприятия. Рекомендации по дальнейшему изучению Если вы внимательно прочитали весь материал, изложенный в данной книге, вы уже много знаете о Mono, т.к. это реализация CLI, совместимая с ЕСМА. Если вы хотите узнать больше об особенностях Mono, то лучшим местом для начала будет официальный вебсайт Mono (www.mono-project.com). Там имеется страница www.mono-project.com/Use, которая может служить отправной точкой для ряда важных тем, в числе которых доступ к базам данных с помощью ADO.NET, веб-разработка с помощью ASP.NET и т.д.
Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1385 Кроме того, для веб-сайта DevX (www.devx.com) мной написана пара статей, которые могут вас заинтересовать: • "Mono IDEs: Going Beyond the Command Line" (Среды разработки Mono: выход за пределы командной строки) — в этой статье рассматриваются многие IDE, пригодные для разработки приложений Mono. • "Building Robust UIs in Mono with Gtk#" (Создание надежных UI в Mono с помощью Gtk#) — здесь описывается создание графических приложений с помощью инструментального набора GTK# в качестве альтернативы Windows Forms. И, наконец, рекомендуется ознакомиться с веб-сайтом документации по Mono (www.go-mono.com/docs). Здесь вы найдете документацию по библиотекам базовых классов Mono, средствам разработки и другим темам (рис. Б. 10). Documentation - Wir; go-mono.com » 3| м"по Doc... x .*f Gmeik Email f... 0 ' Safety w Tools » ф~ Щ Base Class Library [£i Commands and Files S Qandarma Framanork [Э Gnome Libraries S Languages Э Mono Embedding Э Mono Libraries 53 MonoDevelop IDE [±J MonoTouch Framawort (±1 Moonlighf Silverllght Ш Moailla Libraries Й Novell Libraries S3 NUnit Libraries SI Various MONO DOCUMENTATION LIBRARY Pas<? Class щ эгу Commands and Files Gendarme Framework Mono Embedding Mqhq Urjranes MonoDeyetop ЮЕ MonpTonch Framework Nioonlight/gitverHght Mozilla Litxanes NoveJLLifciMies NUnit lifranes Vanous PnntrihufiAn» Ф Internet | Protected Mode Off *% » ^95% » Рис. Б.10. Сайт с документацией по Mono На заметку! Веб-сайт с оперативной документацией по Mono поддерживается сообществом разработчиков. Поэтому не удивляйтесь, если обнаружите пустые ссылки на документы! А поскольку Mono является дистрибутивом Microsoft .NET, совместимым с ЕСМА, то при изучении Mono можно воспользоваться богатой оперативной документацией MSDN. Резюме Данное приложение задумано как введение в межплатформенную природу языка программирования С# и платформы .NET при использовании среды Mono. Вы познакомились с рядом средств командной строки, входящих в состав Mono, которые позволяют создавать различные сборки .NET, в том числе и строго именованные сборки, развернутые в GAC, приложения Windows Forms и кодовые библиотеки .NET. Вы также узнали, что Mono не полностью совместима с API-интерфейсом программирования .NET 3.5 и .NET 4.0 (WPF, WCF, WF или LINQ), а также с возможностями языка С# 2010. Ведется работа (в рамках проекта Olive) по внесению в Mono этих аспектов платформы Microsoft .NET. В любом случае, если вам понадобится создавать приложения .NET, которые должны выполняться под различными операционными системами, то великолепным вариантом для этого будет проект Mono.
Предметный указатель А ADO (Active Data Objects), 754 ADO.NET, 754; 803; 804; 857 API (Application Programming Interface), 45 ASP.NET, 1214; 1225 ASP.NET Profile API, 1319 В BAML (Binary Application Markup Language), 1025 С CIL (Common Intermediate Language), 512 CLI (Common Language Infrastructure), 77; 1370 CLR (Common Language Runtime), 49; 66; 500; 654 CLS (Common Language Specification), 49, 64 COM (Component Object Model), 46 Cookie-набор, 1315 CSE (Corrupted State Exceptions), 290 CTS (Common Type System), 49; 60; 608; 909 D DCOM (Distributed Component Object Model), 907 DLR (Dynamic Language Runtime), 648; 654 DNS (Domain Name Service), 1215 Documents API, 1088 E EF (Entity Framework), 857 Entity SQL, 879 Expression Blend, 1072; 1122; 1166; 1197; 1200 Expression Design, 1128 G GAC (Global Assembly Cache), 73; 494; 1244 GUID (Globally unique identifier), 808 H HTML (Hypertext Markup Language), 1217 HTTP (Hypertest Transfer Protocol), 1214 I IDE (Integrated Development Environment), 80 IL (Intermediate Language), 54 К Kaxaml, 1027 L LINQ (Language Integrated Query), 463 LINQ to DataSet, 468; 851 LINQ to Entities, 468; 859; 878 LINQ to Objects, 463 LINQ to Objects., 468 LINQ to XML, 468; 891 M MDI (Multiple Document Interface), 1340 MFC (Microsoft Foundation Classes), 45 Mono, 1369; 1373 MonoDevelop IDE, 1376 MSIL (Microsoft Intermediate Language), 55 MSMQ (Microsoft Message Queuing), 909 N .NETRemoting, 909 P PIA (Primary Interop Assembly), 660 PLINQ (Parallel LINQ), 468; 708 s SEH (Structured Exception Handling), 265 Silverlight, 1003 SQL Server Express, 91 V VES (Virtual Execution System), 77 Visual Basic, 893 Visual Basic 2010 Express, 90 Visual C# 2010 Express, 90; 91 Visual C++ 2010 Express, 91 Visual Studio 2010, 92; 111; 499; 938; 1049 Object Browser, 68 средство IntelliSense, 445; 651 Visual Web Developer 2010 Express, 90 w WCF (Windows Communication Fbundation), 542; 799; 906; 913 WF (Windows Workflow Foundations), 799; 961 Windows Forms, 819; 886; 1330 под Linux, 1384
WPF (Windows Presentation Foundation), 996; 1047; 1103; 1137; 1170 службы анимации WPF, 1152 службы графической визуализации WPF, 1103 элементы управления WPF, 1049 X XAML (extensible Application Markup Language), 996; 1019 A Адаптер данных, 763; 805; 828 Адрес WCF, 924 Анимация с использованием дискретных ключевых кадров, 1159 с помощью Expression Blend, 1200 Аргумент необязательный, 156 Атрибут контекстный, 604 уровня сборки и модуля, 571 Б База данных ASPNETDB.mdf, 1319 Библиотека MFC, 45 mscoree.dll, 67 Tksk Parallel Library, 700 базовых классов, 50 доступа к данным создание, 785 кода, 500 рабочего потока, 991 Бизнес-процесс, 962 В Ввод-вывод базовый с помощью класса Console, 114 файловый, 711 Веб-приложение, 1214 Веб-сервер, 1216 Веб-страница, 1214 Веб-элемент управления, 1257 ASP.NET, 1265 Выражение запроса (query expression), 144 лямбда, 416; 465 Г Г£аф объекта, 297 Предметный указатель 1387 д Данные адаптер данных, 805 доступ к данным ADO.NET, 803 индексация данных с использованием строковых значений, 423 кэширование данных, 1307 очередизация данных, 909 параллелизм данных, 702 строго типизированные, 143 строковые, 127 фильтрация данных, 479 чтение данных, 783 экземпляра, 198 Действие WF, 971 Делегат, 387; 404 ковариантность делегатов, 400 обобщенный, 402 Дерево выражений, 654 логическое, 1185 Диалоговое окно модальное, 1353 немодальное, 1353 Документ HTML-, 1218 Домен приложения, 582 з Задача параллелизм задач, 705 Запрос Entity SQL, 879 LINQ, 855 LINQ to Entities, 878 И Идентификатор GUID, 808 Имя полностью уточненное, 72 строгое (strong name), 524 Индексация данных с использованием строковых значений, 423 Инкапсуляция, 203; 204 Инструмент Kaxaml, 1027 Интерфейс, 61; 320 API, 45; 906 ASP.NET Profile API, 1319 Documents API, 1088 IAsyncResult, 675 ICloneable, 357 ICollection, 357 ICollection<T>, 369
1388 Предметный указатель IComparable, 350 IComparer<T>, 369 IDataAdapter, 763 IDataParameter, 762 IDataReader, 763; 783 IDataRecord, 763 IDbCommand, 762 IDbConnection, 761 IDbDataAdapter, 763 IDbDataParameter, 762 IDblransaction, 762 IDictionaiy, 357 IDictionaiy<TKey, TValuo, 369 IEnumerable, 357 IEnumerable<T>, 369 IEnumerator, 357 IEnumerator<T>, 369 IFormatter, 739 IList, 357 IList<T>, 369 IRemotingFbrmatter, 739 ISerializable, 749 ISet<T>, 369 LINQtoXML, 893 Task Parallel Library API, 700 полиморфный, 250 Исключение (exception), 266 внутреннее, 286 отладка необработанных исключений с помощью Visual Studio, 289 передача исключений, 285 перехват исключений, 272 связанное с поврежденным состоянием (CSE), 290 уровня системы, 277 Исполняющая среда динамического языка (DLR), 648 Итератор, 343 К Кадр ключевой (keyframe), 1201 Канал именованный (pipe), 913 Класс, 185 Activator, 560 AppDomain, 594 Application, 1005 ApplicationException, 278 Array, 165 ArrayList, 357 AsyncResult, 679 AutoResetEvent, 688 BinaryReader, 731 BinaryWriter, 731 Brush, 1115 CompareValidator, 1284 Component, 1340 Console, 113 ContainerControl, 1340 ContentControl, 1007 ContentPresenter, 1196 Control, 1008; 1260; 1331; 1340; 1341 Convert, 140 DataColumn, 808 DataRow, 811; 845 DataSet, 806; 843 DataTable, 845 DbCommand, 773 DbConnection, 773 DbDataAdapter, 773; 828 DbDataReader, 773 DbParameter, 773 DbTransaction, 773 Delegate, 389; 397 DependencyObject, 1010 Dictionary<TKey, TValuo, 370 Directory, 712 Directorylnfo, 713 DispatcherObject, 1011 Drawing, 1125 DrawingVisual, 1131 Enumerable, 484 Environment, 112 Exception, 268 File, 712 Filelnfo, 713; 719 FileStream, 725 FUeSystemlnfo, 713 Form, 1340; 1343 FrameworkElement, 1009 GC, 300 Geometry, 1112 Graphics, 1358 Hashtable, 357 HttpApplication, 1302 LinkedList<T>, 370 List<T>, 370 MulticastDelegate, 389 Object, 258; 1340 ObjectContext, 863 ObjectSet<T>, 863 Page, 1246 Parallel, 701 ParallelEnumarable, 708 Process, 585 ProcessStartlnfo, 592 ProcessThread, 590 Program, 1337 Queue, 357 Queue<T>, 370; 374 Range Validator, 1284 RegularExpressionValidator, 1284 RequiredFieldValidator, 1283 ScrollableControl, 1340
Предметный указатель 1389 Shape, 1106 SortedDictionaiy<TKey, TValue>, 370 SortedList, 357 SortedSet<T>, 370 Stack, 357 Stack<T>, 370; 373 Stream, 724 StreamReader, 726 StreamWriter, 726 String, 127 StringBuilder, 133 StringReader, 730 StringWriter, 730 SystemException, 277 Task, 703 Thread, 682 Timeline, 1154 Transform, 1118 Type, 548 UIElement, 1010 Visual, 1010; 1130 WebClient, 706 WebControl, 1260; 1264 Window, 1007 WorkflowApplication, 970 Workflowlnvoker, 967 XmlSerializer, 744 абстрактный, 61; 249 базовый (или родительский), 232 запечатанный (sealed), 61; 241 конкретный (concrete), 61 обобщенный, 381 правила приведения к базовому и производному классу, 255 производный (или дочерний), 232 статический, 202 степень видимости, 61 Клиентский профиль, 77 Ключ открытый (public), 525 секретный (private), 525 строгого имени, 525 Ключевое слово as, 257 base, 238 char, 124 checked, 138 class, 61; 185 default, 380 delegate, 387 dynamic, 648; 652; 657 explicit, 436 fixed, 460 implicit, 436 interface, 61 is, 257 lock, 692 namespace, 494 operator, 436 override, 245 partial, 448 protected, 240 sealed, 234 sizeof, 461 stackalloc, 460 static, 197; 440 string, 124 struct, 62 this, 191; 440 unchecked, 139 unsafe, 456 using, 497 var, 451 virtual, 245 where, 383 yield, 343 Ковариантность, 401 Код CIL, 56 метка кода, 615 неуправляемый (unmanaged), 52 отделенный (behind), 1237 управляемый (managed), 52 Компилятор csc.exe, 82 оперативный (JIT), 58 Конкатенация строк, 129 Конструктор, 188 перегрузка, 190 по умолчанию, 120; 188; 190 статический, 201 цепочка конструкторов, 193 Кэш, 1307 загрузки, 506; 539 приложения, 1307 сборок глобальный (GAC), 73; 494; 501; 525; 1244 Л Лямбда-выражение, 416; 465 м Манифест, 55 сборки, 60 уровня модуля, 517 Маркер, 692 отмены (cancellation token), 704 Массивы в С#, 160 зубчатые Gagged), 164 инициализация массива, 161 многомерные, 163 неявно типизированные локальные массивы, 162 определение массива объектов, 163 Мастер-страница, 1257; 1268
1390 Предметный указатель Метаданные, 59 Метка кода, 615 Метод вызов метода асинхронный, 676 -индексатор, 421 многомерный, 425 перегрузка методов, 158 переопределение метода (overriding), 245 расширяющий (extension), 440; 466 частичный, 448 Многопоточность, 670 Модель СОМ, 46 DCOM, 907 Модификатор out, 152 params, 154 ref, 153 Модификаторы доступа С#, 207 по умолчанию, 208 Модуль, 56; 590 первичный или главный (primary), 56 н Наследование, 203; 204; 231 классическое, 232 множественное, 234 о Обобщение (generic), 356 Обработка исключений структурированная, 265 Общая система типов (CTS), 49; 60 Общеязыковая исполняющая среда (CLR), Объект, 185 адаптеров данных, 828 граф объектов, 297; 736 команды, 762 конфигурирование объектов для сериализации, 737 подключения, 761 представления (view object), 826 приложения, 107 сериализация объектов, 711; 734 создание высвобождаемых объектов, 307 финализируемых объектов, 304 транзакции, 762 Оператор С# do/while, 146 for, 145 foreach, 145 if/else, 147 switch, 148 Операция -,428 --, 429 -=, 429 ->, 459 ??, 184 *, 458 &, 458 +,426 ++, 429 +=, 429 перегрузка операций, 426 явного приведения типов, 136 Очередь, 374 финализации, 307 Ошибка пользовательская (user), 265 программная (bug), 265 п Параллелизм данных, 702 задач, 705 Перегрузка операций, 426 Перо (реп), 1118 Платформа Mono, 1373 Поле только для чтения, 228 Полиморфизм, 203; 206 Поток (thread), 670 -демон, 689 переднего плана (foreground), 689 рабочий, 584 фоновый (background), 689 Преобразование пользовательских типов неявное, 434 явное, 434 Приложение WPF, 1001 ХВАР, 1002 Программирование параллельное на платформе .NET, 700 Пространство имен (namespace), 67 Microsoft, 71 в .NET, 70 Протокол HTTP, 1214 Профиль клиентский, 77 Процедура хранимая (stored procedure), 792 Процесс, 582 Р Распаковка (unboxing), 359 Редактор Kaxaml, 1028 Рефакторинг, 96
Предметный указатель 1391 Рефлексия, 547 атрибутов с использованием позднего связывания, 573 с использованием раннего связывания, 573 возвращаемых значений, 554 интерфейсов, 552 методов, 550 обобщенных типов, 554 параметров, 554 полей, 551 разделяемых сборок, 558 свойств, 551 с Сборка (assembly), 52; 501 Microsoft.CSharp.dll, 651 PIA, 660 PresentationCore.dll, 1004 PresentationFoundation.dll, 1004 System.Xaml.dll, 1004 WindowsBase.dll, 1004 WPF, 1004 глобальный кэш сборок, 525 динамическая, 638 динамически загружаемая, 556 загрузка в специальный домен приложений, 600 конфигурирование, 494 манифест сборки, 60; 501; 505 многомофайловая, 56 многофайловая, 505; 516 однофайловая, 56; 505; 506 подключение внешних сборок, 581 подчиненная (satellite), 505 политик издателя, 537 приватная, 519 разделяемая, 524; 531 создание сборки .NET на CIL, 634 Сборка мусора параллельная, 299 Свойство CLR, 1171 автоматическое, 219 зависимости (dependency property), 1170 только для записи, 218 только для чтения, 218 Связывание позднее (late binding), 560 Сервер веб-, 1216 Сериализация объектов, 734 Службы СОМ+/Enterprise Services, 908 DNS, 1215 анимации WPF, 1152 веб-, 910 графической визуализации WPF, 1103 Событие, 406 маршрутизируемое (routed event), 1170 прямое (direct event), 1181 пузырьковое (bubbling event), 1181 туннелируемое (tunneling event), 1181 Сокет, 913 Состояние представления (view state), 1296 Спецификация CLS, 64 CTS, 49 ECMA-334, 1370 ECMA-335, 1370 Среда исполняющая динамического языка (DLR), 654 общеязыковая исполняющая (CLR), 49; 654 Стек виртуальный выполнения (virtual execution stack), 611 Страница веб-, 1214 мастер-, 1257; 1268 содержимого (content page), 1268; 1274 Строка дословная (verabtim), 131 конкатенация строк, 129 Сущности (entities), 859 т Таблица стилей, 1288 Технология ADO.NET, 803 ASP.NET, 1225 Silverlight, 1003 Тип enum, 167 анонимный, 451; 466 вложенный, 208 данных внутренний (intrinsic), 120 встроенный, 63 нулевой (nullable), 181 делегата (delegate), 63 значения, 174; 177 интерфейса, 61 класса (class), 61, 185 CTSbC#,61 контекстно-зависимый (context-bound), 603 контекстно-свободный (context-agile), 603 перечисления (enumeration), 62 преобразование типов неявное, 434 явное, 434 ссылочный (reference), 131; 174 структуры (struct), 62; 172 указателя, 455
1392 Предметный указатель Типизация неявная, 648 Транзакция, 799 У Упаковка (boxing), 358 Управляющие последовательности символов (escape characters), 130 Утилита al.exe, 537 dumpbin.exe, 502 EdmGen.exe, 857; 866 gacutil, 1379; 1380 ilasm.exe, 617 ildasm.exe, 74; 387; 510; 542; 612; 615 monodis, 1379 топор, 1377 msbuild.exe, 1022; 1038 peveriry.exe, 619; 625 reflector.exe, 75; 76; 542 sn.exe, 525 SvcConfigEditor.exe, 948 svcutil.exe, 937 Ф Файл airvehicles.dll, 518 *.aspx, 1239; 1268; 1274 AssemblyInfo.es, 572 *.baml, 1025 *.cs, 1239 *.csproj, 1244 csc.rsp, 86 *.dll, 500; 634 *.edmx, 863; 871 Global.asax, 1299 *.htm, 1218 *.il, 616; 634 ♦.master, 1268; 1274 *.skin, 1289 *.sln, 1244 *.svc, 959 ufo.netmodule, 517 Web.conflg, 959; 1254 *.xaml, 965; 1020 ответный (response), 86 Форма HTML, 1219 Формат BAML, 1025 Форматирование вывода, 115 числовых данных, 116 Функция обратного вызова (callback), 386 X Хеш-код, 262; 525 Хостинг рабочего потока с использованием класса WorkflowApplication, 970 Хранимая процедура (stored procedure), 792 ц Цикл do/while, 146 for, 145 foreach, 145 while, 146 э Экземпляр данные экземпляра, 198 Элемент управления WPF, 1049 веб-, 1257 проверкой достоверности (validation control), 1282 Я Язык Boo, 1375 С#, 50; 106; 666 C++, 45 CIL, 512; 607 Entity SQL, 879 F#, 53 HTML, 1217 Java, 46 MSIL, 55 VB6,45 XAML, 1019; 1030 интегрированных запросов (LINQ), 463 промежуточный (IL), 54